suo 0.2.1 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ef6a0837cc8be9a03a3767df546c630adf1e74cf
4
- data.tar.gz: d2fefd52a16706479a3f62b4a89557ece8e74672
3
+ metadata.gz: c3c678d658e34c698d95ca20d8d7ddfea5c11c6c
4
+ data.tar.gz: cb7bf87ef58efff50356c9310adc1fb10fad6ad1
5
5
  SHA512:
6
- metadata.gz: 4dc9fa8f2377e19efce5d6d4d009a145214360fd51efac442aaabcd9ba9077e49bdbad7828304c9aab9a273ad76b017c6fadce7bccf83c1d0897d31c63a0333d
7
- data.tar.gz: a0de3ab597fc8d61fe6f47f2b239aeaa11baf9d6cde481481840f96a8f673150a9d10db13e53796cd552cafceb01fbedffaa4160228686b6396335444dbf64e5
6
+ metadata.gz: c33bc02743629ab053afa32ff38e0c837cc1b2deacdbb50342e14de50c83321f7d232df4e2dd2338ee8ec16e54b0aa7cc81b97adc67bd86d75c597c06822be49
7
+ data.tar.gz: ef2790ad5ad8ac1567346fd04b340288ec8ea6924a267d8b9ea4daea02808b6c665b267bbaee61c90c544bfeb4de3a6953100d12e36d70365ee505765b73c833
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## 0.2.3
2
+
3
+ - Clarify documentation further with respect to semaphores.
4
+
5
+ ## 0.2.2
6
+
7
+ - Fix bug with refresh - typo would've prevented real use.
8
+ - Clean up code.
9
+ - Improve documentation a bit.
10
+ - 100% test coverage.
11
+
1
12
  ## 0.2.1
2
13
 
3
14
  - Fix bug when dealing with real-world Redis error conditions.
@@ -18,7 +29,7 @@
18
29
 
19
30
  ## 0.1.1
20
31
 
