mongoid_lazy_migration 0.2.1 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -3,45 +3,50 @@ Mongoid Lazy Migration
3
3
 
4
4
  [![Build Status](https://secure.travis-ci.org/nviennot/mongoid_lazy_migration.png?branch=master)](http://travis-ci.org/nviennot/mongoid_lazy_migration)
5
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
- -----------
6
+ LazyMigration allows you to migrate a Mongoid collection on the fly. As
7
+ instances of your model are initialized, the migration is run. In the
8
+ background, workers can traverse the collection and migrate other documents.
9
+ Thus, your application acts as though your migration as already taken place.
10
+
11
+ LazyMigration can be used for any app which uses Mongoid. It is most commonly
12
+ used for Rails apps. We use it at [Crowdtap](http://crowdtap.it/about-us).
13
+
14
+
15
+ Workflow
16
+ --------
17
+
18
+ Once installed with
19
+
20
+ ```ruby
21
+ gem 'mongoid_lazy_migration'
22
+ ```
23
+
24
+ You can use the following recipe to perform a migration:
25
+
26
+ 1. Include Mongoid::LazyMigration in your document and write your migration
27
+ specification. Modify your application code to reflect the changes from the
28
+ migration.
29
+ 2. Deploy.
30
+ 3. Run `rake db:mongoid:migrate`.
31
+ 4. Remove the migration block from your model.
32
+ 5. Deploy.
33
+ 6. Run `rake db:mongoid:cleanup[Model]`.
34
+
35
+ The migration specification can be written in one of two modes: *atomic* or
36
+ *locked*. Atomic is the default, and is tolerant to dying workers, but it
37
+ introduces some constraints on your migration.
38
+
39
+ Atomic Mode (default)
40
+ ---------------------
27
41
 
28
- Your migration code must respect a few constrains:
42
+ ### Constraints
29
43
 
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.
44
+ Atomic migration code must respect a few constrains:
36
45
 
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.
46
+ 1. It can never write anything to the database. This means you should not call
47
+ save yourself; LazyMigration will do this for you. Essentially, you should
48
+ only be setting document fields.
49
+ 2. It must be deterministic to ensure consistency in the face of migration races.
45
50
 
46
51
  ### Migration States
47
52
 
@@ -53,7 +58,7 @@ A document migration goes through the following states:
53
58
  ### Atomic Migration Example
54
59
 
55
60
  Suppose we have a model GroupOfPeople with two fields: num_males and
56
- num_females. We wish to add a field num_people. Here is how it goes:
61
+ num_females. We wish to add the field num_people. We can do this as follows:
57
62
 
58
63
  ```ruby
59
64
  class GroupOfPeople
@@ -68,6 +73,8 @@ class GroupOfPeople
68
73
  self.num_people = num_males + num_females
69
74
  end
70
75
 
76
+ # This can be used by other code while the migration is running
77
+ # because it is atomic
71
78
  def inc_num_people
72
79
  self.inc(:num_people, 1)
73
80
  end
@@ -80,19 +87,18 @@ because only one client will be able to commit the migration to the database.
80
87
  Locked Mode
81
88
  -----------
82
89
 
83
- The locked mode guarantees that only one client will run the migration code,
84
- because a lock is taken. As a consequence, your migration code can pretty much
85
- do anything.
90
+ Locked mode guarantees that only one client will run the migration code, because
91
+ it uses locking. Thus, the restrictions on an atomic migration are removed.
86
92
 
87
- In case of contention, the other clients will have to wait until the migration
88
- is complete. This has some heavy consequences: if the owner of the lock dies
89
- (exception, ctrl+c, lost the ssh connection, asteroid crashes on your
90
- datacenter, ...) the document will stay locked in the processing state. There
91
- is no rollback. You are responsible to clean up the mess. Be aware that you
92
- cannot instantiate a document stuck in locked state state without removing
93
- the migration block.
93
+ However, this has some consequences. Most importantly, if the owner of the lock
94
+ dies (exception, ctrl+c, a lost ssh connection, an explosion in your
95
+ datacenter, etc.), the document will stay locked in the processing state.
96
+ There is no automatic rollback. You are responsible for cleaning up. Be aware
97
+ that you cannot instantiate a document stuck in locked state state without
98
+ removing the migration block.
94
99
 
95
- Because of the locking, a locked migration runs slower than an atomic one.
100
+ Because the lock involves additional database requests, including writes,
101
+ a locked migration runs slower than an atomic one.
96
102
 
97
103
  ### Migration States
98
104
 
@@ -105,8 +111,9 @@ A document migration goes through the following states:
105
111
  ### Locked Migration Example
106
112
 
107
113
  Suppose we have a model GroupOfPeople with an array people_names, which is an
108
- array of strings. Our migration consists of introducing a new model Person, and
109
- removing the array people_names from GroupOfPeople. Here it goes:
114
+
115
+ an array of strings. Our migration consists of introducing a new model called
116
+ Person and removing the array people_names from GroupOfPeople.
110
117
 
111
118
  ```ruby
112
119
  class Person
@@ -131,56 +138,56 @@ class GroupOfPeople
131
138
  end
132
139
  ```
133
140
 
141
+ We cannot perform an atomic migration in this case because new documents are
142
+ created while running the migration block. In locked mode, we are guaranteed
143
+ that we only create the associations once.
144
+
134
145
  Since only one client can execute the migration block, we are guaranteed that
135
146
  we only create the associations once.
136
147
 
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.
148
+ Notice that we don't unset the people_names field in the migration. We keep it
149
+ until the entire collection has migrated allowing us to rollback in case of
150
+ failure.
139
151
 
140
152
  Background Migration
141
153
  --------------------
142
154
 
143
- While the frontend servers are migrating models on demand, it is recommended to
144
- migrate the rest of the collection in the background.
155
+ While the some of your server migrating models on demand, it is recommended to
156
+ migrate the rest of the collection in the background. Otherwise, if one
157
+ document in your collection is not accessed for a year, your migration will
158
+ take one year to complete.
145
159
 
146
- It can be done programmatically:
160
+ A rake task is provided:
147
161
 
148
162
  ```ruby
149
- # migrate all the models that have a migration block.
150
- Mongoid::LazyMigration.migrate
163
+ # Migrate all the documents that have a pending migration
164
+ rake db:mongoid:migrate
151
165
 
152
- # migrate only Model.
153
- Mongoid::LazyMigration.migrate(Model)
166
+ # Migrate all the documents of GroupOfPeople
167
+ rake db:mongoid:migrate[GroupOfPeople]
154
168
 
155
- # migrate only active documents of Model.
156
- Mongoid::LazyMigration.migrate(Model.where(:active => true))
169
+ # Migrate all the documents of GroupOfPeople that have specific people in the group
170
+ rake db:mongoid:migrate[GroupOfPeople.where(:people_names.in => ["marie", "joelle"])]
157
171
  ```
158
172
 
159
- A rake task is provided wrapping the migrate method:
173
+ Note that you might need to use single quotes around the whole rake argument,
174
+ otherwise your shell might be tempted to evaluate the expression.
160
175
 
161
- ```ruby
162
- rake db:mongoid:migrate
163
- rake db:mongoid:migrate[Model]
164
- rake db:mongoid:migrate[Model.where(:active => true)]
165
- ```
176
+ The task displays a progress bar.
166
177
 
167
- Both methods display a progress bar.
178
+ For performance, it is recommended to migrate the most accessed documents first
179
+ so they don't need to be migrated when a user requests them. You can also use
180
+ multiple workers on different shards.
168
181
 
169
- It is recommended to migrate the most accessed documents first so they don't
170
- need to be migrated when a user requests them. It is also possible to use
171
- multiple workers. Example:
182
+ If your database doesn't fit entirely in memory, be very careful when migrating
183
+ rarely accessed documents since your working set may be evicted from cache.
184
+ MongoDB could start trashing in unexpected ways.
172
185
 
173
- Worker 1:
174
- ```ruby
175
- rake db:mongoid:migrate[User.where(:shard => 1, :active => true)]
176
- rake db:mongoid:migrate[User.where(:shard => 1, :active => false)]
177
- ```
186
+ The migration is be performed one document at a time, so we avoid holding up
187
+ the global lock on MongoDB for long period of time.
178
188
 
179
- Worker 2:
180
- ```ruby
181
- rake db:mongoid:migrate[User.where(:shard => 2, :active => false)]
182
- rake db:mongoid:migrate[User.where(:shard => 2, :active => true)]
183
- ```
189
+ Only atomic migration can be safely aborted with Ctrl+C. Support for aborting a
190
+ locked migration will be added in the future.
184
191
 
185
192
  Cleaning up
186
193
  -----------
@@ -189,71 +196,49 @@ Once all the document have their migration state equal to done, you must do two
189
196
 
190
197
  1. Remove the migration block from your model.
191
198
  2. Cleanup the database (removing the migration_state field) to ensure that you
192
- can run a future migration.
193
-
194
- The two ways to cleanup a collection are:
199
+ can run a future migration with the following rake task:
195
200
 
196
201
  ```ruby
197
- # programmatically
198
- Mongoid::LazyMigration.cleanup(Model)
199
-
200
- # rake task
201
202
  rake db:mongoid:cleanup_migration[Model]
202
203
  ```
203
204
 
204
205
  The cleanup process will be aborted if any of the document migrations are still
205
- in the processing state, or if you haven't removed the migration block.
206
+ the processing state or if you haven't removed the migration block.
206
207
 
207
208
  Important Considerations
208
209
  ------------------------
209
210
 
210
211
  A couple of points that you need to be aware of:
211
212
 
213
+ * save() will be called on the document once done with the migration. Don't
214
+ do it yourself. It's racy in atomic mode and unnecessary in locked mode.
215
+ * save and update callbacks will not be called when persisting the migration.
216
+ * No validation will be performed during the migration.
217
+ * The migration will not update the `updated_at` field.
218
+ * LazyMigration is designed to support a single production environment and does
219
+ not support versioning, unlike traditional ActiveRecord migrations. Thus, the
220
+ migration code can be included directly in the model and can be removed after
221
+ migration is done.
212
222
  * Because the migrations are run during the model instantiation,
213
223
  using some query like `Member.count` will not perform any migration.
214
- With the same rational, If you do something sneaky in your application by
215
- interacting directly with the mongo driver and bypassing mongoid, you are on
216
- your own.
224
+ Similarly, if you bypass Mongoid and use the Mongo driver directly in your
225
+ application, LazyMigration might not be run.
217
226
  * If you use only() in some of your queries, make sure that you include the
218
227
  migration_state field.
219
228
  * Do not use identity maps and threads at the same time. It is currently
220
- unsupported, but support may be added in the future.
229
+ unsupported, though support may be added in the future.
221
230
  * Make sure you can migrate a document quickly, because migrations will be
222
231
  performed while processing user requests.
223
232
  * SlaveOk is fine, even in locked mode.
224
- * save() will be called on the document once done with the migration, don't
225
- do it yourself.
226
- * The save and update callbacks will not be called when persisting the
227
- migration.
228
- * No validation will be performed during the migration.
229
- * The migration will not update the updated_at field.
230
-
231
- Workflow
232
- --------
233
-
234
- First, add the following line in your Gemfile:
235
-
236
- ```ruby
237
- gem 'mongoid_lazy_migration'
238
- ```
239
-
240
- Follow the following steps to perform a migration:
241
-
242
- 1. Run `rake db:mongoid:cleanup[Model]` just to be safe.
243
- 2. Include `Mongoid::LazyMigration` in your document and write your migration.
244
- Modify your application code to reflect the changes from the migration.
245
- 3. Deploy.
246
- 4. Run `rake db:mongoid:migrate`.
247
- 5. Remove the migration block from your model.
248
- 6. Deploy.
249
- 7. Run `rake db:mongoid:cleanup[Model]`.
233
+ * You might want to try a migration on a staging environment which replicates a
234
+ production workload to evaluate the impact of the lazy migration.
250
235
 
251
236
  Compatibility
252
237
  -------------
253
238
 
254
239
  LazyMigration is tested against against MRI 1.8.7, 1.9.2, 1.9.3, JRuby-1.8, JRuby-1.9.
255
240
 
256
- Only Mongoid 2.4.x is currently supported.
241
+ Both Mongoid 2.4.x and Mongoid 3.0.x are supported.
257
242
 
258
243
  License
259
244
  -------
@@ -11,6 +11,9 @@ module Mongoid
11
11
  extend ActiveSupport::Concern
12
12
  extend Tasks
13
13
 
14
+ mattr_reader :mongoid3
15
+ @@mongoid3 = Gem.loaded_specs['mongoid'].version >= Gem::Version.new('3.0.0')
16
+
14
17
  mattr_reader :models_to_migrate
15
18
  @@models_to_migrate = Set.new
16
19
 
@@ -9,9 +9,9 @@ module Mongoid::LazyMigration::Document
9
9
  end
10
10
 
11
11
  def atomic_selector
12
- return super unless @running_migrate_block
12
+ return super unless @migrating
13
13
 
14
- unless self.class.lock_migration
14
+ if @running_migrate_block && !self.class.lock_migration
15
15
  raise ["You cannot save during an atomic migration,",
16
16
  "You are only allowed to set the document fields",
17
17
  "The document will be commited once the migration is complete.",
@@ -19,20 +19,22 @@ module Mongoid::LazyMigration::Document
19
19
  ].join("\n")
20
20
  end
21
21
 
22
+ # see perform migration
22
23
  super.merge(:migration_state => { "$ne" => :done })
23
24
  end
24
25
 
25
26
  def run_callbacks(*args, &block)
26
- if @migrating
27
- block.call if block
28
- else
29
- super(*args, &block)
30
- end
27
+ return super(*args, &block) unless @migrating
28
+
29
+ block.call if block
31
30
  end
32
31
 
33
32
  private
34
33
 
35
34
  def perform_migration
35
+ # For locked migrations, perform_migration is bypassed when raced by
36
+ # another client. We busy wait until the other client is done with the
37
+ # migration in wait_for_completion.
36
38
  return if self.class.lock_migration && !try_lock_migration
37
39
 
38
40
  begin
@@ -63,10 +65,16 @@ module Mongoid::LazyMigration::Document
63
65
  # transitions from the pending to the progress state. This operation is
64
66
  # done atomically by MongoDB (test and set).
65
67
  self.migration_state = :processing
66
- collection.update(
67
- atomic_selector.merge(:migration_state => { "$in" => [nil, :pending] }),
68
- { "$set" => { :migration_state => :processing }},
69
- { :safe => true })['updatedExisting']
68
+
69
+ selector = atomic_selector.merge(:migration_state => { "$in" => [nil, :pending] })
70
+ changes = { "$set" => { :migration_state => :processing }}
71
+ safety = { :safe => true }
72
+
73
+ if Mongoid::LazyMigration.mongoid3
74
+ self.class.with(safety).where(selector).query.update(changes)
75
+ else
76
+ collection.update( selector, changes, safety)
77
+ end['updatedExisting']
70
78
  end
71
79
 
72
80
  def wait_for_completion
@@ -25,9 +25,15 @@ module Mongoid::LazyMigration::Tasks
25
25
  "Don't forget to remove the migration block"].join("\n")
26
26
  end
27
27
 
28
- model.collection.update(
29
- { :migration_state => { "$exists" => true }},
30
- {"$unset" => { :migration_state => 1}},
31
- { :safe => true, :multi => true })
28
+ selector = { :migration_state => { "$exists" => true }}
29
+ changes = {"$unset" => { :migration_state => 1}}
30
+ safety = { :safe => true, :multi => true }
31
+ multi = { :multi => true }
32
+
33
+ if Mongoid::LazyMigration.mongoid3
34
+ model.with(safety).where(selector).query.update(changes, multi)
35
+ else
36
+ model.collection.update(selector, changes, safety.merge(multi))
37
+ end
32
38
  end
33
39
  end
@@ -1,5 +1,5 @@
1
1
  module Mongoid
2
2
  module LazyMigration
3
- VERSION = "0.2.1"
3
+ VERSION = "0.5.1"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid_lazy_migration
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.5.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,14 +9,14 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-07-10 00:00:00.000000000 Z
12
+ date: 2012-07-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mongoid
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
- - - ~>
19
+ - - ! '>='
20
20
  - !ruby/object:Gem::Version
21
21
  version: '2.4'
22
22
  type: :runtime
@@ -24,7 +24,7 @@ dependencies:
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  none: false
26
26
  requirements:
27
- - - ~>
27
+ - - ! '>='
28
28
  - !ruby/object:Gem::Version
29
29
  version: '2.4'
30
30
  - !ruby/object:Gem::Dependency
@@ -69,9 +69,9 @@ files:
69
69
  - lib/mongoid_lazy_migration.rb
70
70
  - lib/mongoid/lazy_migration/version.rb
71
71
  - lib/mongoid/lazy_migration/tasks.rb
72
+ - lib/mongoid/lazy_migration/document.rb
72
73
  - lib/mongoid/lazy_migration/railtie/migrate.rake
73
74
  - lib/mongoid/lazy_migration/railtie.rb
74
- - lib/mongoid/lazy_migration/document.rb
75
75
  - lib/mongoid/lazy_migration.rb
76
76
  - README.md
77
77
  homepage: http://github.com/nviennot/mongoid_lazy_migration