redlock 0.2.2 → 1.2.1

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
- SHA1:
3
- metadata.gz: 70744bd95881adacc0fc6623a3ba75ee0840339e
4
- data.tar.gz: 3b5b971c30395079d645eab57504ba002b7bed61
2
+ SHA256:
3
+ metadata.gz: a91b03cbb845e7b01556262e80e946e2d72980beeebce12d51380d7c57c57fb9
4
+ data.tar.gz: 73d38bac32a8003a556d3d8967155d15a8edbe988fc43387174ec506b21e8c99
5
5
  SHA512:
6
- metadata.gz: 3e468fb7a3a98f4cb896a8d6ac6bed4ed25574ae20b6bd9f66c478674f8a3346404dcfc063bc82e74c1669e3e5402f8b1423e381c6e520073baf652b38ef69b1
7
- data.tar.gz: 91506cfd3c6913f84cb50ce243ad13e4b9c4593a093872f83ac9518227ca477416f022b564f4aaa9004dab6c6f083614934283f58219b0970aa0cb33140d27f7
6
+ metadata.gz: e55aa47e007175e619e22ec2b2ca996add245143290750c79a27fbafc8bb6bd32e5c02b07d9a9752c1ad8cb7f22a10a2b245c9b963c1b4f186427d6c7a987d3a
7
+ data.tar.gz: dd61e0d187a5972ecbe09bde3345c1a6174263f06fca6b7d1da3ce8be19fe5bb69ffca4cfc24073705c5b5a40551bb1a65ffba1358c5fbaf37f10ea46a15a929
data/.gitignore CHANGED
@@ -1,3 +1,5 @@
1
1
  *.gem
2
2
  coverage/
3
3
  .bundle/
4
+ Gemfile.lock
5
+ dump.rdb
@@ -1,8 +1,23 @@
1
1
  language: ruby
2
- services:
3
- - redis-server
2
+ cache: bundler
3
+ sudo: false
4
+
4
5
  rvm:
5
- - "2.2.2"
6
+ - 2.4.9
7
+ - 2.5.7
8
+ - 2.6.5
9
+ - 2.7.0
10
+ - ruby-head
11
+
12
+ before_install:
13
+ - yes | gem update --system
14
+ - gem install bundler -v "~> 2.0"
15
+
6
16
  script: bundle exec rspec spec
7
- sudo: false
8
- cache: bundler
17
+
18
+ jobs:
19
+ allow_failures:
20
+ - rvm: ruby-head
21
+
22
+ services:
23
+ - redis-server
data/Makefile CHANGED
@@ -2,3 +2,8 @@ default: test
2
2
  test:
3
3
  docker-compose run --rm test
4
4
 
