suo 0.2.1 → 0.2.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.
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