mongo-lock 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,72 @@
1
+ module Mongo
2
+ class Lock
3
+ class Configuration
4
+
5
+ attr_accessor :connections
6
+ attr_accessor :limit
7
+ attr_accessor :timeout_in
8
+ attr_accessor :frequency
9
+ attr_accessor :expires_after
10
+ attr_accessor :owner
11
+ attr_accessor :raise
12
+
13
+ def initialize defaults, options, &block
14
+ options = defaults.merge(options)
15
+ options[:collections] ||= {}
16
+ if options[:collection]
17
+ options[:collections][:default] = options[:collection]
18
+ end
19
+ options.each_pair do |key,value|
20
+ self.send(:"#{key}=",value)
21
+ end
22
+ yield self if block_given?
23
+ end
24
+
25
+ def collection= collection
26
+ collections[:default] = collection
27
+ end
28
+
29
+ def collection collection = :default
30
+ collection = collection.to_sym if collection.is_a? String
31
+ if collection.is_a? Symbol
32
+ collections[collection]
33
+ else
34
+ collection
35
+ end
36
+ end
37
+
38
+ def collections= collections
39
+ @collections = collections
40
+ end
41
+
42
+ def set_collections_keep_default collections
43
+ collections[:default] = @collections[:default]
44
+ @collections = collections
45
+ end
46
+
47
+ def collections
48
+ @collections ||= {}
49
+ end
50
+
51
+ def to_hash
52
+ {
53
+ timeout_in: timeout_in,
54
+ limit: limit,
55
+ frequency: frequency,
56
+ expires_after: expires_after,
57
+ owner: owner,
58
+ raise: raise
59
+ }
60
+ end
61
+
62
+ def owner
63
+ if @owner.is_a? Proc
64
+ @owner.call.to_s
65
+ else
66
+ @owner.to_s
67
+ end
68
+ end
69
+
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,5 @@
1
+ module Mongo
2
+ class Lock
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mongo-lock/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mongo-lock"
8
+ spec.version = Mongo::Lock::VERSION
9
+ spec.authors = ["Matthew Spence"]
10
+ spec.email = "msaspence@gmail.com"
11
+ spec.homepage = "https://github.com/trakio/mongo-lock"
12
+ spec.summary = "Pessimistic locking for Ruby and MongoDB"
13
+ spec.description = "Key based pessimistic locking for Ruby and MongoDB. Is this key avaliable? Yes - Lock it for me for a sec will you. No - OK I'll just wait here until its ready."
14
+ spec.required_rubygems_version = ">= 1.3.6"
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files`.split($/)
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ["lib"]
21
+
22
+ # s.add_dependency 'some-gem'
23
+ spec.extra_rdoc_files = ['README.md', 'LICENSE']
24
+
25
+ spec.add_development_dependency 'rspec'
26
+ spec.add_development_dependency 'fuubar'
27
+ spec.add_development_dependency 'pry'
28
+ spec.add_development_dependency 'activesupport'
29
+
30
+ end
@@ -0,0 +1,6 @@
1
+ development:
2
+ sessions:
3
+ default:
4
+ database: locks
5
+ hosts:
6
+ - localhost:27017
@@ -0,0 +1,184 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mongo::Lock do
4
+
5
+ describe '.acquire' do
6
+
7
+ it "creates and returns a new Mongo::Lock instance" do
8
+ expect(Mongo::Lock.acquire 'my_lock').to be_a Mongo::Lock
9
+ end
10
+
11
+ it "calls #acquire to acquire the lock" do
12
+ expect_any_instance_of(Mongo::Lock).to receive(:acquire)
13
+ Mongo::Lock.acquire 'my_lock'
14
+ end
15
+
16
+ context "when options are provided" do
17
+
18
+ it "passes them to the new lock" do
19
+ lock = Mongo::Lock.acquire('my_lock', { limit: 3 })
20
+ expect(lock.configuration.limit).to be 3
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+
27
+ describe '#acquire' do
28
+
29
+ let(:lock) { Mongo::Lock.new 'my_lock', owner: 'spence' }
30
+
31
+ context "when lock is available" do
32
+
33
+ it "acquires the lock" do
34
+ lock.acquire
35
+ expect(collection.find(key: 'my_lock').count).to be 1
36
+ end
37
+
38
+ it "sets the lock to expire" do
39
+ lock.acquire
40
+ expect(collection.find(key: 'my_lock').first['expires_at']).to be_within(1.second).of(10.seconds.from_now)
41
+ expect(collection.find(key: 'my_lock').first['ttl']).to be_within(1.second).of(10.seconds.from_now)
42
+ end
43
+
44
+ it "returns true" do
45
+ expect(lock.acquire).to be_true
46
+ end
47
+
48
+ end
49
+
50
+ context "when the frequency option is a Proc" do
51
+
52
+ let(:lock) { Mongo::Lock.new 'my_lock' }
53
+
54
+ it "should call the Proc with the attempt number" do
55
+ collection.insert key: 'my_lock', owner: 'tobie', expires_at: 0.04.seconds.from_now
56
+ proc = Proc.new{ |x| x }
57
+ expect(proc).to receive(:call).with(1).and_return(0.01)
58
+ expect(proc).to receive(:call).with(2).and_return(0.01)
59
+ expect(proc).to receive(:call).with(3).and_return(0.01)
60
+ lock.acquire limit: 3, frequency: proc
61
+ end
62
+
63
+ end
64
+
65
+ context "when the lock is unavailable" do
66
+
67
+ it "retries until it can acquire it" do
68
+ collection.insert key: 'my_lock', owner: 'tobie', expires_at: 0.1.seconds.from_now
69
+ lock.acquire frequency: 0.01, timeout_in: 0.2, limit: 20
70
+ expect(collection.find(key: 'my_lock', owner: 'spence').count).to be 1
71
+ end
72
+
73
+ end
74
+
75
+ context "when the lock is already acquired but by the same owner" do
76
+
77
+ before :each do
78
+ collection.insert key: 'my_lock', owner: 'spence', expires_at: 10.minutes.from_now
79
+ end
80
+
81
+ it "doesn't create a new lock" do
82
+ lock.acquire
83
+ expect(collection.find(key: 'my_lock').count).to be 1
84
+ end
85
+
86
+ it "returns true" do
87
+ expect(lock.acquire).to be_true
88
+ end
89
+
90
+ it "sets this instance as acquired" do
91
+ lock.acquire
92
+ expect(lock.instance_variable_get('@acquired')).to be_true
93
+ end
94
+
95
+ end
96
+
97
+ context "when the lock cannot be acquired" do
98
+
99
+ context "and acquisition timeout_in occurs" do
100
+
101
+ let(:lock) { Mongo::Lock.new 'my_lock', owner: 'spence', timeout_in: 0.03, frequency: 0.01 }
102
+
103
+ it "should return false" do
104
+ collection.insert key: 'my_lock', owner: 'tobie', expires_at: 0.2.seconds.from_now
105
+ expect(lock.acquire).to be_false
106
+ end
107
+
108
+ end
109
+
110
+ context "and acquisition limit is exceeded" do
111
+
112
+ let(:lock) { Mongo::Lock.new 'my_lock', owner: 'spence', timeout_in: 0.4, limit: 3, frequency: 0.01 }
113
+
114
+ it "should return false" do
115
+ collection.insert key: 'my_lock', owner: 'tobie', expires_at: 0.2.seconds.from_now
116
+ expect(lock.acquire).to be_false
117
+ end
118
+
119
+ end
120
+
121
+ end
122
+
123
+ context "when the lock cannot be acquired and raise option is set to true" do
124
+
125
+ context "and acquisition timeout_in occurs" do
126
+
127
+ let(:lock) { Mongo::Lock.new 'my_lock', owner: 'tobie', timeout_in: 0.4, frequency: 0.01, raise: true }
128
+
129
+ it "should raise Mongo::Lock::NotAcquiredError" do
130
+ collection.insert key: 'my_lock', owner: 'spence', expires_at: 0.2.seconds.from_now
131
+ expect{lock.acquire}.to raise_error Mongo::Lock::NotAcquiredError
132
+ end
133
+
134
+ end
135
+
136
+ context "and acquisition limit is exceeded" do
137
+
138
+ let(:lock) { Mongo::Lock.new 'my_lock', owner: 'tobie', timeout_in: 0.3, limit: 3, frequency: 0.01, raise: true }
139
+
140
+ it "should raise Mongo::Lock::NotAcquiredError" do
141
+ collection.insert key: 'my_lock', owner: 'spence', expires_at: 0.2.seconds.from_now
142
+ expect{lock.acquire}.to raise_error Mongo::Lock::NotAcquiredError
143
+ end
144
+
145
+ end
146
+
147
+ end
148
+
149
+ context "when options are provided" do
150
+
151
+ let(:lock) { Mongo::Lock.new 'my_lock', owner: 'tobie', timeout_in: 0.2, limit: 11, frequency: 0.01, raise: true }
152
+
153
+ it "overrides the lock's" do
154
+ collection.insert key: 'my_lock', owner: 'spence', expires_at: 0.1.seconds.from_now
155
+ expect(lock.acquire timeout_in: 0.05, limit: 3, frequency: 0.02, raise: false).to be_false
156
+ end
157
+
158
+ end
159
+
160
+ end
161
+
162
+ describe '.acquire!' do
163
+
164
+ let(:lock) { Mongo::Lock.new 'my_lock', owner: 'spence' }
165
+
166
+ it "calls .acquire with raise errors option set to true" do
167
+ expect(Mongo::Lock).to receive(:init_and_send).with('my_lock', { limit: 3 }, :acquire!)
168
+ Mongo::Lock.acquire! 'my_lock', limit: 3
169
+ end
170
+
171
+ end
172
+
173
+ describe '#acquire!' do
174
+
175
+ let(:lock) { Mongo::Lock.new 'my_lock', owner: 'spence' }
176
+
177
+ it "calls #acquire with raise errors option set to true" do
178
+ expect(lock).to receive(:acquire).with({ limit: 3, raise: true })
179
+ lock.acquire! limit: 3
180
+ end
181
+
182
+ end
183
+
184
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mongo::Lock do
4
+
5
+ describe '#acquired?' do
6
+
7
+ let(:lock) { Mongo::Lock.new 'my_lock', owner: 'spence', timeout_in: 0.01, frequency: 0.01 }
8
+
9
+ context "when the lock has been acquired" do
10
+
11
+ it "returns true" do
12
+ lock.acquire
13
+ expect(lock.acquired?).to be_true
14
+ end
15
+
16
+ end
17
+
18
+ context "when the lock hasn't been acquired" do
19
+
20
+ it "returns false" do
21
+ collection.insert key: 'my_lock', owner: 'tobie', expires_at: 1.minute.from_now
22
+ lock.acquire
23
+ expect(lock.acquired?).to be_false
24
+ end
25
+
26
+ end
27
+
28
+ context "when the lock was acquired but has since expired" do
29
+
30
+ it "returns false" do
31
+ collection.insert key: 'my_lock', owner: 'spence', expires_at: 0.01.seconds.from_now
32
+ lock.acquire
33
+ sleep 0.02
34
+ expect(lock.acquired?).to be_false
35
+ end
36
+
37
+ end
38
+
39
+ context "when the lock was acquired but has since been released" do
40
+
41
+ it "returns false" do
42
+ collection.insert key: 'my_lock', owner: 'tobie', expires_at: 1.minute.ago
43
+ lock.acquire
44
+ lock.release
45
+ expect(lock.acquired?).to be_false
46
+ end
47
+
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mongo::Lock do
4
+
5
+ describe '.available?' do
6
+
7
+ it "creates and returns a new Mongo::Lock instance" do
8
+ expect(Mongo::Lock.available? 'my_lock').to be_a Mongo::Lock
9
+ end
10
+
11
+ it "calls #available?" do
12
+ expect_any_instance_of(Mongo::Lock).to receive(:available?)
13
+ Mongo::Lock.available? 'my_lock'
14
+ end
15
+
16
+ context "when options are provided" do
17
+
18
+ it "passes them to the new lock" do
19
+ lock = Mongo::Lock.available?('my_lock', { owner: 'spence' })
20
+ expect(lock.configuration.owner).to eql 'spence'
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+
27
+ describe '#available?' do
28
+
29
+ let(:lock) { Mongo::Lock.new 'my_lock', owner: 'spence', timeout_in: 0.01, frequency: 0.01 }
30
+
31
+ context "when the lock is available" do
32
+
33
+ it "returns true" do
34
+ expect(lock.available?).to be_true
35
+ end
36
+
37
+ end
38
+
39
+ context "when the lock is expired" do
40
+
41
+ it "returns true" do
42
+ collection.insert key: 'my_lock', owner: 'tobie', expires_at: 1.minute.ago
43
+ expect(lock.available?).to be_true
44
+ end
45
+
46
+ end
47
+
48
+ context "when the lock is already acquired but by this owner" do
49
+
50
+ it "returns true" do
51
+ collection.insert key: 'my_lock', owner: 'spence', expires_at: 1.minute.from_now
52
+ expect(lock.available?).to be_true
53
+ end
54
+
55
+ end
56
+
57
+ context "when the lock is already acquired" do
58
+
59
+ it "returns false" do
60
+ collection.insert key: 'my_lock', owner: 'tobie', expires_at: 1.minute.from_now
61
+ expect(lock.available?).to be_false
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+
68
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mongo::Lock do
4
+
5
+ describe '.clear_expired' do
6
+
7
+ it "deletes expired locks in all collections" do
8
+ Mongo::Lock.configure collections: { default: collection, other: other_collection }
9
+ collection.insert owner: 'owner', key: 'my_lock', expires_at: 1.minute.from_now
10
+ collection.insert owner: 'owner', key: 'my_lock', expires_at: 1.minute.ago
11
+ other_collection.insert owner: 'owner', key: 'my_lock', expires_at: 1.minute.from_now
12
+ other_collection.insert owner: 'owner', key: 'my_lock', expires_at: 1.minute.ago
13
+ Mongo::Lock.clear_expired
14
+ expect(collection.find().count).to be 1
15
+ expect(other_collection.find().count).to be 1
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,238 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mongo::Lock::Configuration do
4
+
5
+ subject { Mongo::Lock::Configuration.new({}, {}) }
6
+
7
+ let (:collections_with_default) { { a: 'a', b: 'b', default: collection } }
8
+ let (:collections) { { a: 'a', b: 'b' } }
9
+ let (:collection) { 'default' }
10
+
11
+ describe '#initialize' do
12
+
13
+ context "when provided with a hash" do
14
+
15
+ it "sets each value" do
16
+ config = Mongo::Lock::Configuration.new({}, { limit: 3, timeout_in: 4 })
17
+ expect(config.limit).to be 3
18
+ expect(config.timeout_in).to be 4
19
+ end
20
+
21
+ context "when provided with a default connection" do
22
+
23
+ it "stores it in the connections hash as :default" do
24
+ config = Mongo::Lock::Configuration.new({}, { collection: collection, collections: collections })
25
+ expect(config.collections).to eql collections_with_default
26
+ end
27
+
28
+ end
29
+
30
+ end
31
+
32
+ context "when provided with a default" do
33
+
34
+ it "sets each value" do
35
+ config = Mongo::Lock::Configuration.new({ limit: 3, timeout_in: 4 }, { limit: 5 })
36
+ expect(config.limit).to be 5
37
+ expect(config.timeout_in).to be 4
38
+ end
39
+
40
+ context "when provided with a default connection" do
41
+
42
+ it "stores it in the connections hash as :default" do
43
+ config = Mongo::Lock::Configuration.new({ collections: collections }, { collection: collection})
44
+ expect(config.collections).to eql collections_with_default
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+
53
+ describe "#collections=" do
54
+
55
+ it "should set collections hash" do
56
+ subject.collections = collections
57
+ expect(subject.instance_variable_get('@collections')).to be collections
58
+ end
59
+
60
+ it "should remove default from collections hash" do
61
+ subject.instance_variable_set('@collections', collections_with_default)
62
+ subject.collections = collections
63
+ expect(subject.instance_variable_get('@collections')).to be collections
64
+ end
65
+
66
+ end
67
+
68
+ describe "#set_collections_keep_default" do
69
+
70
+ it "should keep default in the collections hash" do
71
+ subject.instance_variable_set('@collections', collections_with_default)
72
+ subject.set_collections_keep_default collections
73
+ expect(subject.instance_variable_get('@collections')).to eql collections_with_default
74
+ end
75
+
76
+ end
77
+
78
+ describe "#collections" do
79
+
80
+ it "should return the collections hash" do
81
+ subject.instance_variable_set('@collections', collections)
82
+ expect(subject.collections).to be collections
83
+ end
84
+
85
+ end
86
+
87
+ describe "#collection=" do
88
+
89
+ it "should set the default collection" do
90
+ subject.collection = collection
91
+ expect(subject.instance_variable_get('@collections')[:default]).to be collection
92
+ end
93
+
94
+ end
95
+
96
+ describe "#collection" do
97
+
98
+ context "when a symbol is provided" do
99
+
100
+ it "should return that collection" do
101
+ subject.instance_variable_set('@collections', collections)
102
+ expect(subject.collection :a).to eql 'a'
103
+ end
104
+
105
+ end
106
+
107
+ context "when a string is provided" do
108
+
109
+ it "should return that collection" do
110
+ subject.instance_variable_set('@collections', collections)
111
+ expect(subject.collection 'a').to eql 'a'
112
+ end
113
+
114
+ end
115
+
116
+ context "when it's any other object is" do
117
+
118
+ it "should return that collection" do
119
+ my_collection = Object.new
120
+ subject.instance_variable_set('@collections', collections)
121
+ expect(subject.collection my_collection).to be my_collection
122
+ end
123
+
124
+ end
125
+
126
+ context "when a symbol isn't provided" do
127
+
128
+ it "should return the default collection" do
129
+ subject.instance_variable_set('@collections', collections_with_default)
130
+ expect(subject.collection).to eql 'default'
131
+ end
132
+
133
+ end
134
+
135
+ end
136
+
137
+ describe "#timeout_in=" do
138
+
139
+ it "should set the timeout_in value" do
140
+ subject.timeout_in = 123
141
+ expect(subject.instance_variable_get('@timeout_in')).to be 123
142
+ end
143
+
144
+ end
145
+
146
+ describe "#timeout_in" do
147
+
148
+ it "should return the timeout_in value" do
149
+ subject.instance_variable_set('@timeout_in', 456)
150
+ expect(subject.timeout_in).to be 456
151
+ end
152
+
153
+ end
154
+
155
+ describe "#limit=" do
156
+
157
+ it "should set the limit value" do
158
+ subject.limit = 7
159
+ expect(subject.instance_variable_get('@limit')).to be 7
160
+ end
161
+
162
+ end
163
+
164
+ describe "#limit" do
165
+
166
+ it "should return the limit value" do
167
+ subject.instance_variable_set('@limit', 8)
168
+ expect(subject.limit).to be 8
169
+ end
170
+
171
+ end
172
+
173
+ describe "#frequency=" do
174
+
175
+ it "should set the frequency value" do
176
+ subject.frequency = 9
177
+ expect(subject.instance_variable_get('@frequency')).to be 9
178
+ end
179
+
180
+ end
181
+
182
+ describe "#frequency" do
183
+
184
+ it "should return the frequency value" do
185
+ subject.instance_variable_set('@frequency', 1)
186
+ expect(subject.frequency).to be 1
187
+ end
188
+
189
+ end
190
+
191
+ describe "#expires_after=" do
192
+
193
+ it "should set the expires_after value" do
194
+ subject.expires_after = 9
195
+ expect(subject.instance_variable_get('@expires_after')).to be 9
196
+ end
197
+
198
+ end
199
+
200
+ describe "#expires_after" do
201
+
202
+ it "should return the expires_after value" do
203
+ subject.instance_variable_set('@expires_after', 1)
204
+ expect(subject.expires_after).to be 1
205
+ end
206
+
207
+ end
208
+
209
+ describe "#owner" do
210
+
211
+ it "should return the owner value" do
212
+ subject.instance_variable_set('@owner', 'spence')
213
+ expect(subject.owner).to eql 'spence'
214
+ end
215
+
216
+ context "when owner is a Proc" do
217
+
218
+ it "is called" do
219
+ proc = Proc.new { }
220
+ expect(proc).to receive(:call)
221
+ subject.instance_variable_set('@owner', proc)
222
+ subject.owner
223
+ end
224
+
225
+ end
226
+
227
+ end
228
+
229
+ describe "#owner=" do
230
+
231
+ it "should set the owner value" do
232
+ subject.owner = 'spence'
233
+ expect(subject.instance_variable_get('@owner')).to eql 'spence'
234
+ end
235
+
236
+ end
237
+
238
+ end