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 +251 -0
- data/lib/mongoid/lazy_migration.rb +35 -0
- data/lib/mongoid/lazy_migration/document.rb +76 -0
- data/lib/mongoid/lazy_migration/railtie.rb +5 -0
- data/lib/mongoid/lazy_migration/railtie/migrate.rake +15 -0
- data/lib/mongoid/lazy_migration/tasks.rb +33 -0
- data/lib/mongoid/lazy_migration/version.rb +5 -0
- data/lib/mongoid_lazy_migration.rb +1 -0
- metadata +102 -0
data/README.md
ADDED
@@ -0,0 +1,251 @@
|
|
1
|
+
Mongoid Lazy Migration
|
2
|
+
======================
|
3
|
+
|
4
|
+

|
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,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 @@
|
|
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
|