21
- - Use [MessagePack](https://github.com/msgpack/msgpack-ruby) for semaphore serialization.
32
+ - Use [MessagePack](https://github.com/msgpack/msgpack-ruby) for lock serialization.
22
33
 
23
34
  ## 0.1.0
24
35
 
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # Suo [![Build Status](https://travis-ci.org/nickelser/suo.svg?branch=master)](https://travis-ci.org/nickelser/suo) [![Gem Version](https://badge.fury.io/rb/suo.svg)](http://badge.fury.io/rb/suo)
1
+ # Suo [![Build Status](https://travis-ci.org/nickelser/suo.svg?branch=master)](https://travis-ci.org/nickelser/suo) [![Code Climate](https://codeclimate.com/github/nickelser/suo/badges/gpa.svg)](https://codeclimate.com/github/nickelser/suo) [![Test Coverage](https://codeclimate.com/github/nickelser/suo/badges/coverage.svg)](https://codeclimate.com/github/nickelser/suo) [![Gem Version](https://badge.fury.io/rb/suo.svg)](http://badge.fury.io/rb/suo)
2
2
 
3
- :lock: Distributed semaphores using Memcached or Redis in Ruby.
3
+ :lock: Distributed locks using Memcached or Redis in Ruby.
4
4
 
5
- Suo provides a very performant distributed lock solution using Compare-And-Set (`CAS`) commands in Memcached, and `WATCH/MULTI` in Redis.
5
+ Suo provides a very performant distributed lock solution using Compare-And-Set (`CAS`) commands in Memcached, and `WATCH/MULTI` in Redis. It allows locking both single exclusion (a mutex - sharing one resource), and multiple resources.
6
6
 
7
7
  ## Installation
8
8
 
@@ -31,6 +31,7 @@ suo.lock("some_key") do
31
31
  @puppies.pet!
32
32
  end
33
33
 
34
+ # The second argument to lock is the number of arguments (defaulting to one - a mutex)
34
35
  Thread.new { suo.lock("other_key", 2) { puts "One"; sleep 2 } }
35
36
  Thread.new { suo.lock("other_key", 2) { puts "Two"; sleep 2 } }
36
37
  Thread.new { suo.lock("other_key", 2) { puts "Three" } }
@@ -41,16 +42,39 @@ Thread.new { suo.lock("other_key", 2) { puts "Three" } }
41
42
  suo = Suo::Client::Memcached.new(client: some_dalli_client, acquisition_timeout: 1) # in seconds
42
43
 
43
44
  # manually locking/unlocking
44
- suo.lock("a_key")
45
+ # the return value from lock without a block is a unique token valid only for the current lock
46
+ # which must be unlocked manually
47
+ lock = suo.lock("a_key")
45
48
  foo.baz!
46
- suo.unlock("a_key")
49
+ suo.unlock("a_key", lock)
47
50
 
48
- # custom stale lock cleanup (cleaning of dead clients)
51
+ # custom stale lock expiration (cleaning of dead locks)
49
52
  suo = Suo::Client::Redis.new(client: some_redis_client, stale_lock_expiration: 60*5)
50
53
  ```
51
54
 
55
+ ### Stale locks
56
+
57
+ "Stale locks" - those acquired more than `stale_lock_expiration` (defaulting to 3600 or one hour) ago - are automatically cleared during any operation on the key (`lock`, `unlock`, `refresh`). The `locked?` method will not return true if only stale locks exist, but will not modify the key itself.
58
+
59
+ To re-acquire a lock in the middle of a block, you can use the refresh method on client.
60
+
61
+ ```ruby
62
+ suo = Suo::Client::Redis.new
63
+
64
+ # lock is the same token as seen in the manual example, above
65
+ suo.lock("foo") do |lock|
66
+ 5.times do
67
+ baz.bar!
68
+ suo.refresh("foo", lock)
69
+ end
70
+ end
71
+ ```
72
+
73
+ ## Semaphore
74
+
75
+ With multiple resources, Suo acts like a semaphore, but is not strictly a semaphore according to the traditional definition, as the lock acquires ownership.
76
+
52
77
  ## TODO
53
- - better stale key handling (refresh blocks)
54
78
  - more race condition tests
55
79
 
56
80
  ## History
@@ -24,7 +24,7 @@ module Suo
24
24
 
25
25
  if block_given? && token
26
26
  begin
27
- yield
27
+ yield(token)
28
28
  ensure
29
29
  unlock(key, token)
30
30
  end
@@ -39,9 +39,9 @@ module Suo
39
39
 
40
40
  def locks(key)
41
41
  val, _ = get(key)
42
- locks = deserialize_locks(val)
42
+ cleared_locks = deserialize_and_clear_locks(val)
43
43
 
44
- locks
44
+ cleared_locks
45
45
  end
46
46
 
47
47
  def refresh(key, acquisition_token)
@@ -53,11 +53,11 @@ module Suo
53
53
  next
54
54
  end
55
55
 
56
- locks = deserialize_and_clear_locks(val)
56
+ cleared_locks = deserialize_and_clear_locks(val)
57
57
 
58
- refresh_lock(locks, acquisition_token)
58
+ refresh_lock(cleared_locks, acquisition_token)
59
59
 
60
- break if set(key, serialize_locks(locks), cas)
60
+ break if set(key, serialize_locks(cleared_locks), cas)
61
61
  end
62
62
  end
63
63
 
@@ -69,12 +69,12 @@ module Suo
69
69
 
70
70
  break if val.nil?
71
71
 
72
- locks = deserialize_and_clear_locks(val)
72
+ cleared_locks = deserialize_and_clear_locks(val)
73
73
 
74
- acquisition_lock = remove_lock(locks, acquisition_token)
74
+ acquisition_lock = remove_lock(cleared_locks, acquisition_token)
75
75
 
76
76
  break unless acquisition_lock
77
- break if set(key, serialize_locks(locks), cas)
77
+ break if set(key, serialize_locks(cleared_locks), cas)
78
78
  end
79
79
  rescue LockClientError => _ # rubocop:disable Lint/HandleExceptions
80
80
  # ignore - assume success due to optimistic locking
@@ -87,7 +87,6 @@ module Suo
87
87
  private
88
88
 
89
89
  def acquire_lock(key, resources = 1)
90
- acquisition_token = nil
91
90
  token = SecureRandom.base64(16)
92
91
 
93
92
  retry_with_timeout(key) do
@@ -98,32 +97,29 @@ module Suo
98
97
  next
99
98
  end
100
99
 
101
- locks = deserialize_and_clear_locks(val)
100
+ cleared_locks = deserialize_and_clear_locks(val)
102
101
 
103
- if locks.size < resources
104
- add_lock(locks, token)
102
+ if cleared_locks.size < resources
103
+ add_lock(cleared_locks, token)
105
104
 
106
- newval = serialize_locks(locks)
105
+ newval = serialize_locks(cleared_locks)
107
106
 
108
- if set(key, newval, cas)
109
- acquisition_token = token
110
- break
111
- end
107
+ return token if set(key, newval, cas)
112
108
  end
113
109
  end
114
110
 
115
- acquisition_token
111
+ nil
116
112
  end
117
113
 
118
114
  def get(key) # rubocop:disable Lint/UnusedMethodArgument
119
115
  fail NotImplementedError
120
116
  end
121
117
 
122
- def set(key, newval, oldval) # rubocop:disable Lint/UnusedMethodArgument
118
+ def set(key, newval, cas) # rubocop:disable Lint/UnusedMethodArgument
123
119
  fail NotImplementedError
124
120
  end
125
121
 
126
- def initial_set(key) # rubocop:disable Lint/UnusedMethodArgument
122
+ def initial_set(key, val = "") # rubocop:disable Lint/UnusedMethodArgument
127
123
  fail NotImplementedError
128
124
  end
129
125
 
@@ -135,8 +131,8 @@ module Suo
135
131
  start = Time.now.to_f
136
132
 
137
133
  @retry_count.times do
138
- now = Time.now.to_f
139
- break if now - start > @options[:acquisition_timeout]
134
+ elapsed = Time.now.to_f - start
135
+ break if elapsed >= @options[:acquisition_timeout]
140
136
 
141
137
  synchronize(key) do
142
138
  yield
@@ -162,7 +158,7 @@ module Suo
162
158
  unpacked.map do |time, token|
163
159
  [Time.at(time), token]
164
160
  end
165
- rescue EOFError => _
161
+ rescue EOFError, MessagePack::MalformedFormatError => _
166
162
  []
167
163
  end
168
164
 
@@ -171,8 +167,8 @@ module Suo
171
167
  locks.reject { |time, _| time < expired }
172
168
  end
173
169
 
174
- def add_lock(locks, token)
175
- locks << [Time.now.to_f, token]
170
+ def add_lock(locks, token, time = Time.now.to_f)
171
+ locks << [time, token]
176
172
  end
177
173
 
178
174
  def remove_lock(locks, acquisition_token)
@@ -182,7 +178,7 @@ module Suo
182
178
 
183
179
  def refresh_lock(locks, acquisition_token)
184
180
  remove_lock(locks, acquisition_token)
185
- add_lock(locks, token)
181
+ add_lock(locks, acquisition_token)
186
182
  end
187
183
  end
188
184
  end
@@ -20,8 +20,8 @@ module Suo
20
20
  @client.set_cas(key, newval, cas)
21
21
  end
22
22
 
23
- def initial_set(key)
24
- @client.set(key, "")
23
+ def initial_set(key, val = "")
24
+ @client.set(key, val)
25
25
  end
26
26
  end
27
27
  end
@@ -32,8 +32,8 @@ module Suo
32
32
  @client.unwatch
33
33
  end
34
34
 
35
- def initial_set(key)
36
- @client.set(key, "")
35
+ def initial_set(key, val = "")
36
+ @client.set(key, val)
37
37
  end
38
38
  end
39
39
  end
data/lib/suo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Suo
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.3"
3
3
  end
data/suo.gemspec CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Nick Elser"]
10
10
  spec.email = ["nick.elser@gmail.com"]
11
11
 
12
- spec.summary = %q(Distributed semaphores using Memcached or Redis.)
13
- spec.description = %q(Distributed semaphores using Memcached or Redis.)
12
+ spec.summary = %q(Distributed locks using Memcached or Redis.)
13
+ spec.description = %q(Distributed locks using Memcached or Redis.)
14
14
  spec.homepage = "https://github.com/nickelser/suo"
15
15
  spec.license = "MIT"
16
16
 
@@ -30,4 +30,5 @@ Gem::Specification.new do |spec|
30
30
  spec.add_development_dependency "rake", "~> 10.0"
31
31
  spec.add_development_dependency "rubocop", "~> 0.30.0"
32
32
  spec.add_development_dependency "minitest", "~> 5.5.0"
33
+ spec.add_development_dependency "codeclimate-test-reporter", "~> 0.4.7"
33
34
  end
data/test/client_test.rb CHANGED
@@ -3,6 +3,10 @@ require "test_helper"
3
3
  TEST_KEY = "suo_test_key".freeze
4
4
 
5
5
  module ClientTests
6
+ def client(options = {})
7
+ @client.class.new(options.merge(client: @client.client))
8
+ end
9
+
6
10
  def test_throws_failed_error_on_bad_client
7
11
  assert_raises(Suo::LockClientError) do
8
12
  client = @client.class.new(client: {})
@@ -27,6 +31,23 @@ module ClientTests
27
31
  assert_equal false, locked
28
32
  end
29
33
 
34
+ def test_empty_lock_on_invalid_data
35
+ @client.send(:initial_set, TEST_KEY, "bad value")
36
+ locked = @client.locked?(TEST_KEY)
37
+ assert_equal false, locked
38
+ end
39
+
40
+ def test_clear
41
+ lock1 = @client.lock(TEST_KEY, 1)
42
+ refute_nil lock1
43
+
44
+ @client.clear(TEST_KEY)
45
+
46
+ locked = @client.locked?(TEST_KEY, 1)
47
+
48
+ assert_equal false, locked
49
+ end
50
+
30
51
  def test_multiple_resource_locking
31
52
  lock1 = @client.lock(TEST_KEY, 2)
32
53
  refute_nil lock1
@@ -77,7 +98,7 @@ module ClientTests
77
98
  sleep 0.1
78
99
  threads << Thread.new { @client.lock(TEST_KEY, 2) { output << "Three" } }
79
100
 
80
- threads.map(&:join)
101
+ threads.each(&:join)
81
102
 
82
103
  ret = []
83
104
 
@@ -88,13 +109,14 @@ module ClientTests
88
109
 
89
110
  assert_equal 0, output.size
90
111
  assert_equal %w(One Two), ret
112
+ assert_equal false, @client.locked?(TEST_KEY)
91
113
  end
92
114
 
93
115
  def test_block_multiple_resource_locking
94
116
  success_counter = Queue.new
95
117
  failure_counter = Queue.new
96
118
 
97
- client = @client.class.new(acquisition_timeout: 0.9, client: @client.client)
119
+ client = client(acquisition_timeout: 0.9)
98
120
 
99
121
  100.times.map do |i|
100
122
  Thread.new do
@@ -105,17 +127,18 @@ module ClientTests
105
127
 
106
128
  failure_counter << i unless success
107
129
  end
108
- end.map(&:join)
130
+ end.each(&:join)
109
131
 
110
132
  assert_equal 50, success_counter.size
111
133
  assert_equal 50, failure_counter.size
134
+ assert_equal false, client.locked?(TEST_KEY)
112
135
  end
113
136
 
114
137
  def test_block_multiple_resource_locking_longer_timeout
115
138
  success_counter = Queue.new
116
139
  failure_counter = Queue.new
117
140
 
118
- client = @client.class.new(acquisition_timeout: 3, client: @client.client)
141
+ client = client(acquisition_timeout: 3)
119
142
 
120
143
  100.times.map do |i|
121
144
  Thread.new do
@@ -126,10 +149,187 @@ module ClientTests
126
149
 
127
150
  failure_counter << i unless success
128
151
  end
129
- end.map(&:join)
152
+ end.each(&:join)
130
153
 
131
154
  assert_equal 100, success_counter.size
132
155
  assert_equal 0, failure_counter.size
156
+ assert_equal false, client.locked?(TEST_KEY)
157
+ end
158
+
159
+ def test_unstale_lock_acquisition
160
+ success_counter = Queue.new
161
+ failure_counter = Queue.new
162
+
163
+ client = client(stale_lock_expiration: 0.5)
164
+
165
+ t1 = Thread.new { client.lock(TEST_KEY) { sleep 0.6; success_counter << 1 } }
166
+ sleep 0.3
167
+ t2 = Thread.new do
168
+ locked = client.lock(TEST_KEY) { success_counter << 1 }
169
+ failure_counter << 1 unless locked
170
+ end
171
+
172
+ [t1, t2].each(&:join)
173
+
174
+ assert_equal 1, success_counter.size
175
+ assert_equal 1, failure_counter.size
176
+ assert_equal false, client.locked?(TEST_KEY)
177
+ end
178
+
179
+ def test_stale_lock_acquisition
180
+ success_counter = Queue.new
181
+ failure_counter = Queue.new
182
+
183
+ client = client(stale_lock_expiration: 0.5)
184
+
185
+ t1 = Thread.new { client.lock(TEST_KEY) { sleep 0.6; success_counter << 1 } }
186
+ sleep 0.55
187
+ t2 = Thread.new do
188
+ locked = client.lock(TEST_KEY) { success_counter << 1 }
189
+ failure_counter << 1 unless locked
190
+ end
191
+
192
+ [t1, t2].each(&:join)
193
+
194
+ assert_equal 2, success_counter.size
195
+ assert_equal 0, failure_counter.size
196
+ assert_equal false, client.locked?(TEST_KEY)
197
+ end
198
+
199
+ def test_refresh
200
+ client = client(stale_lock_expiration: 0.5)
201
+
202
+ lock1 = client.lock(TEST_KEY)
203
+
204
+ assert_equal true, client.locked?(TEST_KEY)
205
+
206
+ client.refresh(TEST_KEY, lock1)
207
+
208
+ assert_equal true, client.locked?(TEST_KEY)
209
+
210
+ sleep 0.55
211
+
212
+ assert_equal false, client.locked?(TEST_KEY)
213
+
214
+ lock2 = client.lock(TEST_KEY)
215
+
216
+ client.refresh(TEST_KEY, lock1)
217
+
218
+ assert_equal true, client.locked?(TEST_KEY)
219
+
220
+ client.unlock(TEST_KEY, lock1)
221
+
222
+ # edge case with refresh lock in the middle
223
+ assert_equal true, client.locked?(TEST_KEY)
224
+
225
+ client.clear(TEST_KEY)
226
+
227
+ assert_equal false, client.locked?(TEST_KEY)
228
+
229
+ client.refresh(TEST_KEY, lock2)
230
+
231
+ assert_equal true, client.locked?(TEST_KEY)
232
+
233
+ client.unlock(TEST_KEY, lock2)
234
+
235
+ # now finally unlocked
236
+ assert_equal false, client.locked?(TEST_KEY)
237
+ end
238
+
239
+ def test_block_refresh
240
+ success_counter = Queue.new
241
+ failure_counter = Queue.new
242
+
243
+ client = client(stale_lock_expiration: 0.5)
244
+
245
+ t1 = Thread.new do
246
+ client.lock(TEST_KEY) do |token|
247
+ sleep 0.6
248
+ client.refresh(TEST_KEY, token)
249
+ sleep 1
250
+ success_counter << 1
251
+ end
252
+ end
253
+
254
+ t2 = Thread.new do
255
+ sleep 0.8
256
+ locked = client.lock(TEST_KEY) { success_counter << 1 }
257
+ failure_counter << 1 unless locked
258
+ end
259
+
260
+ [t1, t2].each(&:join)
261
+
262
+ assert_equal 1, success_counter.size
263
+ assert_equal 1, failure_counter.size
264
+ assert_equal false, client.locked?(TEST_KEY)
265
+ end
266
+
267
+ def test_refresh_multi
268
+ success_counter = Queue.new
269
+ failure_counter = Queue.new
270
+
271
+ client = client(stale_lock_expiration: 0.5)
272
+
273
+ t1 = Thread.new do
274
+ client.lock(TEST_KEY, 2) do |token|
275
+ sleep 0.4
276
+ client.refresh(TEST_KEY, token)
277
+ success_counter << 1
278
+ sleep 0.5
279
+ end
280
+ end
281
+
282
+ t2 = Thread.new do
283
+ sleep 0.55
284
+ locked = client.lock(TEST_KEY, 2) do
285
+ success_counter << 1
286
+ sleep 0.5
287
+ end
288
+
289
+ failure_counter << 1 unless locked
290
+ end
291
+
292
+ t3 = Thread.new do
293
+ sleep 0.75
294
+ locked = client.lock(TEST_KEY, 2) { success_counter << 1 }
295
+ failure_counter << 1 unless locked
296
+ end
297
+
298
+ [t1, t2, t3].each(&:join)
299
+
300
+ assert_equal 2, success_counter.size
301
+ assert_equal 1, failure_counter.size
302
+ assert_equal false, client.locked?(TEST_KEY)
303
+ end
304
+
305
+ def test_increment_reused_client
306
+ i = 0
307
+
308
+ threads = 2.times.map do
309
+ Thread.new do
310
+ @client.lock(TEST_KEY) { i += 1 }
311
+ end
312
+ end
313
+
314
+ threads.each(&:join)
315
+
316
+ assert_equal 2, i
317
+ assert_equal false, client.locked?(TEST_KEY)
318
+ end
319
+
320
+ def test_increment_new_client
321
+ i = 0
322
+
323
+ threads = 2.times.map do
324
+ Thread.new do
325
+ client.lock(TEST_KEY) { i += 1 }
326
+ end
327
+ end
328
+
329
+ threads.each(&:join)
330
+
331
+ assert_equal 2, i
332
+ assert_equal false, client.locked?(TEST_KEY)
133
333
  end
134
334
  end
135
335
 
@@ -142,6 +342,18 @@ class TestBaseClient < Minitest::Test
142
342
  assert_raises(NotImplementedError) do
143
343
  @client.send(:get, TEST_KEY)
144
344
  end
345
+
346
+ assert_raises(NotImplementedError) do
347
+ @client.send(:set, TEST_KEY, "", "")
348
+ end
349
+
350
+ assert_raises(NotImplementedError) do
351
+ @client.send(:initial_set, TEST_KEY)
352
+ end
353
+
354
+ assert_raises(NotImplementedError) do
355
+ @client.send(:clear, TEST_KEY)
356
+ end
145
357
  end
146
358
  end
147
359
 
@@ -151,6 +363,7 @@ class TestMemcachedClient < Minitest::Test
151
363
  def setup
152
364
  @dalli = Dalli::Client.new("127.0.0.1:11211")
153
365
  @client = Suo::Client::Memcached.new
366
+ teardown
154
367
  end
155
368
 
156
369
  def teardown
@@ -164,6 +377,7 @@ class TestRedisClient < Minitest::Test
164
377
  def setup
165
378
  @redis = Redis.new
166
379
  @client = Suo::Client::Redis.new
380
+ teardown
167
381
  end
168
382
 
169
383
  def teardown
data/test/test_helper.rb CHANGED
@@ -1,5 +1,10 @@
1
1
  $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
2
2
 
3
+ if ENV["CODECLIMATE_REPO_TOKEN"]
4
+ require "codeclimate-test-reporter"
5
+ CodeClimate::TestReporter.start
6
+ end
7
+
3
8
  require "suo"
4
9
  require "thread"
5
10
  require "minitest/autorun"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: suo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Elser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-04-13 00:00:00.000000000 Z
11
+ date: 2015-04-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dalli
@@ -108,7 +108,21 @@ dependencies:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
110
  version: 5.5.0
111
- description: Distributed semaphores using Memcached or Redis.
111
+ - !ruby/object:Gem::Dependency
112
+ name: codeclimate-test-reporter
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.4.7
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.4.7
125
+ description: Distributed locks using Memcached or Redis.
112
126
  email:
113
127
  - nick.elser@gmail.com
114
128
  executables:
@@ -159,7 +173,7 @@ rubyforge_project:
159
173
  rubygems_version: 2.4.5
160
174
  signing_key:
161
175
  specification_version: 4
162
- summary: Distributed semaphores using Memcached or Redis.
176
+ summary: Distributed locks using Memcached or Redis.
163
177
  test_files:
164
178
  - test/client_test.rb
165
179
  - test/test_helper.rb