mongoid_lazy_migration 0.2

Sign up to get free protection for your applications and to get access to all the features.
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