mongoid_optimistic_locking 0.0.2 → 0.0.3
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/.travis.yml +0 -1
- data/Gemfile +0 -1
- data/README.md +26 -3
- data/lib/mongoid/optimistic_locking/operations.rb +13 -11
- data/lib/mongoid/optimistic_locking/version.rb +1 -1
- data/mongoid_optimistic_locking.gemspec +1 -2
- data/spec/optimistic_locking_spec.rb +234 -44
- data/spec/spec_helper.rb +2 -1
- metadata +22 -18
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -6,9 +6,9 @@ The gem is an addon over [Mongoid ODM](http://mongoid.org/) and is based on [Act
|
|
6
6
|
|
7
7
|
## Compatibility
|
8
8
|
|
9
|
-
|
9
|
+
Works with *Mongoid 3*.
|
10
10
|
|
11
|
-
|
11
|
+
For Mongoid 2 use version 0.0.2
|
12
12
|
|
13
13
|
## Rails 3 Installation
|
14
14
|
|
@@ -23,7 +23,6 @@ To use it, all you have to do is add include `Mongoid::OptimisticLocking`:
|
|
23
23
|
class Post
|
24
24
|
include Mongoid::Document
|
25
25
|
include Mongoid::OptimisticLocking
|
26
|
-
|
27
26
|
field :text
|
28
27
|
end
|
29
28
|
|
@@ -47,6 +46,28 @@ For example:
|
|
47
46
|
|
48
47
|
That's it!
|
49
48
|
|
49
|
+
## Embedded Document Caveats
|
50
|
+
|
51
|
+
While `Mongoid::OptimisticLocking` can be used to some degree within embedded documents, there are certain limitations due to Mongoid's document embedding callback structure. Consider the following example:
|
52
|
+
|
53
|
+
class Post
|
54
|
+
include Mongoid::Document
|
55
|
+
field :text
|
56
|
+
embeds_many :comments
|
57
|
+
end
|
58
|
+
|
59
|
+
class Comment
|
60
|
+
include Mongoid::Document
|
61
|
+
include Mongoid::OptimisticLocking
|
62
|
+
embedded_in :post
|
63
|
+
field :text
|
64
|
+
end
|
65
|
+
|
66
|
+
post = Post.new
|
67
|
+
comment = post.comments.build(:text => 'hello')
|
68
|
+
comment.save # will use optimistic locking checks
|
69
|
+
post.save # will not use optimistic locking checks
|
70
|
+
|
50
71
|
## Open sourced by
|
51
72
|
|
52
73
|
[Boxee](http://www.boxee.tv)
|
@@ -55,3 +76,5 @@ That's it!
|
|
55
76
|
[Mongo Developer FAQ - How do I do transactions/locking?](http://docs.mongodb.org/manual/faq/developers/#how-do-i-do-transactions-and-locking-in-mongodb)
|
56
77
|
|
57
78
|
[Mongo Atomic Operations - "Update if Current"](http://www.mongodb.org/display/DOCS/Atomic+Operations#AtomicOperations-%22UpdateifCurrent%22)
|
79
|
+
|
80
|
+
[Presentation from "Startup Day" on Mongoid Optimistic Locking](https://speakerdeck.com/u/burgalon/p/mongoid-optimistic-locking)
|
@@ -2,19 +2,20 @@ module Mongoid
|
|
2
2
|
module OptimisticLocking
|
3
3
|
module Operations
|
4
4
|
|
5
|
-
def insert(
|
5
|
+
def insert(options = {})
|
6
6
|
return super unless optimistic_locking?
|
7
7
|
increment_lock_version do
|
8
8
|
super
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
|
-
def update(
|
12
|
+
def update(options = {})
|
13
13
|
return super unless optimistic_locking?
|
14
14
|
set_lock_version_for_selector do
|
15
15
|
increment_lock_version do
|
16
16
|
result = super
|
17
|
-
|
17
|
+
getlasterror = mongo_session.command({:getlasterror => 1})
|
18
|
+
if result && !getlasterror['updatedExisting']
|
18
19
|
raise Mongoid::Errors::StaleDocument.new('update', self)
|
19
20
|
end
|
20
21
|
result
|
@@ -22,15 +23,16 @@ module Mongoid
|
|
22
23
|
end
|
23
24
|
end
|
24
25
|
|
25
|
-
def remove(
|
26
|
-
return super unless optimistic_locking?
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
26
|
+
def remove(options = {})
|
27
|
+
return super unless optimistic_locking? && persisted?
|
28
|
+
# we need to just see if the document exists and got updated with
|
29
|
+
# a higher lock version
|
30
|
+
existing = _reload # get the current root or embedded document
|
31
|
+
if existing.present? && existing['_lock_version'] &&
|
32
|
+
existing['_lock_version'].to_i > _lock_version.to_i
|
33
|
+
raise Mongoid::Errors::StaleDocument.new('destroy', self)
|
33
34
|
end
|
35
|
+
super
|
34
36
|
end
|
35
37
|
|
36
38
|
def atomic_selector
|
@@ -11,10 +11,9 @@ Gem::Specification.new do |s|
|
|
11
11
|
s.summary = %q{Allows optimisitic locking for Mongoid models}
|
12
12
|
s.description = %q{Allows optimisitic locking for Mongoid models. See https://github.com/burgalon/mongoid_optimistic_locking}
|
13
13
|
|
14
|
-
s.add_dependency 'mongoid', '
|
14
|
+
s.add_dependency 'mongoid', '>= 3.0.0rc'
|
15
15
|
s.add_development_dependency 'rake', '~> 0.9.0'
|
16
16
|
s.add_development_dependency 'rspec', '~> 2.6'
|
17
|
-
s.add_development_dependency 'bson_ext', '~> 1.5'
|
18
17
|
|
19
18
|
s.rubyforge_project = "mongoid_optimistic_locking"
|
20
19
|
|
@@ -2,106 +2,296 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Mongoid::OptimisticLocking do
|
4
4
|
|
5
|
-
|
5
|
+
COLLECTIONS = %w(Company Person Car State Flag City)
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
7
|
+
before(:all) do
|
8
|
+
|
9
|
+
class Company
|
10
|
+
include Mongoid::Document
|
11
|
+
field :name
|
12
|
+
has_many :people, :class_name => 'Person', :dependent => :destroy
|
13
|
+
end
|
14
|
+
|
15
|
+
class Person
|
16
|
+
include Mongoid::Document
|
17
|
+
include Mongoid::OptimisticLocking
|
18
|
+
field :name
|
19
|
+
has_many :cars, :dependent => :destroy
|
13
20
|
end
|
14
21
|
|
15
|
-
|
16
|
-
|
17
|
-
|
22
|
+
class Car
|
23
|
+
include Mongoid::Document
|
24
|
+
include Mongoid::OptimisticLocking
|
25
|
+
field :color
|
26
|
+
belongs_to :person
|
27
|
+
end
|
28
|
+
|
29
|
+
class State
|
30
|
+
include Mongoid::Document
|
31
|
+
field :name
|
32
|
+
embeds_one :flag, :inverse_of => :state
|
33
|
+
embeds_many :cities, :class_name => 'City', :inverse_of => :state
|
34
|
+
end
|
35
|
+
|
36
|
+
class Flag
|
37
|
+
include Mongoid::Document
|
38
|
+
include Mongoid::OptimisticLocking
|
39
|
+
embedded_in :state, :inverse_of => :flag
|
40
|
+
field :color
|
41
|
+
end
|
42
|
+
|
43
|
+
class City
|
44
|
+
include Mongoid::Document
|
45
|
+
include Mongoid::OptimisticLocking
|
46
|
+
embedded_in :state, :inverse_of => :cities
|
47
|
+
field :name
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
after(:all) { COLLECTIONS.each { |n| Object.send(:remove_const, n) } }
|
53
|
+
before { (COLLECTIONS - %w(Flag City)).each { |n| n.constantize.delete_all } }
|
54
|
+
|
55
|
+
context 'without optimistic locking' do
|
56
|
+
|
57
|
+
let!(:c1) { Company.create!(:name => 'Acme') }
|
58
|
+
let!(:c2) { Company.find(c1.id) }
|
59
|
+
|
60
|
+
it 'should allow collisions when saving' do
|
18
61
|
c1.name = 'Biz'
|
19
|
-
c1.save.should
|
62
|
+
c1.save.should be_true
|
20
63
|
c2.name = 'Baz'
|
21
|
-
c2.save.should
|
64
|
+
c2.save.should be_true
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should allow collisions when destroying' do
|
68
|
+
c1.destroy.should be_true
|
69
|
+
c2.destroy.should be_true
|
22
70
|
end
|
23
71
|
|
24
72
|
end
|
25
73
|
|
26
|
-
context '
|
74
|
+
context 'optimistic locking on a document' do
|
27
75
|
|
28
|
-
let(:
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
76
|
+
let(:person) { Person.create!(:name => 'Bob') }
|
77
|
+
|
78
|
+
it 'should allow creating' do
|
79
|
+
person._lock_version.should == 1
|
80
|
+
person.save.should be_true
|
81
|
+
person._lock_version.should == 2
|
82
|
+
Person.count.should == 1
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'should allow updating' do
|
86
|
+
person.name = 'Chris'
|
87
|
+
person.save.should be_true
|
88
|
+
person._lock_version.should == 2
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'should allow destroying after create' do
|
92
|
+
person.destroy.should be_true
|
93
|
+
person.should be_frozen
|
94
|
+
person._lock_version.should == 1
|
95
|
+
Person.count.should == 0
|
35
96
|
end
|
36
97
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
98
|
+
it 'should allow destroying from find' do
|
99
|
+
another = Person.find(person.id)
|
100
|
+
another.destroy.should be_true
|
101
|
+
another.should be_frozen
|
102
|
+
another._lock_version.should == 1
|
103
|
+
Person.count.should == 0
|
41
104
|
end
|
42
105
|
|
43
|
-
context 'after updating the first' do
|
106
|
+
context 'after updating the first of 3 instances of the same document' do
|
44
107
|
|
45
108
|
before do
|
109
|
+
@p1 = person
|
110
|
+
@p2 = Person.find(@p1.id)
|
111
|
+
@p3 = Person.find(@p1.id)
|
46
112
|
@p1.name = 'Michael'
|
47
|
-
@p1.save.should
|
113
|
+
@p1.save.should be_true
|
48
114
|
end
|
49
115
|
|
50
|
-
it 'should
|
116
|
+
it 'should raise an exception when updating the second' do
|
51
117
|
expect {
|
52
118
|
@p2.name = 'George'
|
53
119
|
@p2.save
|
54
120
|
}.to raise_error(Mongoid::Errors::StaleDocument)
|
55
121
|
end
|
56
|
-
|
122
|
+
|
57
123
|
it 'should succeed when updating the second without locking' do
|
58
124
|
@p2.name = 'George'
|
59
|
-
@p2.unlocked.save.should
|
125
|
+
@p2.unlocked.save.should be_true
|
60
126
|
end
|
61
|
-
|
127
|
+
|
62
128
|
it 'should succeed when updating the second without locking, ' +
|
63
|
-
'then
|
129
|
+
'then raise an exception when updating the third' do
|
64
130
|
@p2.name = 'George'
|
65
|
-
@p2.unlocked.save.should
|
131
|
+
@p2.unlocked.save.should be_true
|
66
132
|
expect {
|
67
133
|
@p3.name = 'Sally'
|
68
134
|
@p3.save
|
69
135
|
}.to raise_error(Mongoid::Errors::StaleDocument)
|
70
136
|
end
|
71
137
|
|
72
|
-
it 'should
|
138
|
+
it 'should raise an exception when destroying the second' do
|
73
139
|
expect {
|
74
140
|
@p2.destroy
|
75
141
|
}.to raise_error(Mongoid::Errors::StaleDocument)
|
142
|
+
Person.count.should == 1
|
76
143
|
end
|
77
144
|
|
78
145
|
it 'should succeed when destroying the second without locking' do
|
79
146
|
@p2.unlocked.destroy
|
147
|
+
Person.count.should == 0
|
80
148
|
end
|
81
149
|
|
82
150
|
it 'should succeed when destroying the second without locking, ' +
|
83
|
-
'
|
151
|
+
'and succeed when destroying the third with locking' do
|
84
152
|
@p2.unlocked.destroy
|
85
|
-
|
86
|
-
|
87
|
-
}.to raise_error(Mongoid::Errors::StaleDocument)
|
153
|
+
Person.count.should == 0
|
154
|
+
@p3.destroy.should
|
88
155
|
end
|
89
156
|
|
90
157
|
end
|
91
158
|
|
159
|
+
it 'should destroy dependents with dependent destroy option' do
|
160
|
+
Car.count.should == 0
|
161
|
+
person.cars.create(:color => 'Red')
|
162
|
+
person.destroy
|
163
|
+
Car.count.should == 0
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'should destroy dependents with dependent destroy option even when they are concurrently edited' do
|
167
|
+
car1 = person.cars.create(:color => 'Red')
|
168
|
+
car2 = Car.find(car1.id)
|
169
|
+
car2.update_attribute :color, 'Green'
|
170
|
+
person.reload # important
|
171
|
+
person.destroy
|
172
|
+
Person.count.should == 0
|
173
|
+
Car.count.should == 0
|
174
|
+
end
|
175
|
+
|
92
176
|
it 'should give a deprecation warning for #save_optimistic!' do
|
93
177
|
::ActiveSupport::Deprecation.should_receive(:warn).once
|
94
|
-
|
178
|
+
person.save_optimistic!
|
95
179
|
end
|
96
180
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
181
|
+
end
|
182
|
+
|
183
|
+
context 'optimistic locking on an embeds_one document' do
|
184
|
+
|
185
|
+
let!(:state) { State.new(:name => 'California') }
|
186
|
+
|
187
|
+
it 'should be savable with no embeds' do
|
188
|
+
state.name = 'Nevada'
|
189
|
+
state.save.should be_true
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'should be savable with a built embed' do
|
193
|
+
state.build_flag(:color => 'Red')
|
194
|
+
state.save.should be_true
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'should allow creating the embed' do
|
198
|
+
state.create_flag(:color => 'Red')
|
199
|
+
state.flag.should be_persisted
|
200
|
+
end
|
201
|
+
|
202
|
+
it 'should not increment the lock version when saving the base' do
|
203
|
+
state.build_flag(:color => 'Red')
|
204
|
+
state.flag._lock_version.should == 0
|
205
|
+
state.save
|
206
|
+
state.flag._lock_version.should == 0
|
207
|
+
end
|
208
|
+
|
209
|
+
it 'should increment lock version when saving the embed' do
|
210
|
+
state.build_flag(:color => 'Red')
|
211
|
+
state.flag._lock_version.should == 0
|
212
|
+
state.flag.save
|
213
|
+
state.flag._lock_version.should == 1
|
214
|
+
end
|
215
|
+
|
216
|
+
it 'should not raise an exception on saving the base when another process updated it' do
|
217
|
+
flag1 = state.create_flag(:color => 'Red')
|
218
|
+
flag2 = State.find(state.id).flag
|
219
|
+
flag1.color = 'Green'
|
220
|
+
state.save # doesn't increment lock version
|
221
|
+
flag2.update_attribute(:color, 'Purple').should be_true
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'should raise an exception on update when another process updated it' do
|
225
|
+
flag1 = state.create_flag(:color => 'Red')
|
226
|
+
flag2 = State.find(state.id).flag
|
227
|
+
flag1.update_attribute :color, 'Green'
|
228
|
+
expect {
|
229
|
+
flag2.update_attribute :color, 'Purple'
|
230
|
+
}.to raise_error(Mongoid::Errors::StaleDocument)
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'should raise an exception on destroy when another process updated it' do
|
234
|
+
flag1 = state.create_flag(:color => 'Red')
|
235
|
+
flag2 = State.find(state.id).flag
|
236
|
+
flag1.update_attribute :color, 'Green'
|
237
|
+
expect {
|
238
|
+
flag2.destroy
|
239
|
+
}.to raise_error(Mongoid::Errors::StaleDocument)
|
240
|
+
end
|
241
|
+
|
242
|
+
end
|
243
|
+
|
244
|
+
context 'optimistic locking on an embeds_many document' do
|
245
|
+
|
246
|
+
let!(:state) { State.create!(:name => 'California') }
|
247
|
+
|
248
|
+
it 'should be savable with no embeds' do
|
249
|
+
state.name = 'Nevada'
|
250
|
+
state.save.should be_true
|
251
|
+
end
|
252
|
+
|
253
|
+
it 'should be savable with a built embed' do
|
254
|
+
state.cities.build(:name => 'Los Angeles')
|
255
|
+
state.save.should be_true
|
256
|
+
end
|
257
|
+
|
258
|
+
it 'should allow embedded build to save' do
|
259
|
+
city = state.cities.build(:name => 'Los Angeles')
|
260
|
+
city.save.should be_true
|
261
|
+
end
|
262
|
+
|
263
|
+
it 'should allow embedded create' do
|
264
|
+
city = state.cities.create(:name => 'Los Angeles')
|
265
|
+
city.should be_persisted
|
103
266
|
end
|
104
267
|
|
268
|
+
it 'should not increment the lock version when saving the base' do
|
269
|
+
city = state.cities.build(:name => 'Los Angeles')
|
270
|
+
city._lock_version.should == 0
|
271
|
+
state.save
|
272
|
+
city._lock_version.should == 0
|
273
|
+
end
|
274
|
+
|
275
|
+
it 'should increment the lock version when building and saving the embed' do
|
276
|
+
city = state.cities.build(:name => 'Los Angeles')
|
277
|
+
city._lock_version.should == 0
|
278
|
+
city.save
|
279
|
+
city._lock_version.should == 1
|
280
|
+
end
|
281
|
+
|
282
|
+
it 'should increment the lock version when creating the embed' do
|
283
|
+
city = state.cities.create(:name => 'Los Angeles')
|
284
|
+
city._lock_version.should == 1
|
285
|
+
end
|
286
|
+
|
287
|
+
end
|
288
|
+
|
289
|
+
it 'should give a deprecation warning for including Mongoid::Lockable' do
|
290
|
+
::ActiveSupport::Deprecation.should_receive(:warn).once
|
291
|
+
Class.new do
|
292
|
+
include Mongoid::Document
|
293
|
+
include Mongoid::Lockable
|
294
|
+
end
|
105
295
|
end
|
106
296
|
|
107
297
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -4,8 +4,9 @@ Bundler.setup
|
|
4
4
|
|
5
5
|
require 'mongoid'
|
6
6
|
|
7
|
+
# Set the database that the spec suite connects to.
|
7
8
|
Mongoid.configure do |config|
|
8
|
-
config.
|
9
|
+
config.connect_to('mongoid_optimistic_locking_test')
|
9
10
|
end
|
10
11
|
|
11
12
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mongoid_optimistic_locking
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,22 +10,27 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2013-03-05 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: mongoid
|
17
|
-
requirement:
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
18
|
none: false
|
19
19
|
requirements:
|
20
|
-
- -
|
20
|
+
- - ! '>='
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version:
|
22
|
+
version: 3.0.0rc
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
|
-
version_requirements:
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: 3.0.0rc
|
26
31
|
- !ruby/object:Gem::Dependency
|
27
32
|
name: rake
|
28
|
-
requirement:
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
29
34
|
none: false
|
30
35
|
requirements:
|
31
36
|
- - ~>
|
@@ -33,10 +38,15 @@ dependencies:
|
|
33
38
|
version: 0.9.0
|
34
39
|
type: :development
|
35
40
|
prerelease: false
|
36
|
-
version_requirements:
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ~>
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.9.0
|
37
47
|
- !ruby/object:Gem::Dependency
|
38
48
|
name: rspec
|
39
|
-
requirement:
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
40
50
|
none: false
|
41
51
|
requirements:
|
42
52
|
- - ~>
|
@@ -44,18 +54,12 @@ dependencies:
|
|
44
54
|
version: '2.6'
|
45
55
|
type: :development
|
46
56
|
prerelease: false
|
47
|
-
version_requirements:
|
48
|
-
- !ruby/object:Gem::Dependency
|
49
|
-
name: bson_ext
|
50
|
-
requirement: &2161831560 !ruby/object:Gem::Requirement
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
58
|
none: false
|
52
59
|
requirements:
|
53
60
|
- - ~>
|
54
61
|
- !ruby/object:Gem::Version
|
55
|
-
version: '
|
56
|
-
type: :development
|
57
|
-
prerelease: false
|
58
|
-
version_requirements: *2161831560
|
62
|
+
version: '2.6'
|
59
63
|
description: Allows optimisitic locking for Mongoid models. See https://github.com/burgalon/mongoid_optimistic_locking
|
60
64
|
email:
|
61
65
|
- burgalon@gmail.com
|
@@ -106,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
106
110
|
version: '0'
|
107
111
|
requirements: []
|
108
112
|
rubyforge_project: mongoid_optimistic_locking
|
109
|
-
rubygems_version: 1.8.
|
113
|
+
rubygems_version: 1.8.24
|
110
114
|
signing_key:
|
111
115
|
specification_version: 3
|
112
116
|
summary: Allows optimisitic locking for Mongoid models
|