mongoid_lazy_migration 0.2.1 → 0.5.1
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 +105 -120
- data/lib/mongoid/lazy_migration.rb +3 -0
- data/lib/mongoid/lazy_migration/document.rb +19 -11
- data/lib/mongoid/lazy_migration/tasks.rb +10 -4
- data/lib/mongoid/lazy_migration/version.rb +1 -1
- metadata +5 -5
data/README.md
CHANGED
@@ -3,45 +3,50 @@ Mongoid Lazy Migration
|
|
3
3
|
|
4
4
|
[](http://travis-ci.org/nviennot/mongoid_lazy_migration)
|
5
5
|
|
6
|
-
LazyMigration allows you to migrate
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
Once
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
42
|
+
### Constraints
|
29
43
|
|
30
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
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
|
-
|
84
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
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
|
-
|
109
|
-
|
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
|
-
|
138
|
-
|
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
|
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
|
-
|
160
|
+
A rake task is provided:
|
147
161
|
|
148
162
|
```ruby
|
149
|
-
#
|
150
|
-
|
163
|
+
# Migrate all the documents that have a pending migration
|
164
|
+
rake db:mongoid:migrate
|
151
165
|
|
152
|
-
#
|
153
|
-
|
166
|
+
# Migrate all the documents of GroupOfPeople
|
167
|
+
rake db:mongoid:migrate[GroupOfPeople]
|
154
168
|
|
155
|
-
#
|
156
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
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
|
-
|
174
|
-
|
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
|
-
|
180
|
-
|
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
|
-
|
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
|
-
|
215
|
-
|
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,
|
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
|
-
*
|
225
|
-
|
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
|
-
|
241
|
+
Both Mongoid 2.4.x and Mongoid 3.0.x are supported.
|
257
242
|
|
258
243
|
License
|
259
244
|
-------
|
@@ -9,9 +9,9 @@ module Mongoid::LazyMigration::Document
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def atomic_selector
|
12
|
-
return super unless @
|
12
|
+
return super unless @migrating
|
13
13
|
|
14
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
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.
|
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-
|
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
|