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 +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
|
[![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
|
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
|