mongo-lock 1.0.0

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