mongoid_lazy_migration 0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,251 @@
1
+ Mongoid Lazy Migration
2
+ ======================
3
+
4
+ ![Build Status](https://secure.travis-ci.org/nviennot/mongoid_lazy_migration.png?branch=master)
5
+
6
+ LazyMigration allows you to migrate your document on the fly.
7
+ The migration is run when an instance of your model is initialized.
8
+ While your frontend servers are migrating the documents on demand, some workers
9
+ traverse the collection to migrate all the documents.
10
+
11
+ It allows your application logic to be tolerant from having only a portion of
12
+ the collection migrated. We use it at [Crowdtap](http://crowdtap.it/about-us).
13
+
14
+ LazyMigration is designed to support only one production environment. There are
15
+ no versioning as opposed to traditional ActiveRecord migrations.
16
+ With this in mind, the migration code is directly implemented in the model
17
+ itself, as opposed to having a decorator.
18
+ Once the migration is complete, the migration code must be deleted, and code
19
+ must be redeployed before cleaning up the database.
20
+
21
+ LazyMigration has two modes of operations: *atomic* and *locked*.
22
+ Atomic is the default one, which is less prone to fuck ups, but adds
23
+ constrains on what you can do during your migration.
24
+
25
+ Atomic Mode
26
+ -----------
27
+
28
+ Your migration code must respect a few constrains:
29
+
30
+ 1. Your migrate code should never write anything to the database.
31
+ You should not call save yourself, but let LazyMigration do it.
32
+ Basically you are only allowed to set some document fields.
33
+ 2. Your migrate code should be deterministic (don't play with Random)
34
+ to keep some sort of consistency even in the presence of races with
35
+ migrations.
36
+
37
+ When multiple clients are trying to perform the migration at the same
38
+ time, all of them will run the migration code, but only one will commit
39
+ the changes to the database.
40
+ After the migration, the document will not be reloaded, regardless if the
41
+ write to the database happened. This implies that the field values are
42
+ the one from the migration, and may not correspond to what is actually in
43
+ the database. Which is why using a non deterministic migration is not
44
+ recommended.
45
+
46
+ ### Migration States
47
+
48
+ A document migration goes through the following states:
49
+
50
+ 1. pending: the migration needs to be performed.
51
+ 2. done: the migration is complete.
52
+
53
+ ### Atomic Migration Example
54
+
55
+ Suppose we have a model `GroupOfPeople` with two fields: `num_males` and
56
+ `num_females`.
57
+ We wish to add a field `num_people`. Here is how it goes:
58
+
59
+ ```ruby
60
+ class GroupOfPeople
61
+ include Mongoid::Document
62
+ include Mongoid::LazyMigration
63
+
64
+ field :num_males, :type => Integer
65
+ field :num_females, :type => Integer
66
+ field :num_people, :type => Integer
67
+
68
+ migration do
69
+ self.num_people = num_males + num_females
70
+ end
71
+
72
+ def inc_num_people
73
+ self.inc(:num_people, 1)
74
+ end
75
+ end
76
+ ```
77
+
78
+ Note that calling `inc_num_people` is perfectly fine in presence of contention
79
+ because only one client will be able to commit the migration to the database.
80
+
81
+ Locked Mode
82
+ -----------
83
+
84
+ The locked mode guarantees that only one client will run the migration code,
85
+ because a lock is taken. As a consequence, your migration code can pretty much
86
+ do anything.
87
+
88
+ In case of contention, the other clients will have to wait until the migration
89
+ is complete. This has some heavy consequences: if the owner of the lock dies
90
+ (exception, ctrl+c, lost the ssh connection, asteroid crashes on your
91
+ datacenter, ...) the document will stay locked in the processing state. There
92
+ is no rollback. You are responsible to clean up the mess. Be aware that you
93
+ cannot instantiate a document stuck in locked state state without removing
94
+ the migration block.
95
+
96
+ Because of the locking, a locked migration runs slower than an atomic one.
97
+
98
+ ### Migration States
99
+
100
+ A document migration goes through the following states:
101
+ 1. pending: the migration needs to be performed.
102
+ 2. processing: the document is being migrated, blocking other clients.
103
+ 3. done: the migration is complete.
104
+
105
+ ### Locked Migration Example
106
+
107
+ Suppose we have a model `GroupOfPeople` with an array `people_names`, which is
108
+ an array of strings. Our migration consists of introducing a new model `Person`,
109
+ and removing the array `people_names` from `GroupOfPeople`. Here it goes:
110
+
111
+ ```ruby
112
+ class Person
113
+ include Mongoid::Document
114
+
115
+ field :name, :type => String
116
+ belongs_to :group_of_people
117
+ end
118
+
119
+ class GroupOfPeople
120
+ include Mongoid::Document
121
+ include Mongoid::LazyMigration
122
+
123
+ field :people_names, :type => Array
124
+ has_many :people
125
+
126
+ migration(:lock => true) do
127
+ people.names.each do |name|
128
+ self.people.create(:name => name)
129
+ end
130
+ end
131
+ end
132
+ ```
133
+
134
+ Since only one client can execute the migration block, we are guaranteed that
135
+ we only create the associations once.
136
+
137
+ Just to be safe, we don't unset the `people_names` in the migration. It would
138
+ be done manually once the migration is complete.
139
+
140
+ Background Migration
141
+ --------------------
142
+
143
+ While the frontend servers are migrating models on demand, it is recommended to
144
+ migrate the rest of the collection in the background.
145
+
146
+ It can be done programmatically:
147
+
148
+ * `Mongoid::LazyMigration.migrate` will migrate all the models that have a
149
+ migration block.
150
+ * `Mongoid::LazyMigration.migrate(Model)` will only migrate `Model`.
151
+ * `Mongoid::LazyMigration.migrate(Model.where(:active => true))` will only
152
+ migrate the active models.
153
+
154
+ A rake task is provided to call the migrate() method:
155
+
156
+ * `rake db:mongoid:migrate`
157
+ * `rake db:mongoid:migrate[Model]`
158
+ * `rake db:mongoid:migrate[Model.where(:active => true)]`
159
+
160
+ Both methods display a progress bar.
161
+
162
+ It is recommended to migrate the most accessed documents first so they don't
163
+ need to be migrated when a user requests them. It is also possible to use
164
+ multiple workers. Example:
165
+
166
+ Worker 1:
167
+ ```ruby
168
+ rake db:mongoid:migrate[User.where(:shard => 1, :active => true)]
169
+ rake db:mongoid:migrate[User.where(:shard => 1, :active => false)]
170
+ ```
171
+
172
+ Worker 2:
173
+ ```ruby
174
+ rake db:mongoid:migrate[User.where(:shard => 2, :active => false)]
175
+ rake db:mongoid:migrate[User.where(:shard => 2, :active => true)]
176
+ ```
177
+
178
+ Cleaning up
179
+ -----------
180
+
181
+ Once all the document have their migration state equal to done, you must
182
+ cleanup your collection (removing the migration_state field) to clear some
183
+ database space, and to ensure that you can run a future migration.
184
+
185
+ The two ways to cleanup a model are:
186
+
187
+ * `Mongoid::LazyMigration.cleanup(Model)`
188
+ * `rake db:mongoid:cleanup_migration[Model]`
189
+
190
+ The cleanup process will abort if any of the document migrations are still in
191
+ the processing state, or if you haven't removed the migration block.
192
+
193
+ Important Considerations
194
+ ------------------------
195
+
196
+ A couple of points that you need to be aware of:
197
+
198
+ * Because the migrations are run during the model instantiation,
199
+ using some query like `Member.count` will not perform any migration.
200
+ With the same rational, If you do something sneaky in your application by
201
+ interacting directly with the mongo driver and bypassing mongoid, you are on
202
+ your own.
203
+ * If you use only() in some of your queries, make sure that you include the
204
+ `migration_state` field.
205
+ * Do not use identity maps and threads at the same time. It is currently
206
+ unsupported, but support may be added in the future.
207
+ * Make sure you can migrate a document quickly, because migrations will be
208
+ performed while processing user requests.
209
+ * SlaveOk is fine, even in locked mode.
210
+ * save() will be called on the document once done with the migration, don't
211
+ do it yourself.
212
+ * The save and update callbacks will not be called when persisting the
213
+ migration.
214
+ * No validation will be performed during the migration.
215
+ * The migration will not update the `updated_at` field.
216
+
217
+ You can pass a criteria to `Mongoid::LazyMigration.migrate` (used at step 4
218
+ of the recipe). It allows you to:
219
+
220
+ * Update your most important document first (like your active members),
221
+ and do the rest after.
222
+ * Use many workers on different shards of your database. Note that it is
223
+ safe to try migrating the same document by different workers, but not
224
+ recommended for performance reasons.
225
+
226
+ Workflow
227
+ --------
228
+
229
+ You may use the following recepe to perform a migration:
230
+
231
+ 1. Run `rake db:mongoid:cleanup[Model]` just to be safe.
232
+ 2. Include Mongoid::LazyMigration in your document and write your migration
233
+ specification. Modify your application code to reflect the changes from the
234
+ migration.
235
+ 3. Deploy.
236
+ 4. Run `rake db:mongoid:migrate`.
237
+ 5. Remove the migration block from your model.
238
+ 6. Deploy.
239
+ 7. Run `rake db:mongoid:cleanup[Model]`.
240
+
241
+ Compatibility
242
+ -------------
243
+
244
+ LazyMigration is tested against against MRI 1.8.7, 1.9.2, 1.9.3, JRuby-1.8, JRuby-1.9.
245
+
246
+ Only Mongoid 2.4.x is currently supported.
247
+
248
+ License
249
+ -------
250
+
251
+ LazyMigration is distributed under the MIT license.
@@ -0,0 +1,35 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/core_ext'
3
+ require 'set'
4
+
5
+ module Mongoid
6
+ module LazyMigration
7
+ require 'mongoid/lazy_migration/version'
8
+ require 'mongoid/lazy_migration/document'
9
+ require 'mongoid/lazy_migration/tasks'
10
+
11
+ extend ActiveSupport::Concern
12
+ extend Tasks
13
+
14
+ mattr_reader :models_to_migrate
15
+ @@models_to_migrate = Set.new
16
+
17
+ module ClassMethods
18
+ def migration(options = {}, &block)
19
+ include Mongoid::LazyMigration::Document
20
+
21
+ field :migration_state, :type => Symbol, :default => :pending
22
+ after_initialize :ensure_migration, :unless => proc { @migrating }
23
+
24
+ cattr_accessor :migrate_block, :lock_migration
25
+ self.migrate_block = block
26
+ self.lock_migration = options[:lock]
27
+
28
+ Mongoid::LazyMigration.models_to_migrate << self
29
+ end
30
+ end
31
+
32
+ end
33
+ end
34
+
35
+ require "mongoid/lazy_migration/railtie" if defined?(Rails)
@@ -0,0 +1,76 @@
1
+ module Mongoid::LazyMigration::Document
2
+ def ensure_migration
3
+ self.migration_state = :done if new_record?
4
+
5
+ @migrating = true
6
+ perform_migration if migration_state == :pending
7
+ wait_for_completion if migration_state == :processing
8
+ @migrating = false
9
+ end
10
+
11
+ def atomic_selector
12
+ if @running_migrate_block && !self.class.lock_migration
13
+ raise ["You cannot save during an atomic migration,",
14
+ "You are only allowed to set the document fields",
15
+ "The document will be commited once the migration is complete.",
16
+ "If you need to get fancy, like creating associations, use :lock => true"
17
+ ].join("\n")
18
+ end
19
+
20
+ super.merge(:migration_state => { "$ne" => :done })
21
+ end
22
+
23
+ def run_callbacks(*args, &block)
24
+ if @migrating
25
+ block.call if block
26
+ else
27
+ super(*args, &block)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def perform_migration
34
+ return if self.class.lock_migration && !try_lock_migration
35
+
36
+ begin
37
+ self.class.skip_callback :create, :update
38
+ Mongoid::Threaded.timeless = true
39
+
40
+ @running_migrate_block = true
41
+ instance_eval(&self.class.migrate_block)
42
+ @running_migrate_block = false
43
+
44
+ # For atomic migrations, save() will be performed with the
45
+ # following additional selector (see atomic_selector):
46
+ # :migration_state => { "$ne" => :done }
47
+ # This guarantee that we never commit more than one migration on a
48
+ # document, even though we are not taking a lock.
49
+ # Since we do not call reload on the model in case of a race, it is
50
+ # preferable to have a deterministic migration to avoid surprises.
51
+ self.migration_state = :done
52
+ save(:validate => false)
53
+ ensure
54
+ Mongoid::Threaded.timeless = false
55
+ self.class.set_callback :create, :update
56
+ end
57
+ end
58
+
59
+ def try_lock_migration
60
+ # try_lock_migration returns true if and if only the document
61
+ # transitions from the pending to the progress state. This operation is
62
+ # done atomically by MongoDB (test and set).
63
+ self.migration_state = :processing
64
+ collection.update(
65
+ atomic_selector.merge(:migration_state => { "$in" => [nil, :pending] }),
66
+ { "$set" => { :migration_state => :processing }},
67
+ { :safe => true })['updatedExisting']
68
+ end
69
+
70
+ def wait_for_completion
71
+ # We do not explicitly sleep during the loop in the hope of getting a
72
+ # lower latency. reload() sleeps anyway waiting for mongodb to respond.
73
+ # Besides, this is a corner case since contention should be very low.
74
+ reload until migration_state == :done
75
+ end
76
+ end
@@ -0,0 +1,5 @@
1
+ module Mongoid::LazyMigration
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks { load 'mongoid/lazy_migration/railtie/migrate.rake' }
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ namespace :db do
2
+ namespace :mongoid do
3
+ desc 'Migrate the documents specified by criteria. criteria is optional'
4
+ task :migrate, [:criteria] => :environment do |t, args|
5
+ criteria = args.criteria ? eval(args.criteria) : nil
6
+ Mongoid::LazyMigration.migrate(criteria)
7
+ end
8
+
9
+ desc 'Cleanup a migration'
10
+ task :cleanup_migration, [:model] => :environment do |t, args|
11
+ raise "Please provide a model" unless args.model
12
+ Mongoid::LazyMigration.cleanup(eval(args.model))
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,33 @@
1
+ module Mongoid::LazyMigration::Tasks
2
+ def migrate(criteria=nil)
3
+ require 'progressbar'
4
+
5
+ criterias = criteria.nil? ? Mongoid::LazyMigration.models_to_migrate : [criteria]
6
+ criterias.each do |criteria|
7
+ to_migrate = criteria.where(:migration_state.ne => :done)
8
+ progress = ProgressBar.new(to_migrate.klass.to_s, to_migrate.count)
9
+ progress.long_running
10
+ to_migrate.each { progress.inc }
11
+ progress.finish
12
+ end
13
+ true
14
+ end
15
+
16
+ def cleanup(model)
17
+ if model.in? Mongoid::LazyMigration.models_to_migrate
18
+ raise "Remove the migration from your model before cleaning up the database"
19
+ end
20
+
21
+ if model.where(:migration_state => :processing).limit(1).count > 0
22
+ raise ["Some models are still being processed.",
23
+ "Remove the migration code, and go inspect them with:",
24
+ "#{model}.where(:migration_state => :processing))",
25
+ "Don't forget to remove the migration block"].join("\n")
26
+ end
27
+
28
+ model.collection.update(
29
+ { :migration_state => { "$exists" => true }},
30
+ {"$unset" => { :migration_state => 1}},
31
+ { :safe => true, :multi => true })
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ module Mongoid
2
+ module LazyMigration
3
+ VERSION = 0.2
4
+ end
5
+ end
@@ -0,0 +1 @@
1
+ require 'mongoid/lazy_migration'
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongoid_lazy_migration
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.2'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Nicolas Viennot
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mongoid
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '2.4'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '2.4'
30
+ - !ruby/object:Gem::Dependency
31
+ name: activesupport
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: progressbar
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: Migrate your documents lazily in atomic, or locked fashion to avoid downtime
63
+ email:
64
+ - nicolas@viennot.biz
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - lib/mongoid/lazy_migration/version.rb
70
+ - lib/mongoid/lazy_migration/railtie.rb
71
+ - lib/mongoid/lazy_migration/railtie/migrate.rake
72
+ - lib/mongoid/lazy_migration/tasks.rb
73
+ - lib/mongoid/lazy_migration/document.rb
74
+ - lib/mongoid/lazy_migration.rb
75
+ - lib/mongoid_lazy_migration.rb
76
+ - README.md
77
+ homepage: http://github.com/nviennot/mongoid_lazy_migration
78
+ licenses: []
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ! '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubyforge_project:
97
+ rubygems_version: 1.8.24
98
+ signing_key:
99
+ specification_version: 3
100
+ summary: Mongoid lazy migration toolkit
101
+ test_files: []
102
+ has_rdoc: false