5
+ build:
6
+ docker-compose run --rm test gem build redlock.gemspec
7
+
8
+ publish:
9
+ docker-compose run --rm test gem push `ls -lt *gem | head -n 1 | awk '{ print $$9 }'`
data/README.md CHANGED
@@ -1,12 +1,9 @@
1
- [![Stories in Ready](https://badge.waffle.io/leandromoreira/redlock-rb.png?label=ready&title=Ready)](https://waffle.io/leandromoreira/redlock-rb)
2
1
  [![Build Status](https://travis-ci.org/leandromoreira/redlock-rb.svg?branch=master)](https://travis-ci.org/leandromoreira/redlock-rb)
3
2
  [![Coverage Status](https://coveralls.io/repos/leandromoreira/redlock-rb/badge.svg?branch=master)](https://coveralls.io/r/leandromoreira/redlock-rb?branch=master)
4
3
  [![Code Climate](https://codeclimate.com/github/leandromoreira/redlock-rb/badges/gpa.svg)](https://codeclimate.com/github/leandromoreira/redlock-rb)
5
- [![Dependency Status](https://gemnasium.com/leandromoreira/redlock-rb.svg)](https://gemnasium.com/leandromoreira/redlock-rb)
6
4
  [![Gem Version](https://badge.fury.io/rb/redlock.svg)](http://badge.fury.io/rb/redlock)
7
5
  [![security](https://hakiri.io/github/leandromoreira/redlock-rb/master.svg)](https://hakiri.io/github/leandromoreira/redlock-rb/master)
8
6
  [![Inline docs](http://inch-ci.org/github/leandromoreira/redlock-rb.svg?branch=master)](http://inch-ci.org/github/leandromoreira/redlock-rb)
9
- [![Join the chat at https://gitter.im/leandromoreira/redlock-rb](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/leandromoreira/redlock-rb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
10
7
 
11
8
 
12
9
  # Redlock - A ruby distributed lock using redis.
@@ -43,30 +40,28 @@ Or install it yourself as:
43
40
 
44
41
  ## Usage example
45
42
 
43
+ ### Acquiring a lock
44
+
45
+ NOTE: All expiration durations are in milliseconds.
46
46
  ```ruby
47
47
  # Locking
48
48
  lock_manager = Redlock::Client.new([ "redis://127.0.0.1:7777", "redis://127.0.0.1:7778", "redis://127.0.0.1:7779" ])
49
49
  first_try_lock_info = lock_manager.lock("resource_key", 2000)
50
50
  second_try_lock_info = lock_manager.lock("resource_key", 2000)
51
51
 
52
- # it prints lock info {validity: 1987, resource: "resource_key", value: "generated_uuid4"}
53
52
  p first_try_lock_info
54
- # it prints false
53
+ # => {validity: 1987, resource: "resource_key", value: "generated_uuid4"}
54
+
55
55
  p second_try_lock_info
56
+ # => false
56
57
 
57
58
  # Unlocking
58
59
  lock_manager.unlock(first_try_lock_info)
60
+
59
61
  second_try_lock_info = lock_manager.lock("resource_key", 2000)
60
62
 
61
- # now it prints lock info
62
63
  p second_try_lock_info
63
- ```
64
-
65
- Redlock works seamlessly with [redis sentinel](http://redis.io/topics/sentinel), which is supported in redis 3.2+. It also allows clients to set any other arbitrary options on the Redis connection, e.g. password, driver, and more.
66
-
67
- ```ruby
68
- servers = [ 'redis://localhost:6379', Redis.new(:url => 'redis://someotherhost:6379') ]
69
- redlock = Redlock::Client.new(servers)
64
+ # => {validity: 1962, resource: "resource_key", value: "generated_uuid5"}
70
65
  ```
71
66
 
72
67
  There's also a block version that automatically unlocks the lock:
@@ -81,7 +76,7 @@ lock_manager.lock("resource_key", 2000) do |locked|
81
76
  end
82
77
  ```
83
78
 
84
- There's also a bang version that only executes the block if the lock is successfully acquired, returning the block's value as a result, or raising an exception otherwise:
79
+ There's also a bang version that only executes the block if the lock is successfully acquired, returning the block's value as a result, or raising an exception otherwise. Passing a block is mandatory.
85
80
 
86
81
  ```ruby
87
82
  begin
@@ -93,34 +88,105 @@ rescue Redlock::LockError
93
88
  end
94
89
  ```
95
90
 
91
+ ### Extending a lock
92
+
96
93
  To extend the life of the lock:
97
94
 
98
95
  ```ruby
99
96
  begin
100
- block_result = lock_manager.lock!("resource_key", 2000) do |lock_info|
101
- # critical code
102
- lock_manager.lock("resource key", 3000, extend: lock_info)
103
- # more critical code
97
+ lock_info = lock_manager.lock("resource_key", 2000)
98
+ while lock_info
99
+ # Critical code
100
+
101
+ # Time up and more work to do? Extend the lock.
102
+ lock_info = lock_manager.lock("resource key", 3000, extend: lock_info)
104
103
  end
105
104
  rescue Redlock::LockError
106
105
  # error handling
107
106
  end
108
107
  ```
109
108
 
110
- The above code will also acquire the lock if the previous lock has expired and the lock is currently free. Keep in mind that this means the lock could have been acquired by someone else in the meantime. To only extend the life of the lock if currently locked by yourself, use `extend_life` parameter:
109
+ The above code will also acquire the lock if the previous lock has expired and the lock is currently free. Keep in mind that this means the lock could have been acquired and released by someone else in the meantime. To only extend the life of the lock if currently locked by yourself, use the `extend_only_if_locked` parameter:
111
110
 
112
111
  ```ruby
113
- begin
114
- block_result = lock_manager.lock!("resource_key", 2000) do |lock_info|
115
- # critical code
116
- lock_manager.lock("resource key", 3000, extend: lock_info, extend_life: true)
117
- # more critical code, only if lock was still hold
118
- end
119
- rescue Redlock::LockError
120
- # error handling
121
- end
112
+ lock_manager.lock("resource key", 3000, extend: lock_info, extend_only_if_locked: true)
122
113
  ```
123
114
 
115
+ ### Querying lock status
116
+
117
+ You can check if a resource is locked:
118
+
119
+ ```ruby
120
+ resource = "resource_key"
121
+ lock_info = lock_manager.lock(resource, 2000)
122
+ lock_manager.locked?(resource)
123
+ #=> true
124
+
125
+ lock_manager.unlock(lock_info)
126
+ lock_manager.locked?(resource)
127
+ #=> false
128
+ ```
129
+
130
+ Any caller can call the above method to query the status. If you hold a lock and would like to check if it is valid, you can use the `valid_lock?` method:
131
+
132
+ ```ruby
133
+ lock_info = lock_manager.lock("resource_key", 2000)
134
+ lock_manager.valid_lock?(lock_info)
135
+ #=> true
136
+
137
+ lock_manager.unlock(lock_info)
138
+ lock_manager.valid_lock?(lock_info)
139
+ #=> false
140
+ ```
141
+
142
+ The above methods **are not safe if you are using this to time critical code**, since they return true if the lock has not expired, even if there's only (for example) 1ms left on the lock. If you want to safely time the lock validity, you can use the `get_remaining_ttl_for_lock` and `get_remaining_ttl_for_resource` methods.
143
+
144
+ Use `get_remaining_ttl_for_lock` if you hold a lock and want to check the TTL specifically for your lock:
145
+ ```ruby
146
+ resource = "resource_key"
147
+ lock_info = lock_manager.lock(resource, 2000)
148
+ sleep 1
149
+
150
+ lock_manager.get_remaining_ttl_for_lock(lock_info)
151
+ #=> 986
152
+
153
+ lock_manager.unlock(lock_info)
154
+ lock_manager.get_remaining_ttl_for_lock(lock_info)
155
+ #=> nil
156
+ ```
157
+
158
+ Use `get_remaining_ttl_for_resource` if you do not hold a lock, but want to know the remaining TTL on a locked resource:
159
+ ```ruby
160
+ # Some part of the code
161
+ resource = "resource_key"
162
+ lock_info = lock_manager.lock(resource, 2000)
163
+
164
+ # Some other part of the code
165
+ lock_manager.locked?(resource)
166
+ #=> true
167
+ lock_manager.get_remaining_ttl_for_resource(resource)
168
+ #=> 1975
169
+
170
+ # Sometime later
171
+ lock_manager.locked?(resource)
172
+ #=> false
173
+ lock_manager.get_remaining_ttl_for_resource(resource)
174
+ #=> nil
175
+ ```
176
+
177
+ ## Redis client configuration
178
+
179
+ `Redlock::Client` expects URLs or Redis objects on initialization. Redis objects should be used for configuring the connection in more detail, i.e. setting username and password.
180
+
181
+ ```ruby
182
+ servers = [ 'redis://localhost:6379', Redis.new(:url => 'redis://someotherhost:6379') ]
183
+ redlock = Redlock::Client.new(servers)
184
+ ```
185
+
186
+ Redlock works seamlessly with [redis sentinel](http://redis.io/topics/sentinel), which is supported in redis 3.2+.
187
+
188
+ ## Redlock configuration
189
+
124
190
  It's possible to customize the retry logic providing the following options:
125
191
 
126
192
  ```ruby
@@ -133,8 +199,15 @@ It's possible to customize the retry logic providing the following options:
133
199
  })
134
200
  ```
135
201
 
136
- For more information you can check [documentation](http://www.rubydoc.info/gems/redlock/Redlock%2FClient:initialize).
202
+ It is possible to associate `:retry_delay` option with `Proc` object. It will be called every time, with attempt number
203
+ as argument, to get delay time value before next retry.
204
+
205
+ ```ruby
206
+ retry_delay = proc { |attempt_number| 200 * attempt_number ** 2 } # delay of 200ms for 1st retry, 800ms for 2nd retry, etc.
207
+ lock_manager = Redlock::Client.new(servers, retry_delay: retry_delay)
208
+ ```
137
209
 
210
+ For more information you can check [documentation](http://www.rubydoc.info/gems/redlock/Redlock%2FClient:initialize).
138
211
 
139
212
  ## Run tests
140
213
 
@@ -11,17 +11,23 @@ services:
11
11
  - REDIS1_PORT=6379
12
12
  - REDIS2_HOST=redis2.local.com
13
13
  - REDIS2_PORT=6379
14
+ - REDIS3_HOST=redis3.local.com
15
+ - REDIS3_PORT=6379
14
16
  - DEFAULT_REDIS_HOST=redis1.local.com
15
17
  - DEFAULT_REDIS_PORT=6379
16
18
  links:
17
19
  - redis1:redis1.local.com
18
20
  - redis2:redis2.local.com
21
+ - redis3:redis3.local.com
19
22
  depends_on:
20
23
  - redis1
21
24
  - redis2
25
+ - redis3
22
26
 
23
27
  redis1:
24
28
  image: redis
25
29
  redis2:
26
30
  image: redis
31
+ redis3:
32
+ image: redis
27
33
 
@@ -3,5 +3,9 @@ require 'redlock/version'
3
3
  module Redlock
4
4
  autoload :Client, 'redlock/client'
5
5
 
6
- LockError = Class.new(StandardError)
6
+ class LockError < StandardError
7
+ def initialize(resource)
8
+ super "failed to acquire lock on '#{resource}'".freeze
9
+ end
10
+ end
7
11
  end
@@ -12,14 +12,27 @@ module Redlock
12
12
  DEFAULT_RETRY_JITTER = 50
13
13
  CLOCK_DRIFT_FACTOR = 0.01
14
14
 
15
+ ##
16
+ # Returns default time source function depending on CLOCK_MONOTONIC availability.
17
+ #
18
+ def self.default_time_source
19
+ if defined?(Process::CLOCK_MONOTONIC)
20
+ proc { (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i }
21
+ else
22
+ proc { (Time.now.to_f * 1000).to_i }
23
+ end
24
+ end
25
+
15
26
  # Create a distributed lock manager implementing redlock algorithm.
16
27
  # Params:
17
28
  # +servers+:: The array of redis connection URLs or Redis connection instances. Or a mix of both.
18
- # +options+:: You can override the default value for `retry_count`, `retry_delay` and `retry_gitter`.
29
+ # +options+::
19
30
  # * `retry_count` being how many times it'll try to lock a resource (default: 3)
20
31
  # * `retry_delay` being how many ms to sleep before try to lock again (default: 200)
21
32
  # * `retry_jitter` being how many ms to jitter retry delay (default: 50)
22
33
  # * `redis_timeout` being how the Redis timeout will be set in seconds (default: 0.1)
34
+ # * `time_source` being a callable object returning a monotonic time in milliseconds
35
+ # (default: see #default_time_source)
23
36
  def initialize(servers = DEFAULT_REDIS_URLS, options = {})
24
37
  redis_timeout = options[:redis_timeout] || DEFAULT_REDIS_TIMEOUT
25
38
  @servers = servers.map do |server|
@@ -33,6 +46,7 @@ module Redlock
33
46
  @retry_count = options[:retry_count] || DEFAULT_RETRY_COUNT
34
47
  @retry_delay = options[:retry_delay] || DEFAULT_RETRY_DELAY
35
48
  @retry_jitter = options[:retry_jitter] || DEFAULT_RETRY_JITTER
49
+ @time_source = options[:time_source] || self.class.default_time_source
36
50
  end
37
51
 
38
52
  # Locks a resource for a given time.
@@ -41,11 +55,21 @@ module Redlock
41
55
  # +ttl+:: The time-to-live in ms for the lock.
42
56
  # +options+:: Hash of optional parameters
43
57
  # * +extend+: A lock ("lock_info") to extend.
44
- # * +extend_only_if_life+: If +extend+ is given, only acquire lock if currently held
58
+ # * +extend_only_if_locked+: Boolean, if +extend+ is given, only acquire lock if currently held
59
+ # * +extend_only_if_life+: Deprecated, same as +extend_only_if_locked+
60
+ # * +extend_life+: Deprecated, same as +extend_only_if_locked+
45
61
  # +block+:: an optional block to be executed; after its execution, the lock (if successfully
46
62
  # acquired) is automatically unlocked.
47
63
  def lock(resource, ttl, options = {}, &block)
48
64
  lock_info = try_lock_instances(resource, ttl, options)
65
+ if options[:extend_only_if_life] && !Gem::Deprecate.skip
66
+ warn 'DEPRECATION WARNING: The `extend_only_if_life` option has been renamed `extend_only_if_locked`.'
67
+ options[:extend_only_if_locked] = options[:extend_only_if_life]
68
+ end
69
+ if options[:extend_life] && !Gem::Deprecate.skip
70
+ warn 'DEPRECATION WARNING: The `extend_life` option has been renamed `extend_only_if_locked`.'
71
+ options[:extend_only_if_locked] = options[:extend_life]
72
+ end
49
73
 
50
74
  if block_given?
51
75
  begin
@@ -69,15 +93,52 @@ module Redlock
69
93
  # Locks a resource, executing the received block only after successfully acquiring the lock,
70
94
  # and returning its return value as a result.
71
95
  # See Redlock::Client#lock for parameters.
72
- def lock!(*args)
96
+ def lock!(resource, *args)
73
97
  fail 'No block passed' unless block_given?
74
98
 
75
- lock(*args) do |lock_info|
76
- raise LockError, 'failed to acquire lock' unless lock_info
99
+ lock(resource, *args) do |lock_info|
100
+ raise LockError, resource unless lock_info
77
101
  return yield
78
102
  end
79
103
  end
80
104
 
105
+ # Gets remaining ttl of a resource. The ttl is returned if the holder
106
+ # currently holds the lock and it has not expired, otherwise the method
107
+ # returns nil.
108
+ # Params:
109
+ # +lock_info+:: the lock that has been acquired when you locked the resource
110
+ def get_remaining_ttl_for_lock(lock_info)
111
+ ttl_info = try_get_remaining_ttl(lock_info[:resource])
112
+ return nil if ttl_info.nil? || ttl_info[:value] != lock_info[:value]
113
+ ttl_info[:ttl]
114
+ end
115
+
116
+ # Gets remaining ttl of a resource. If there is no valid lock, the method
117
+ # returns nil.
118
+ # Params:
119
+ # +resource+:: the name of the resource (string) for which to check the ttl
120
+ def get_remaining_ttl_for_resource(resource)
121
+ ttl_info = try_get_remaining_ttl(resource)
122
+ return nil if ttl_info.nil?
123
+ ttl_info[:ttl]
124
+ end
125
+
126
+ # Checks if a resource is locked
127
+ # Params:
128
+ # +lock_info+:: the lock that has been acquired when you locked the resource
129
+ def locked?(resource)
130
+ ttl = get_remaining_ttl_for_resource(resource)
131
+ !(ttl.nil? || ttl.zero?)
132
+ end
133
+
134
+ # Checks if a lock is still valid
135
+ # Params:
136
+ # +lock_info+:: the lock that has been acquired when you locked the resource
137
+ def valid_lock?(lock_info)
138
+ ttl = get_remaining_ttl_for_lock(lock_info)
139
+ !(ttl.nil? || ttl.zero?)
140
+ end
141
+
81
142
  private
82
143
 
83
144
  class RedisInstance
@@ -98,11 +159,26 @@ module Redlock
98
159
  end
99
160
  eos
100
161
 
162
+ PTTL_SCRIPT = <<-eos
163
+ return { redis.call("get", KEYS[1]), redis.call("pttl", KEYS[1]) }
164
+ eos
165
+
166
+ module ConnectionPoolLike
167
+ def with
168
+ yield self
169
+ end
170
+ end
171
+
101
172
  def initialize(connection)
102
- if connection.respond_to?(:client)
173
+ if connection.respond_to?(:with)
103
174
  @redis = connection
104
175
  else
105
- @redis = Redis.new(connection)
176
+ if connection.respond_to?(:client)
177
+ @redis = connection
178
+ else
179
+ @redis = Redis.new(connection)
180
+ end
181
+ @redis.extend(ConnectionPoolLike)
106
182
  end
107
183
 
108
184
  load_scripts
@@ -110,23 +186,34 @@ module Redlock
110
186
 
111
187
  def lock(resource, val, ttl, allow_new_lock)
112
188
  recover_from_script_flush do
113
- @redis.evalsha @lock_script_sha, keys: [resource], argv: [val, ttl, allow_new_lock]
189
+ @redis.with { |conn| conn.evalsha @lock_script_sha, keys: [resource], argv: [val, ttl, allow_new_lock] }
114
190
  end
191
+ rescue Redis::BaseConnectionError
192
+ false
115
193
  end
116
194
 
117
195
  def unlock(resource, val)
118
196
  recover_from_script_flush do
119
- @redis.evalsha @unlock_script_sha, keys: [resource], argv: [val]
197
+ @redis.with { |conn| conn.evalsha @unlock_script_sha, keys: [resource], argv: [val] }
120
198
  end
121
199
  rescue
122
200
  # Nothing to do, unlocking is just a best-effort attempt.
123
201
  end
124
202
 
203
+ def get_remaining_ttl(resource)
204
+ recover_from_script_flush do
205
+ @redis.with { |conn| conn.evalsha @pttl_script_sha, keys: [resource] }
206
+ end
207
+ rescue Redis::BaseConnectionError
208
+ nil
209
+ end
210
+
125
211
  private
126
212
 
127
213
  def load_scripts
128
- @unlock_script_sha = @redis.script(:load, UNLOCK_SCRIPT)
129
- @lock_script_sha = @redis.script(:load, LOCK_SCRIPT)
214
+ @unlock_script_sha = @redis.with { |conn| conn.script(:load, UNLOCK_SCRIPT) }
215
+ @lock_script_sha = @redis.with { |conn| conn.script(:load, LOCK_SCRIPT) }
216
+ @pttl_script_sha = @redis.with { |conn| conn.script(:load, PTTL_SCRIPT) }
130
217
  end
131
218
 
132
219
  def recover_from_script_flush
@@ -149,11 +236,11 @@ module Redlock
149
236
  end
150
237
 
151
238
  def try_lock_instances(resource, ttl, options)
152
- tries = options[:extend] ? 1 : @retry_count
239
+ tries = options[:extend] ? 1 : (@retry_count + 1)
153
240
 
154
241
  tries.times do |attempt_number|
155
242
  # Wait a random delay before retrying.
156
- sleep((@retry_delay + rand(@retry_jitter)).to_f / 1000) if attempt_number > 0
243
+ sleep(attempt_retry_delay(attempt_number)) if attempt_number > 0
157
244
 
158
245
  lock_info = lock_instances(resource, ttl, options)
159
246
  return lock_info if lock_info
@@ -162,9 +249,20 @@ module Redlock
162
249
  false
163
250
  end
164
251
 
252
+ def attempt_retry_delay(attempt_number)
253
+ retry_delay =
254
+ if @retry_delay.respond_to?(:call)
255
+ @retry_delay.call(attempt_number)
256
+ else
257
+ @retry_delay
258
+ end
259
+
260
+ (retry_delay + rand(@retry_jitter)).to_f / 1000
261
+ end
262
+
165
263
  def lock_instances(resource, ttl, options)
166
264
  value = (options[:extend] || { value: SecureRandom.uuid })[:value]
167
- allow_new_lock = (options[:extend_life] || options[:extend_only_if_life]) ? 'no' : 'yes'
265
+ allow_new_lock = options[:extend_only_if_locked] ? 'no' : 'yes'
168
266
 
169
267
  locked, time_elapsed = timed do
170
268
  @servers.select { |s| s.lock resource, value, ttl, allow_new_lock }.size
@@ -180,6 +278,37 @@ module Redlock
180
278
  end
181
279
  end
182
280
 
281
+ def try_get_remaining_ttl(resource)
282
+ # Responses from the servers are a 2 tuple of format [lock_value, ttl].
283
+ # The lock_value is nil if it does not exist. Since servers may have
284
+ # different lock values, the responses are grouped by the lock_value and
285
+ # transofrmed into a hash: { lock_value1 => [ttl1, ttl2, ttl3],
286
+ # lock_value2 => [ttl4, tt5] }
287
+ ttls_by_value, time_elapsed = timed do
288
+ @servers.map { |s| s.get_remaining_ttl(resource) }
289
+ .select { |ttl_tuple| ttl_tuple&.first }
290
+ .group_by(&:first)
291
+ .transform_values { |ttl_tuples| ttl_tuples.map { |t| t.last } }
292
+ end
293
+
294
+ # Authoritative lock value is that which is returned by the majority of
295
+ # servers
296
+ authoritative_value, ttls =
297
+ ttls_by_value.max_by { |(lock_value, ttls)| ttls.length }
298
+
299
+ if ttls && ttls.size >= @quorum
300
+ # Return the minimum TTL of an N/2+1 selection. It will always be
301
+ # correct (it will guarantee that at least N/2+1 servers have a TTL that
302
+ # value or longer)
303
+ min_ttl = ttls.sort.last(@quorum).first
304
+ min_ttl = min_ttl - time_elapsed - drift(min_ttl)
305
+ { value: authoritative_value, ttl: min_ttl }
306
+ else
307
+ # No lock_value is authoritatively held for the resource
308
+ nil
309
+ end
310
+ end
311
+
183
312
  def drift(ttl)
184
313
  # Add 2 milliseconds to the drift to account for Redis expires
185
314
  # precision, which is 1 millisecond, plus 1 millisecond min drift
@@ -188,8 +317,8 @@ module Redlock
188
317
  end
189
318
 
190
319
  def timed
191
- start_time = (Time.now.to_f * 1000).to_i
192
- [yield, (Time.now.to_f * 1000).to_i - start_time]
320
+ start_time = @time_source.call()
321
+ [yield, @time_source.call() - start_time]
193
322
  end
194
323
  end
195
324
  end
@@ -1,17 +1,29 @@
1
+ require 'redlock'
2
+
1
3
  module Redlock
2
4
  class Client
3
- attr_writer :testing_mode
5
+ class << self
6
+ attr_accessor :testing_mode
7
+ end
8
+
9
+ def testing_mode=(mode)
10
+ warn 'DEPRECATION WARNING: Instance-level `testing_mode` has been removed, and this ' +
11
+ 'setter will be removed in the future. Please set the testing mode on the `Redlock::Client` ' +
12
+ 'instead, e.g. `Redlock::Client.testing_mode = :bypass`.'
13
+
14
+ self.class.testing_mode = mode
15
+ end
4
16
 
5
17
  alias_method :try_lock_instances_without_testing, :try_lock_instances
6
18
 
7
19
  def try_lock_instances(resource, ttl, options)
8
- if @testing_mode == :bypass
20
+ if self.class.testing_mode == :bypass
9
21
  {
10
22
  validity: ttl,
11
23
  resource: resource,
12
24
  value: options[:extend] ? options[:extend].fetch(:value) : SecureRandom.uuid
13
25
  }
14
- elsif @testing_mode == :fail
26
+ elsif self.class.testing_mode == :fail
15
27
  false
16
28
  else
17
29
  try_lock_instances_without_testing resource, ttl, options
@@ -21,14 +33,14 @@ module Redlock
21
33
  alias_method :unlock_without_testing, :unlock
22
34
 
23
35
  def unlock(lock_info)
24
- unlock_without_testing lock_info unless @testing_mode == :bypass
36
+ unlock_without_testing lock_info unless self.class.testing_mode == :bypass
25
37
  end
26
38
 
27
39
  class RedisInstance
28
40
  alias_method :load_scripts_without_testing, :load_scripts
29
41
 
30
42
  def load_scripts
31
- load_scripts_without_testing
43
+ load_scripts_without_testing unless Redlock::Client.testing_mode == :bypass
32
44
  rescue Redis::CommandError
33
45
  # FakeRedis doesn't have #script, but doesn't need it either.
34
46
  raise unless defined?(::FakeRedis)
@@ -1,3 +1,3 @@
1
1
  module Redlock
2
- VERSION = '0.2.2'
2
+ VERSION = '1.2.1'
3
3
  end
@@ -1,26 +1,29 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'redlock/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = "redlock"
8
+ spec.name = 'redlock'
8
9
  spec.version = Redlock::VERSION
9
- spec.authors = ["Leandro Moreira"]
10
- spec.email = ["leandro.ribeiro.moreira@gmail.com"]
11
- spec.summary = %q{Distributed lock using Redis written in Ruby.}
12
- spec.description = %q{Distributed lock using Redis written in Ruby. Highly inspired by https://github.com/antirez/redlock-rb.}
13
- spec.homepage = "https://github.com/leandromoreira/redlock-rb"
10
+ spec.authors = ['Leandro Moreira']
11
+ spec.email = ['leandro.ribeiro.moreira@gmail.com']
12
+ spec.summary = 'Distributed lock using Redis written in Ruby.'
13
+ spec.description = 'Distributed lock using Redis written in Ruby. Highly inspired by https://github.com/antirez/redlock-rb.'
14
+ spec.homepage = 'https://github.com/leandromoreira/redlock-rb'
14
15
  spec.license = 'BSD-2-Clause'
15
16
 
16
17
  spec.files = `git ls-files -z`.split("\x0")
17
18
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
19
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
20
+ spec.require_paths = ['lib']
20
21
 
21
22
  spec.add_dependency 'redis', '>= 3.0.0', '< 5.0'
22
23
 
23
- spec.add_development_dependency "coveralls", "~> 0.8.13"
24
- spec.add_development_dependency 'rake', '~> 11.1', '>= 11.1.2'
24
+ spec.add_development_dependency 'connection_pool', '~> 2.2'
25
+ spec.add_development_dependency 'coveralls', '~> 0.8'
26
+ spec.add_development_dependency 'json', '>= 2.3.0', '~> 2.3.1'
27
+ spec.add_development_dependency 'rake', '>= 11.1.2', '~> 13.0'
25
28
  spec.add_development_dependency 'rspec', '~> 3', '>= 3.0.0'
26
29
  end
@@ -1,20 +1,31 @@
1
1
  require 'spec_helper'
2
2
  require 'securerandom'
3
+ require 'redis'
4
+ require 'connection_pool'
3
5
 
4
6
  RSpec.describe Redlock::Client do
5
7
  # It is recommended to have at least 3 servers in production
6
8
  let(:lock_manager_opts) { { retry_count: 3 } }
7
9
  let(:lock_manager) { Redlock::Client.new(Redlock::Client::DEFAULT_REDIS_URLS, lock_manager_opts) }
10
+ let(:redis_client) { Redis.new(url: "redis://#{redis1_host}:#{redis1_port}") }
8
11
  let(:resource_key) { SecureRandom.hex(3) }
9
12
  let(:ttl) { 1000 }
10
13
  let(:redis1_host) { ENV["REDIS1_HOST"] || "localhost" }
11
14
  let(:redis1_port) { ENV["REDIS1_PORT"] || "6379" }
12
15
  let(:redis2_host) { ENV["REDIS2_HOST"] || "127.0.0.1" }
13
16
  let(:redis2_port) { ENV["REDIS2_PORT"] || "6379" }
17
+ let(:redis3_host) { ENV["REDIS3_HOST"] || "127.0.0.1" }
18
+ let(:redis3_port) { ENV["REDIS3_PORT"] || "6379" }
19
+ let(:unreachable_redis) {
20
+ redis = Redis.new(url: 'redis://localhost:46864')
21
+ def redis.with
22
+ yield self
23
+ end
24
+ redis
25
+ }
14
26
 
15
27
  describe 'initialize' do
16
28
  it 'accepts both redis URLs and Redis objects' do
17
- print redis1_host
18
29
  servers = [ "redis://#{redis1_host}:#{redis1_port}", Redis.new(url: "redis://#{redis2_host}:#{redis2_port}") ]
19
30
  redlock = Redlock::Client.new(servers)
20
31
 
@@ -24,6 +35,15 @@ RSpec.describe Redlock::Client do
24
35
 
25
36
  expect(redlock_servers).to match_array([redis1_host, redis2_host])
26
37
  end
38
+
39
+ it 'accepts ConnectionPool objects' do
40
+ pool = ConnectionPool.new { Redis.new(url: "redis://#{redis1_host}:#{redis1_port}") }
41
+ redlock = Redlock::Client.new([pool])
42
+
43
+ lock_info = lock_manager.lock(resource_key, ttl)
44
+ expect(resource_key).to_not be_lockable(lock_manager, ttl)
45
+ lock_manager.unlock(lock_info)
46
+ end
27
47
  end
28
48
 
29
49
  describe 'lock' do
@@ -42,6 +62,12 @@ RSpec.describe Redlock::Client do
42
62
  expect(@lock_info).to be_lock_info_for(resource_key)
43
63
  end
44
64
 
65
+ it 'interprets lock time as milliseconds' do
66
+ ttl = 20000
67
+ @lock_info = lock_manager.lock(resource_key, ttl)
68
+ expect(redis_client.pttl(resource_key)).to be_within(200).of(ttl)
69
+ end
70
+
45
71
  it 'can extend its own lock' do
46
72
  my_lock_info = lock_manager.lock(resource_key, ttl)
47
73
  @lock_info = lock_manager.lock(resource_key, ttl, extend: my_lock_info)
@@ -57,16 +83,26 @@ RSpec.describe Redlock::Client do
57
83
  end
58
84
  end
59
85
 
60
- context 'when extend_only_if_life flag is given' do
86
+ context 'when extend_only_if_locked flag is given' do
61
87
  it 'does not extend a non-existent lock' do
62
- @lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'}, extend_only_if_life: true)
88
+ @lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'}, extend_only_if_locked: true)
63
89
  expect(@lock_info).to eq(false)
64
90
  end
65
91
  end
66
92
 
67
- context 'when extend_only_if_life flag is not given' do
93
+ it '(when extending) resets the TTL, rather than adding extra time to it' do
94
+ ttl = 20000
95
+ lock_info = lock_manager.lock(resource_key, ttl)
96
+ expect(resource_key).to_not be_lockable(lock_manager, ttl)
97
+
98
+ lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_only_if_locked: true)
99
+ expect(lock_info).not_to be_nil
100
+ expect(redis_client.pttl(resource_key)).to be_within(200).of(ttl)
101
+ end
102
+
103
+ context 'when extend_only_if_locked flag is not given' do
68
104
  it "sets the given value when trying to extend a non-existent lock" do
69
- @lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'}, extend_only_if_life: false)
105
+ @lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'}, extend_only_if_locked: false)
70
106
  expect(@lock_info).to be_lock_info_for(resource_key)
71
107
  expect(@lock_info[:value]).to eq('hello world') # really we should test what's in redis
72
108
  end
@@ -77,6 +113,28 @@ RSpec.describe Redlock::Client do
77
113
  second_attempt = lock_manager.lock(resource_key, ttl)
78
114
  expect(second_attempt).to eq(false)
79
115
  end
116
+
117
+ context 'when extend_life flag is given' do
118
+ it 'treats it as extend_only_if_locked but warns it is deprecated' do
119
+ ttl = 20_000
120
+ lock_info = lock_manager.lock(resource_key, ttl)
121
+ expect(resource_key).to_not be_lockable(lock_manager, ttl)
122
+ expect(lock_manager).to receive(:warn).with(/DEPRECATION WARNING: The `extend_life`/)
123
+ lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_life: true)
124
+ expect(lock_info).not_to be_nil
125
+ end
126
+ end
127
+
128
+ context 'when extend_only_if_life flag is given' do
129
+ it 'treats it as extend_only_if_locked but warns it is deprecated' do
130
+ ttl = 20_000
131
+ lock_info = lock_manager.lock(resource_key, ttl)
132
+ expect(resource_key).to_not be_lockable(lock_manager, ttl)
133
+ expect(lock_manager).to receive(:warn).with(/DEPRECATION WARNING: The `extend_only_if_life`/)
134
+ lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_only_if_life: true)
135
+ expect(lock_info).not_to be_nil
136
+ end
137
+ end
80
138
  end
81
139
 
82
140
  context 'when lock is not available' do
@@ -95,14 +153,14 @@ RSpec.describe Redlock::Client do
95
153
  expect(lock_info).to eql(false)
96
154
  end
97
155
 
98
- it 'retries up to \'retry_count\' times' do
156
+ it 'tries up to \'retry_count\' + 1 times' do
99
157
  expect(lock_manager).to receive(:lock_instances).exactly(
100
- lock_manager_opts[:retry_count]).times.and_return(false)
158
+ lock_manager_opts[:retry_count] + 1).times.and_return(false)
101
159
  lock_manager.lock(resource_key, ttl)
102
160
  end
103
161
 
104
162
  it 'sleeps in between retries' do
105
- expect(lock_manager).to receive(:sleep).exactly(lock_manager_opts[:retry_count] - 1).times
163
+ expect(lock_manager).to receive(:sleep).exactly(lock_manager_opts[:retry_count]).times
106
164
  lock_manager.lock(resource_key, ttl)
107
165
  end
108
166
 
@@ -121,6 +179,46 @@ RSpec.describe Redlock::Client do
121
179
  end.at_least(:once)
122
180
  lock_manager.lock(resource_key, ttl)
123
181
  end
182
+
183
+ it 'accepts retry_delay as proc' do
184
+ retry_delay = proc do |attempt_number|
185
+ expect(attempt_number).to eq(1)
186
+ 2000
187
+ end
188
+
189
+ lock_manager = Redlock::Client.new(Redlock::Client::DEFAULT_REDIS_URLS, retry_count: 1, retry_delay: retry_delay)
190
+ another_lock_info = lock_manager.lock(resource_key, ttl)
191
+
192
+ expect(lock_manager).to receive(:sleep) do |sleep|
193
+ expect(sleep * 1000).to be_within(described_class::DEFAULT_RETRY_JITTER).of(2000)
194
+ end.exactly(:once)
195
+ lock_manager.lock(resource_key, ttl)
196
+ lock_manager.unlock(another_lock_info)
197
+ end
198
+ end
199
+
200
+ context 'when a server goes away' do
201
+ it 'does not raise an error on connection issues' do
202
+ # We re-route the lock manager to a (hopefully) non-existent Redis URL.
203
+ redis_instance = lock_manager.instance_variable_get(:@servers).first
204
+ redis_instance.instance_variable_set(:@redis, unreachable_redis)
205
+
206
+ expect {
207
+ expect(lock_manager.lock(resource_key, ttl)).to be_falsey
208
+ }.to_not raise_error
209
+ end
210
+ end
211
+
212
+ context 'when a server comes back' do
213
+ it 'recovers from connection issues' do
214
+ # Same as above.
215
+ redis_instance = lock_manager.instance_variable_get(:@servers).first
216
+ old_redis = redis_instance.instance_variable_get(:@redis)
217
+ redis_instance.instance_variable_set(:@redis, unreachable_redis)
218
+ expect(lock_manager.lock(resource_key, ttl)).to be_falsey
219
+ redis_instance.instance_variable_set(:@redis, old_redis)
220
+ expect(lock_manager.lock(resource_key, ttl)).to be_truthy
221
+ end
124
222
  end
125
223
 
126
224
  context 'when script cache has been flushed' do
@@ -254,7 +352,9 @@ RSpec.describe Redlock::Client do
254
352
  after { lock_manager.unlock(@another_lock_info) }
255
353
 
256
354
  it 'raises a LockError' do
257
- expect { lock_manager.lock!(resource_key, ttl) {} }.to raise_error(Redlock::LockError)
355
+ expect { lock_manager.lock!(resource_key, ttl) {} }.to raise_error(
356
+ Redlock::LockError, "failed to acquire lock on '#{resource_key}'"
357
+ )
258
358
  end
259
359
 
260
360
  it 'does not execute the block' do
@@ -267,4 +367,171 @@ RSpec.describe Redlock::Client do
267
367
  end
268
368
  end
269
369
  end
370
+
371
+ describe 'get_remaining_ttl_for_resource' do
372
+ context 'when lock is valid' do
373
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
374
+
375
+ it 'gets the remaining ttl of a lock' do
376
+ ttl = 20_000
377
+ @lock_info = lock_manager.lock(resource_key, ttl)
378
+ remaining_ttl = lock_manager.get_remaining_ttl_for_resource(resource_key)
379
+ expect(remaining_ttl).to be_within(300).of(ttl)
380
+ end
381
+
382
+ context 'when servers respond with varying ttls' do
383
+ let (:servers) {
384
+ [
385
+ "redis://#{redis1_host}:#{redis1_port}",
386
+ "redis://#{redis2_host}:#{redis2_port}",
387
+ "redis://#{redis3_host}:#{redis3_port}"
388
+ ]
389
+ }
390
+ let (:redlock) { Redlock::Client.new(servers) }
391
+ after(:each) { redlock.unlock(@lock_info) if @lock_info }
392
+
393
+ it 'returns the minimum ttl value' do
394
+ ttl = 20_000
395
+ @lock_info = redlock.lock(resource_key, ttl)
396
+
397
+ # Mock redis server responses to return different ttls
398
+ returned_ttls = [20_000, 15_000, 10_000]
399
+ redlock.instance_variable_get(:@servers).each_with_index do |server, index|
400
+ allow(server).to(receive(:get_remaining_ttl))
401
+ .with(resource_key)
402
+ .and_return([@lock_info[:value], returned_ttls[index]])
403
+ end
404
+
405
+ remaining_ttl = redlock.get_remaining_ttl_for_lock(@lock_info)
406
+
407
+ # Assert that the TTL is closest to the closest to the correct value
408
+ expect(remaining_ttl).to be_within(300).of(returned_ttls[1])
409
+ end
410
+ end
411
+ end
412
+
413
+ context 'when lock is not valid' do
414
+ it 'returns nil' do
415
+ lock_info = lock_manager.lock(resource_key, ttl)
416
+ lock_manager.unlock(lock_info)
417
+ remaining_ttl = lock_manager.get_remaining_ttl_for_resource(resource_key)
418
+ expect(remaining_ttl).to be_nil
419
+ end
420
+ end
421
+
422
+ context 'when server goes away' do
423
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
424
+
425
+ it 'does not raise an error on connection issues' do
426
+ @lock_info = lock_manager.lock(resource_key, ttl)
427
+
428
+ # Replace redis with unreachable instance
429
+ redis_instance = lock_manager.instance_variable_get(:@servers).first
430
+ old_redis = redis_instance.instance_variable_get(:@redis)
431
+ redis_instance.instance_variable_set(:@redis, unreachable_redis)
432
+
433
+ expect {
434
+ remaining_ttl = lock_manager.get_remaining_ttl_for_resource(resource_key)
435
+ expect(remaining_ttl).to be_nil
436
+ }.to_not raise_error
437
+ end
438
+ end
439
+
440
+ context 'when a server comes back' do
441
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
442
+
443
+ it 'recovers from connection issues' do
444
+ @lock_info = lock_manager.lock(resource_key, ttl)
445
+
446
+ # Replace redis with unreachable instance
447
+ redis_instance = lock_manager.instance_variable_get(:@servers).first
448
+ old_redis = redis_instance.instance_variable_get(:@redis)
449
+ redis_instance.instance_variable_set(:@redis, unreachable_redis)
450
+
451
+ expect(lock_manager.get_remaining_ttl_for_resource(resource_key)).to be_nil
452
+
453
+ # Restore redis
454
+ redis_instance.instance_variable_set(:@redis, old_redis)
455
+ expect(lock_manager.get_remaining_ttl_for_resource(resource_key)).to be_truthy
456
+ end
457
+ end
458
+ end
459
+
460
+ describe 'get_remaining_ttl_for_lock' do
461
+ context 'when lock is valid' do
462
+ it 'gets the remaining ttl of a lock' do
463
+ ttl = 20_000
464
+ lock_info = lock_manager.lock(resource_key, ttl)
465
+ remaining_ttl = lock_manager.get_remaining_ttl_for_lock(lock_info)
466
+ expect(remaining_ttl).to be_within(300).of(ttl)
467
+ lock_manager.unlock(lock_info)
468
+ end
469
+ end
470
+
471
+ context 'when lock is not valid' do
472
+ it 'returns nil' do
473
+ lock_info = lock_manager.lock(resource_key, ttl)
474
+ lock_manager.unlock(lock_info)
475
+ remaining_ttl = lock_manager.get_remaining_ttl_for_lock(lock_info)
476
+ expect(remaining_ttl).to be_nil
477
+ end
478
+ end
479
+ end
480
+
481
+ describe 'locked?' do
482
+ context 'when lock is available' do
483
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
484
+
485
+ it 'returns true' do
486
+ @lock_info = lock_manager.lock(resource_key, ttl)
487
+ expect(lock_manager).to be_locked(resource_key)
488
+ end
489
+ end
490
+
491
+ context 'when lock is not available' do
492
+ it 'returns false' do
493
+ lock_info = lock_manager.lock(resource_key, ttl)
494
+ lock_manager.unlock(lock_info)
495
+ expect(lock_manager).not_to be_locked(resource_key)
496
+ end
497
+ end
498
+ end
499
+
500
+ describe 'valid_lock?' do
501
+ context 'when lock is available' do
502
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
503
+
504
+ it 'returns true' do
505
+ @lock_info = lock_manager.lock(resource_key, ttl)
506
+ expect(lock_manager).to be_valid_lock(@lock_info)
507
+ end
508
+ end
509
+
510
+ context 'when lock is not available' do
511
+ it 'returns false' do
512
+ lock_info = lock_manager.lock(resource_key, ttl)
513
+ lock_manager.unlock(lock_info)
514
+ expect(lock_manager).not_to be_valid_lock(lock_info)
515
+ end
516
+ end
517
+ end
518
+
519
+ describe '#default_time_source' do
520
+ context 'when CLOCK_MONOTONIC is available (MRI, JRuby)' do
521
+ it 'returns a callable using Process.clock_gettime()' do
522
+ skip 'CLOCK_MONOTONIC not defined' unless defined?(Process::CLOCK_MONOTONIC)
523
+ expect(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC).and_call_original
524
+ Redlock::Client.default_time_source.call()
525
+ end
526
+ end
527
+
528
+ context 'when CLOCK_MONOTONIC is not available' do
529
+ it 'returns a callable using Time.now()' do
530
+ cm = Process.send(:remove_const, :CLOCK_MONOTONIC)
531
+ expect(Time).to receive(:now).and_call_original
532
+ Redlock::Client.default_time_source.call()
533
+ Process.const_set(:CLOCK_MONOTONIC, cm) if cm
534
+ end
535
+ end
536
+ end
270
537
  end
@@ -41,3 +41,11 @@ RSpec::Matchers.define :be_lockable do |lock_manager, ttl|
41
41
  "expected that #{resource_key} would be lockable"
42
42
  end
43
43
  end
44
+
45
+ RSpec.configure do |c|
46
+ # NOTE: this protects against erroneous "focus: true" commits
47
+ unless ENV['CI'] == 'true'
48
+ c.filter_run focus: true
49
+ c.run_all_when_everything_filtered = true
50
+ end
51
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redlock
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leandro Moreira
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-12-22 00:00:00.000000000 Z
11
+ date: 2020-12-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -31,39 +31,73 @@ dependencies:
31
31
  - !ruby/object:Gem::Version
32
32
  version: '5.0'
33
33
  - !ruby/object:Gem::Dependency
34
- name: coveralls
34
+ name: connection_pool
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 0.8.13
39
+ version: '2.2'
40
40
  type: :development
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 0.8.13
46
+ version: '2.2'
47
47
  - !ruby/object:Gem::Dependency
48
- name: rake
48
+ name: coveralls
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '11.1'
53
+ version: '0.8'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.8'
61
+ - !ruby/object:Gem::Dependency
62
+ name: json
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
54
65
  - - ">="
55
66
  - !ruby/object:Gem::Version
56
- version: 11.1.2
67
+ version: 2.3.0
68
+ - - "~>"
69
+ - !ruby/object:Gem::Version
70
+ version: 2.3.1
57
71
  type: :development
58
72
  prerelease: false
59
73
  version_requirements: !ruby/object:Gem::Requirement
60
74
  requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: 2.3.0
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: 2.3.1
81
+ - !ruby/object:Gem::Dependency
82
+ name: rake
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: 11.1.2
61
88
  - - "~>"
62
89
  - !ruby/object:Gem::Version
63
- version: '11.1'
90
+ version: '13.0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
64
95
  - - ">="
65
96
  - !ruby/object:Gem::Version
66
97
  version: 11.1.2
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: '13.0'
67
101
  - !ruby/object:Gem::Dependency
68
102
  name: rspec
69
103
  requirement: !ruby/object:Gem::Requirement
@@ -96,7 +130,6 @@ files:
96
130
  - ".travis.yml"
97
131
  - CONTRIBUTORS
98
132
  - Gemfile
99
- - Gemfile.lock
100
133
  - LICENSE
101
134
  - Makefile
102
135
  - README.md
@@ -130,8 +163,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
163
  - !ruby/object:Gem::Version
131
164
  version: '0'
132
165
  requirements: []
133
- rubyforge_project:
134
- rubygems_version: 2.5.2.1
166
+ rubygems_version: 3.1.4
135
167
  signing_key:
136
168
  specification_version: 4
137
169
  summary: Distributed lock using Redis written in Ruby.
@@ -1,54 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- redlock (0.2.2)
5
- redis (>= 3.0.0, < 5.0)
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- coveralls (0.8.19)
11
- json (>= 1.8, < 3)
12
- simplecov (~> 0.12.0)
13
- term-ansicolor (~> 1.3)
14
- thor (~> 0.19.1)
15
- tins (~> 1.6)
16
- diff-lcs (1.3)
17
- docile (1.1.5)
18
- json (2.0.3)
19
- rake (11.3.0)
20
- redis (4.0.1)
21
- rspec (3.5.0)
22
- rspec-core (~> 3.5.0)
23
- rspec-expectations (~> 3.5.0)
24
- rspec-mocks (~> 3.5.0)
25
- rspec-core (3.5.4)
26
- rspec-support (~> 3.5.0)
27
- rspec-expectations (3.5.0)
28
- diff-lcs (>= 1.2.0, < 2.0)
29
- rspec-support (~> 3.5.0)
30
- rspec-mocks (3.5.0)
31
- diff-lcs (>= 1.2.0, < 2.0)
32
- rspec-support (~> 3.5.0)
33
- rspec-support (3.5.0)
34
- simplecov (0.12.0)
35
- docile (~> 1.1.0)
36
- json (>= 1.8, < 3)
37
- simplecov-html (~> 0.10.0)
38
- simplecov-html (0.10.0)
39
- term-ansicolor (1.4.0)
40
- tins (~> 1.0)
41
- thor (0.19.4)
42
- tins (1.13.0)
43
-
44
- PLATFORMS
45
- ruby
46
-
47
- DEPENDENCIES
48
- coveralls (~> 0.8.13)
49
- rake (~> 11.1, >= 11.1.2)
50
- redlock!
51
- rspec (~> 3, >= 3.0.0)
52
-
53
- BUNDLED WITH
54
- 1.16.0