redlock 1.2.0 → 2.0.2

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
  SHA256:
3
- metadata.gz: f27e3a9be9009072c86f2f38a18bdfdcd2f177890aac365d981711dc040acd2e
4
- data.tar.gz: efedd1080833cdee147afe6427878ad0df7c824e981e4e8a48c563ac68350d05
3
+ metadata.gz: 8bdda4faebf87c2d1fb0026a3142497d5ae14092340bce6e35e46f51635e5c6a
4
+ data.tar.gz: a9f5636898a2a3ece98b95a286bd88f417e68d119b1f1ad60bd75b5d1da94baf
5
5
  SHA512:
6
- metadata.gz: aed115da4ab51ae932533d73e2f2f79d7722e6d77471028bb685d89149d6085fd395af368e9a143231b25b680a57c1ad4d236365396974c12416dac1131cabd8
7
- data.tar.gz: 75b69cd60c2f562b087179dc939c2fb2bbf3514018dd8c302cf4bc2c4cfa4352a1310dd7cfc35042badcd8c86fc5d237d3a6bcafc57385ca2afcc5a2a280c7bf
6
+ metadata.gz: 0b10ec10f00ce9282d604b1d0c8a2e845d8d95a6684a5a235d6d39b598efa6324725c4d088e8016b11d473b0195a1e29a451e79fad115f04f6b217493be297cf
7
+ data.tar.gz: e1d54bb8bb8a108e0989d1c11baa44cbfc4c971029fb23819d000758b42739698342ef803a826990757c888d113fb8a49663df1f3812603965c5498361596f86
@@ -0,0 +1,31 @@
1
+ name: Ruby CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ test:
11
+
12
+ runs-on: ubuntu-latest
13
+
14
+ strategy:
15
+ matrix:
16
+ ruby-version: [3.1, "3.0", "2.7", "2.6", "2.5", "ruby-head"]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v2
20
+ - name: Set up Ruby ${{ matrix.ruby-version }}
21
+ uses: ruby/setup-ruby@v1
22
+ with:
23
+ ruby-version: ${{ matrix.ruby-version }}
24
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
25
+ - name: Start Redis
26
+ uses: supercharge/redis-github-action@1.2.0
27
+ with:
28
+ redis-version: 6
29
+ - name: Run tests
30
+ run: bundle exec rspec
31
+
data/.gitignore CHANGED
@@ -1,3 +1,5 @@
1
1
  *.gem
2
2
  coverage/
3
3
  .bundle/
4
+ dump.rdb
5
+ Gemfile.lock
data/CHANGELOG.md ADDED
@@ -0,0 +1,40 @@
1
+
2
+ # Change Log
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
+ and this project adheres to [Semantic Versioning](http://semver.org/).
7
+
8
+ ## [Unreleased] - yyyy-mm-dd
9
+
10
+ Here we write upgrading notes for brands. It's a team effort to make them as
11
+ straightforward as possible.
12
+
13
+ ### Added
14
+
15
+ ### Changed
16
+
17
+ ### Fixed
18
+
19
+
20
+ ## [2.0.1] - 2023-02-14
21
+
22
+ ### Added
23
+
24
+ ### Changed
25
+
26
+ ### Fixed
27
+
28
+ * always treat redis instance as pool-like #125
29
+
30
+ ## [2.0.0] - 2023-02-09
31
+
32
+ ### Added
33
+
34
+ * support for redis >= 6.0
35
+
36
+ ### Changed
37
+
38
+ * **BREAKING**: The library now only works with `RedisClient` instance.
39
+
40
+ ### Fixed
data/README.md CHANGED
@@ -1,8 +1,7 @@
1
- [![Build Status](https://travis-ci.org/leandromoreira/redlock-rb.svg?branch=master)](https://travis-ci.org/leandromoreira/redlock-rb)
1
+ [![Build Status](https://github.com/leandromoreira/redlock-rb/actions/workflows/ci.yml/badge.svg)](https://github.com/leandromoreira/redlock-rb/actions/workflows/ci.yml)
2
2
  [![Coverage Status](https://coveralls.io/repos/leandromoreira/redlock-rb/badge.svg?branch=master)](https://coveralls.io/r/leandromoreira/redlock-rb?branch=master)
3
3
  [![Code Climate](https://codeclimate.com/github/leandromoreira/redlock-rb/badges/gpa.svg)](https://codeclimate.com/github/leandromoreira/redlock-rb)
4
4
  [![Gem Version](https://badge.fury.io/rb/redlock.svg)](http://badge.fury.io/rb/redlock)
5
- [![security](https://hakiri.io/github/leandromoreira/redlock-rb/master.svg)](https://hakiri.io/github/leandromoreira/redlock-rb/master)
6
5
  [![Inline docs](http://inch-ci.org/github/leandromoreira/redlock-rb.svg?branch=master)](http://inch-ci.org/github/leandromoreira/redlock-rb)
7
6
 
8
7
 
@@ -16,7 +15,8 @@ This is an implementation of a proposed [distributed lock algorithm with Redis](
16
15
 
17
16
  ## Compatibility
18
17
 
19
- Redlock works with Redis versions 2.6 or later.
18
+ * It works with Redis server versions 6.0 or later.
19
+ * Redlock >= 2.0 only works with [`RedisClient`](https://github.com/redis-rb/redis-client) client instance.
20
20
 
21
21
  ## Installation
22
22
 
@@ -112,12 +112,74 @@ The above code will also acquire the lock if the previous lock has expired and t
112
112
  lock_manager.lock("resource key", 3000, extend: lock_info, extend_only_if_locked: true)
113
113
  ```
114
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
+
115
177
  ## Redis client configuration
116
178
 
117
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.
118
180
 
119
181
  ```ruby
120
- servers = [ 'redis://localhost:6379', Redis.new(:url => 'redis://someotherhost:6379') ]
182
+ servers = [ 'redis://localhost:6379', RedisClient.new(:url => 'redis://someotherhost:6379') ]
121
183
  redlock = Redlock::Client.new(servers)
122
184
  ```
123
185
 
data/docker-compose.yml CHANGED
@@ -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
 
@@ -1,7 +1,18 @@
1
- require 'redis'
1
+ require 'redis-client'
2
2
  require 'securerandom'
3
3
 
4
4
  module Redlock
5
+ include Scripts
6
+
7
+ class LockAcquisitionError < StandardError
8
+ attr_reader :errors
9
+
10
+ def initialize(message, errors)
11
+ super(message)
12
+ @errors = errors
13
+ end
14
+ end
15
+
5
16
  class Client
6
17
  DEFAULT_REDIS_HOST = ENV["DEFAULT_REDIS_HOST"] || "localhost"
7
18
  DEFAULT_REDIS_PORT = ENV["DEFAULT_REDIS_PORT"] || "6379"
@@ -54,6 +65,9 @@ module Redlock
54
65
  # +resource+:: the resource (or key) string to be locked.
55
66
  # +ttl+:: The time-to-live in ms for the lock.
56
67
  # +options+:: Hash of optional parameters
68
+ # * +retry_count+: see +initialize+
69
+ # * +retry_delay+: see +initialize+
70
+ # * +retry_jitter+: see +initialize+
57
71
  # * +extend+: A lock ("lock_info") to extend.
58
72
  # * +extend_only_if_locked+: Boolean, if +extend+ is given, only acquire lock if currently held
59
73
  # * +extend_only_if_life+: Deprecated, same as +extend_only_if_locked+
@@ -102,26 +116,46 @@ module Redlock
102
116
  end
103
117
  end
104
118
 
119
+ # Gets remaining ttl of a resource. The ttl is returned if the holder
120
+ # currently holds the lock and it has not expired, otherwise the method
121
+ # returns nil.
122
+ # Params:
123
+ # +lock_info+:: the lock that has been acquired when you locked the resource
124
+ def get_remaining_ttl_for_lock(lock_info)
125
+ ttl_info = try_get_remaining_ttl(lock_info[:resource])
126
+ return nil if ttl_info.nil? || ttl_info[:value] != lock_info[:value]
127
+ ttl_info[:ttl]
128
+ end
129
+
130
+ # Gets remaining ttl of a resource. If there is no valid lock, the method
131
+ # returns nil.
132
+ # Params:
133
+ # +resource+:: the name of the resource (string) for which to check the ttl
134
+ def get_remaining_ttl_for_resource(resource)
135
+ ttl_info = try_get_remaining_ttl(resource)
136
+ return nil if ttl_info.nil?
137
+ ttl_info[:ttl]
138
+ end
139
+
140
+ # Checks if a resource is locked
141
+ # Params:
142
+ # +lock_info+:: the lock that has been acquired when you locked the resource
143
+ def locked?(resource)
144
+ ttl = get_remaining_ttl_for_resource(resource)
145
+ !(ttl.nil? || ttl.zero?)
146
+ end
147
+
148
+ # Checks if a lock is still valid
149
+ # Params:
150
+ # +lock_info+:: the lock that has been acquired when you locked the resource
151
+ def valid_lock?(lock_info)
152
+ ttl = get_remaining_ttl_for_lock(lock_info)
153
+ !(ttl.nil? || ttl.zero?)
154
+ end
155
+
105
156
  private
106
157
 
107
158
  class RedisInstance
108
- UNLOCK_SCRIPT = <<-eos
109
- if redis.call("get",KEYS[1]) == ARGV[1] then
110
- return redis.call("del",KEYS[1])
111
- else
112
- return 0
113
- end
114
- eos
115
-
116
- # thanks to https://github.com/sbertrang/redis-distlock/blob/master/lib/Redis/DistLock.pm
117
- # also https://github.com/sbertrang/redis-distlock/issues/2 which proposes the value-checking
118
- # and @maltoe for https://github.com/leandromoreira/redlock-rb/pull/20#discussion_r38903633
119
- LOCK_SCRIPT = <<-eos
120
- if (redis.call("exists", KEYS[1]) == 0 and ARGV[3] == "yes") or redis.call("get", KEYS[1]) == ARGV[1] then
121
- return redis.call("set", KEYS[1], ARGV[1], "PX", ARGV[2])
122
- end
123
- eos
124
-
125
159
  module ConnectionPoolLike
126
160
  def with
127
161
  yield self
@@ -135,42 +169,61 @@ module Redlock
135
169
  if connection.respond_to?(:client)
136
170
  @redis = connection
137
171
  else
138
- @redis = Redis.new(connection)
172
+ @redis = RedisClient.new(connection)
139
173
  end
140
174
  @redis.extend(ConnectionPoolLike)
141
175
  end
142
-
143
- load_scripts
144
176
  end
145
177
 
146
178
  def lock(resource, val, ttl, allow_new_lock)
147
179
  recover_from_script_flush do
148
- @redis.with { |conn| conn.evalsha @lock_script_sha, keys: [resource], argv: [val, ttl, allow_new_lock] }
180
+ @redis.with { |conn|
181
+ conn.call('EVALSHA', Scripts::LOCK_SCRIPT_SHA, 1, resource, val, ttl, allow_new_lock)
182
+ }
149
183
  end
150
- rescue Redis::BaseConnectionError
151
- false
152
184
  end
153
185
 
154
186
  def unlock(resource, val)
155
187
  recover_from_script_flush do
156
- @redis.with { |conn| conn.evalsha @unlock_script_sha, keys: [resource], argv: [val] }
188
+ @redis.with { |conn|
189
+ conn.call('EVALSHA', Scripts::UNLOCK_SCRIPT_SHA, 1, resource, val)
190
+ }
157
191
  end
158
192
  rescue
159
193
  # Nothing to do, unlocking is just a best-effort attempt.
160
194
  end
161
195
 
196
+ def get_remaining_ttl(resource)
197
+ recover_from_script_flush do
198
+ @redis.with { |conn|
199
+ conn.call('EVALSHA', Scripts::PTTL_SCRIPT_SHA, 1, resource)
200
+ }
201
+ end
202
+ rescue RedisClient::ConnectionError
203
+ nil
204
+ end
205
+
162
206
  private
163
207
 
164
208
  def load_scripts
165
- @unlock_script_sha = @redis.with { |conn| conn.script(:load, UNLOCK_SCRIPT) }
166
- @lock_script_sha = @redis.with { |conn| conn.script(:load, LOCK_SCRIPT) }
209
+ scripts = [
210
+ Scripts::UNLOCK_SCRIPT,
211
+ Scripts::LOCK_SCRIPT,
212
+ Scripts::PTTL_SCRIPT
213
+ ]
214
+
215
+ @redis.with do |connnection|
216
+ scripts.each do |script|
217
+ connnection.call('SCRIPT', 'LOAD', script)
218
+ end
219
+ end
167
220
  end
168
221
 
169
222
  def recover_from_script_flush
170
223
  retry_on_noscript = true
171
224
  begin
172
225
  yield
173
- rescue Redis::CommandError => e
226
+ rescue RedisClient::CommandError => e
174
227
  # When somebody has flushed the Redis instance's script cache, we might
175
228
  # want to reload our scripts. Only attempt this once, though, to avoid
176
229
  # going into an infinite loop.
@@ -186,36 +239,51 @@ module Redlock
186
239
  end
187
240
 
188
241
  def try_lock_instances(resource, ttl, options)
189
- tries = options[:extend] ? 1 : (@retry_count + 1)
242
+ retry_count = options[:retry_count] || @retry_count
243
+ tries = options[:extend] ? 1 : (retry_count + 1)
244
+ last_error = nil
190
245
 
191
246
  tries.times do |attempt_number|
192
247
  # Wait a random delay before retrying.
193
- sleep(attempt_retry_delay(attempt_number)) if attempt_number > 0
248
+ sleep(attempt_retry_delay(attempt_number, options)) if attempt_number > 0
194
249
 
195
250
  lock_info = lock_instances(resource, ttl, options)
196
251
  return lock_info if lock_info
252
+ rescue => e
253
+ last_error = e
197
254
  end
198
255
 
256
+ raise last_error if last_error
257
+
199
258
  false
200
259
  end
201
260
 
202
- def attempt_retry_delay(attempt_number)
261
+ def attempt_retry_delay(attempt_number, options)
262
+ retry_delay = options[:retry_delay] || @retry_delay
263
+ retry_jitter = options[:retry_jitter] || @retry_jitter
264
+
203
265
  retry_delay =
204
- if @retry_delay.respond_to?(:call)
205
- @retry_delay.call(attempt_number)
266
+ if retry_delay.respond_to?(:call)
267
+ retry_delay.call(attempt_number)
206
268
  else
207
- @retry_delay
269
+ retry_delay
208
270
  end
209
271
 
210
- (retry_delay + rand(@retry_jitter)).to_f / 1000
272
+ (retry_delay + rand(retry_jitter)).to_f / 1000
211
273
  end
212
274
 
213
275
  def lock_instances(resource, ttl, options)
214
276
  value = (options[:extend] || { value: SecureRandom.uuid })[:value]
215
277
  allow_new_lock = options[:extend_only_if_locked] ? 'no' : 'yes'
278
+ errors = []
216
279
 
217
280
  locked, time_elapsed = timed do
218
- @servers.select { |s| s.lock resource, value, ttl, allow_new_lock }.size
281
+ @servers.count do |s|
282
+ s.lock(resource, value, ttl, allow_new_lock)
283
+ rescue => e
284
+ errors << e
285
+ false
286
+ end
219
287
  end
220
288
 
221
289
  validity = ttl - time_elapsed - drift(ttl)
@@ -224,10 +292,46 @@ module Redlock
224
292
  { validity: validity, resource: resource, value: value }
225
293
  else
226
294
  @servers.each { |s| s.unlock(resource, value) }
295
+
296
+ if errors.size >= @quorum
297
+ raise LockAcquisitionError.new('Too many Redis errors prevented lock acquisition', errors)
298
+ end
299
+
227
300
  false
228
301
  end
229
302
  end
230
303
 
304
+ def try_get_remaining_ttl(resource)
305
+ # Responses from the servers are a 2 tuple of format [lock_value, ttl].
306
+ # The lock_value is nil if it does not exist. Since servers may have
307
+ # different lock values, the responses are grouped by the lock_value and
308
+ # transofrmed into a hash: { lock_value1 => [ttl1, ttl2, ttl3],
309
+ # lock_value2 => [ttl4, tt5] }
310
+ ttls_by_value, time_elapsed = timed do
311
+ @servers.map { |s| s.get_remaining_ttl(resource) }
312
+ .select { |ttl_tuple| ttl_tuple&.first }
313
+ .group_by(&:first)
314
+ .transform_values { |ttl_tuples| ttl_tuples.map { |t| t.last } }
315
+ end
316
+
317
+ # Authoritative lock value is that which is returned by the majority of
318
+ # servers
319
+ authoritative_value, ttls =
320
+ ttls_by_value.max_by { |(lock_value, ttls)| ttls.length }
321
+
322
+ if ttls && ttls.size >= @quorum
323
+ # Return the minimum TTL of an N/2+1 selection. It will always be
324
+ # correct (it will guarantee that at least N/2+1 servers have a TTL that
325
+ # value or longer)
326
+ min_ttl = ttls.sort.last(@quorum).first
327
+ min_ttl = min_ttl - time_elapsed - drift(min_ttl)
328
+ { value: authoritative_value, ttl: min_ttl }
329
+ else
330
+ # No lock_value is authoritatively held for the resource
331
+ nil
332
+ end
333
+ end
334
+
231
335
  def drift(ttl)
232
336
  # Add 2 milliseconds to the drift to account for Redis expires
233
337
  # precision, which is 1 millisecond, plus 1 millisecond min drift
@@ -0,0 +1,34 @@
1
+ require 'digest'
2
+
3
+ module Redlock
4
+ module Scripts
5
+ UNLOCK_SCRIPT = <<-eos
6
+ if redis.call("get",KEYS[1]) == ARGV[1] then
7
+ return redis.call("del",KEYS[1])
8
+ else
9
+ return 0
10
+ end
11
+ eos
12
+
13
+ # thanks to https://github.com/sbertrang/redis-distlock/blob/master/lib/Redis/DistLock.pm
14
+ # also https://github.com/sbertrang/redis-distlock/issues/2 which proposes the value-checking
15
+ # and @maltoe for https://github.com/leandromoreira/redlock-rb/pull/20#discussion_r38903633
16
+ LOCK_SCRIPT = <<-eos
17
+ if (redis.call("exists", KEYS[1]) == 0 and ARGV[3] == "yes") or redis.call("get", KEYS[1]) == ARGV[1] then
18
+ return redis.call("set", KEYS[1], ARGV[1], "PX", ARGV[2])
19
+ end
20
+ eos
21
+
22
+ PTTL_SCRIPT = <<-eos
23
+ return { redis.call("get", KEYS[1]), redis.call("pttl", KEYS[1]) }
24
+ eos
25
+
26
+ # We do not want to load the scripts on every Redlock::Client initialization.
27
+ # Hence, we rely on Redis handing out SHA1 hashes of the cached scripts and
28
+ # pre-calculate them instead of loading the scripts unconditionally. If the scripts
29
+ # have not been cached on Redis, `recover_from_script_flush` has our backs.
30
+ UNLOCK_SCRIPT_SHA = Digest::SHA1.hexdigest(UNLOCK_SCRIPT)
31
+ LOCK_SCRIPT_SHA = Digest::SHA1.hexdigest(LOCK_SCRIPT)
32
+ PTTL_SCRIPT_SHA = Digest::SHA1.hexdigest(PTTL_SCRIPT)
33
+ end
34
+ end
@@ -1,3 +1,5 @@
1
+ require 'redlock'
2
+
1
3
  module Redlock
2
4
  class Client
3
5
  class << self
@@ -39,7 +41,7 @@ module Redlock
39
41
 
40
42
  def load_scripts
41
43
  load_scripts_without_testing unless Redlock::Client.testing_mode == :bypass
42
- rescue Redis::CommandError
44
+ rescue RedisClient::CommandError
43
45
  # FakeRedis doesn't have #script, but doesn't need it either.
44
46
  raise unless defined?(::FakeRedis)
45
47
  rescue NoMethodError
@@ -1,3 +1,3 @@
1
1
  module Redlock
2
- VERSION = '1.2.0'
2
+ VERSION = '2.0.2'
3
3
  end
data/lib/redlock.rb CHANGED
@@ -2,6 +2,7 @@ require 'redlock/version'
2
2
 
3
3
  module Redlock
4
4
  autoload :Client, 'redlock/client'
5
+ autoload :Scripts, 'redlock/scripts'
5
6
 
6
7
  class LockError < StandardError
7
8
  def initialize(resource)
data/redlock.gemspec CHANGED
@@ -1,27 +1,28 @@
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
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
19
+ spec.require_paths = ['lib']
20
20
 
21
- spec.add_dependency 'redis', '>= 3.0.0', '< 5.0'
21
+ spec.add_dependency 'redis-client', '~> 0.14.1'
22
22
 
23
- spec.add_development_dependency "coveralls", "~> 0.8"
23
+ spec.add_development_dependency 'connection_pool', '~> 2.2'
24
+ spec.add_development_dependency 'coveralls', '~> 0.8'
25
+ spec.add_development_dependency 'json', '>= 2.3.0', '~> 2.3.1'
24
26
  spec.add_development_dependency 'rake', '>= 11.1.2', '~> 13.0'
25
27
  spec.add_development_dependency 'rspec', '~> 3', '>= 3.0.0'
26
- spec.add_development_dependency 'connection_pool', '~> 2.2'
27
28
  end
data/spec/client_spec.rb CHANGED
@@ -1,46 +1,113 @@
1
1
  require 'spec_helper'
2
2
  require 'securerandom'
3
- require 'redis'
4
3
  require 'connection_pool'
5
4
 
6
5
  RSpec.describe Redlock::Client do
7
6
  # It is recommended to have at least 3 servers in production
8
7
  let(:lock_manager_opts) { { retry_count: 3 } }
9
- let(:lock_manager) { Redlock::Client.new(Redlock::Client::DEFAULT_REDIS_URLS, lock_manager_opts) }
10
- let(:redis_client) { Redis.new }
8
+ let(:redis_urls_or_clients) {
9
+ urls = Redlock::Client::DEFAULT_REDIS_URLS
10
+ if rand(0..1).zero?
11
+ RSpec.configuration.reporter.message "variant: client urls"
12
+ urls
13
+ else
14
+ RSpec.configuration.reporter.message "variant: client objects"
15
+ urls.map {|url|
16
+ ConnectionPool.new { RedisClient.new(url: url) }
17
+ }
18
+ end
19
+ }
20
+ let(:lock_manager) {
21
+ Redlock::Client.new(redis_urls_or_clients, lock_manager_opts)
22
+ }
23
+ let(:redis_client) { RedisClient.new(url: "redis://#{redis1_host}:#{redis1_port}") }
11
24
  let(:resource_key) { SecureRandom.hex(3) }
12
25
  let(:ttl) { 1000 }
13
26
  let(:redis1_host) { ENV["REDIS1_HOST"] || "localhost" }
14
27
  let(:redis1_port) { ENV["REDIS1_PORT"] || "6379" }
15
28
  let(:redis2_host) { ENV["REDIS2_HOST"] || "127.0.0.1" }
16
29
  let(:redis2_port) { ENV["REDIS2_PORT"] || "6379" }
30
+ let(:redis3_host) { ENV["REDIS3_HOST"] || "127.0.0.1" }
31
+ let(:redis3_port) { ENV["REDIS3_PORT"] || "6379" }
32
+ let(:unreachable_redis) {
33
+ redis = RedisClient.new(url: 'redis://localhost:46864')
34
+ def redis.with
35
+ yield self
36
+ end
37
+ redis
38
+ }
17
39
 
18
40
  describe 'initialize' do
19
41
  it 'accepts both redis URLs and Redis objects' do
20
- servers = [ "redis://#{redis1_host}:#{redis1_port}", Redis.new(url: "redis://#{redis2_host}:#{redis2_port}") ]
42
+ servers = [ "redis://#{redis1_host}:#{redis1_port}", RedisClient.new(url: "redis://#{redis2_host}:#{redis2_port}") ]
21
43
  redlock = Redlock::Client.new(servers)
22
44
 
23
45
  redlock_servers = redlock.instance_variable_get(:@servers).map do |s|
24
- s.instance_variable_get(:@redis).connection[:host]
46
+ s.instance_variable_get(:@redis).config.host
25
47
  end
26
48
 
27
49
  expect(redlock_servers).to match_array([redis1_host, redis2_host])
28
50
  end
29
51
 
30
52
  it 'accepts ConnectionPool objects' do
31
- pool = ConnectionPool.new { Redis.new(url: "redis://#{redis1_host}:#{redis1_port}") }
32
- redlock = Redlock::Client.new([pool])
53
+ pool = ConnectionPool.new { RedisClient.new(url: "redis://#{redis1_host}:#{redis1_port}") }
54
+ _redlock = Redlock::Client.new([pool])
33
55
 
34
56
  lock_info = lock_manager.lock(resource_key, ttl)
57
+ expect(lock_info).to be_a(Hash)
35
58
  expect(resource_key).to_not be_lockable(lock_manager, ttl)
36
59
  lock_manager.unlock(lock_info)
37
60
  end
61
+
62
+ it 'does not load scripts' do
63
+ redis_client.call('SCRIPT', 'FLUSH')
64
+
65
+ pool = ConnectionPool.new { RedisClient.new(url: "redis://#{redis1_host}:#{redis1_port}") }
66
+ _redlock = Redlock::Client.new([pool])
67
+
68
+ raw_info = redis_client.call('INFO')
69
+ number_of_cached_scripts = raw_info[/number_of_cached_scripts\:\d+/].split(':').last
70
+
71
+ expect(number_of_cached_scripts).to eq("0")
72
+ end
38
73
  end
39
74
 
40
75
  describe 'lock' do
41
76
  context 'when lock is available' do
42
77
  after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
43
78
 
79
+ context 'when redis connection error occurs' do
80
+ let(:servers_with_quorum) {
81
+ [
82
+ "redis://#{redis1_host}:#{redis1_port}",
83
+ "redis://#{redis2_host}:#{redis2_port}",
84
+ unreachable_redis
85
+ ]
86
+ }
87
+
88
+ let(:servers_without_quorum) {
89
+ [
90
+ "redis://#{redis1_host}:#{redis1_port}",
91
+ unreachable_redis,
92
+ unreachable_redis
93
+ ]
94
+ }
95
+
96
+ it 'locks if majority of redis instances are available' do
97
+ redlock = Redlock::Client.new(servers_with_quorum)
98
+
99
+ expect(redlock.lock(resource_key, ttl)).to be_truthy
100
+ end
101
+
102
+ it 'fails to acquire a lock if majority of Redis instances are not available' do
103
+ redlock = Redlock::Client.new(servers_without_quorum)
104
+
105
+ expect {
106
+ redlock.lock(resource_key, ttl)
107
+ }.to raise_error(Redlock::LockAcquisitionError)
108
+ end
109
+ end
110
+
44
111
  it 'locks' do
45
112
  @lock_info = lock_manager.lock(resource_key, ttl)
46
113
 
@@ -56,7 +123,7 @@ RSpec.describe Redlock::Client do
56
123
  it 'interprets lock time as milliseconds' do
57
124
  ttl = 20000
58
125
  @lock_info = lock_manager.lock(resource_key, ttl)
59
- expect(redis_client.pttl(resource_key)).to be_within(200).of(ttl)
126
+ expect(redis_client.call('PTTL', resource_key)).to be_within(200).of(ttl)
60
127
  end
61
128
 
62
129
  it 'can extend its own lock' do
@@ -88,7 +155,7 @@ RSpec.describe Redlock::Client do
88
155
 
89
156
  lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_only_if_locked: true)
90
157
  expect(lock_info).not_to be_nil
91
- expect(redis_client.pttl(resource_key)).to be_within(200).of(ttl)
158
+ expect(redis_client.call('PTTL', resource_key)).to be_within(200).of(ttl)
92
159
  end
93
160
 
94
161
  context 'when extend_only_if_locked flag is not given' do
@@ -177,7 +244,7 @@ RSpec.describe Redlock::Client do
177
244
  2000
178
245
  end
179
246
 
180
- lock_manager = Redlock::Client.new(Redlock::Client::DEFAULT_REDIS_URLS, retry_count: 1, retry_delay: retry_delay)
247
+ lock_manager = Redlock::Client.new(redis_urls_or_clients, retry_count: 1, retry_delay: retry_delay)
181
248
  another_lock_info = lock_manager.lock(resource_key, ttl)
182
249
 
183
250
  expect(lock_manager).to receive(:sleep) do |sleep|
@@ -186,17 +253,55 @@ RSpec.describe Redlock::Client do
186
253
  lock_manager.lock(resource_key, ttl)
187
254
  lock_manager.unlock(another_lock_info)
188
255
  end
256
+
257
+ context 'when retry_count is given' do
258
+ it 'prioritizes the retry_count in option and tries up to \'retry_count\' + 1 times' do
259
+ retry_count = 1
260
+ expect(retry_count).not_to eq(lock_manager_opts[:retry_count])
261
+ expect(lock_manager).to receive(:lock_instances).exactly(retry_count + 1).times.and_return(false)
262
+ lock_manager.lock(resource_key, ttl, retry_count: retry_count)
263
+ end
264
+ end
265
+
266
+ context 'when retry_delay is given' do
267
+ it 'prioritizes the retry_delay in option and sleeps at least the specified retry_delay in milliseconds' do
268
+ retry_delay = 300
269
+ expect(retry_delay > described_class::DEFAULT_RETRY_DELAY).to eq(true)
270
+ expected_minimum = retry_delay
271
+
272
+ expect(lock_manager).to receive(:sleep) do |sleep|
273
+ expect(sleep).to satisfy { |value| value >= expected_minimum / 1000.to_f }
274
+ end.at_least(:once)
275
+ lock_manager.lock(resource_key, ttl, retry_delay: retry_delay)
276
+ end
277
+ end
278
+
279
+ context 'when retry_jitter is given' do
280
+ it 'prioritizes the retry_jitter in option and sleeps a maximum of retry_delay + retry_jitter in milliseconds' do
281
+ retry_jitter = 60
282
+ expect(retry_jitter > described_class::DEFAULT_RETRY_JITTER).to eq(true)
283
+
284
+ expected_maximum = described_class::DEFAULT_RETRY_DELAY + retry_jitter
285
+ expect(lock_manager).to receive(:sleep) do |sleep|
286
+ expect(sleep).to satisfy { |value| value < expected_maximum / 1000.to_f }
287
+ end.at_least(:once)
288
+ lock_manager.lock(resource_key, ttl, retry_jitter: retry_jitter)
289
+ end
290
+ end
189
291
  end
190
292
 
191
293
  context 'when a server goes away' do
192
- it 'does not raise an error on connection issues' do
193
- # We re-route the lock manager to a (hopefully) non-existent Redis URL.
294
+ it 'raises an error on connection issues' do
295
+ # Set lock manager to a (hopefully) non-existent Redis URL to test error
194
296
  redis_instance = lock_manager.instance_variable_get(:@servers).first
195
297
  redis_instance.instance_variable_set(:@redis, unreachable_redis)
196
298
 
197
299
  expect {
198
- expect(lock_manager.lock(resource_key, ttl)).to be_falsey
199
- }.to_not raise_error
300
+ lock_manager.lock(resource_key, ttl)
301
+ }.to raise_error(Redlock::LockAcquisitionError) do |e|
302
+ expect(e.errors[0]).to be_a(RedisClient::CannotConnectError)
303
+ expect(e.errors.count).to eq 1
304
+ end
200
305
  end
201
306
  end
202
307
 
@@ -206,27 +311,26 @@ RSpec.describe Redlock::Client do
206
311
  redis_instance = lock_manager.instance_variable_get(:@servers).first
207
312
  old_redis = redis_instance.instance_variable_get(:@redis)
208
313
  redis_instance.instance_variable_set(:@redis, unreachable_redis)
209
- expect(lock_manager.lock(resource_key, ttl)).to be_falsey
314
+ expect {
315
+ lock_manager.lock(resource_key, ttl)
316
+ }.to raise_error(Redlock::LockAcquisitionError) do |e|
317
+ expect(e.errors[0]).to be_a(RedisClient::CannotConnectError)
318
+ expect(e.errors.count).to eq 1
319
+ end
210
320
  redis_instance.instance_variable_set(:@redis, old_redis)
211
321
  expect(lock_manager.lock(resource_key, ttl)).to be_truthy
212
322
  end
213
323
  end
214
324
 
215
- def unreachable_redis
216
- redis = Redis.new(url: 'redis://localhost:46864')
217
- def redis.with
218
- yield self
219
- end
220
- redis
221
- end
222
-
223
325
  context 'when script cache has been flushed' do
224
326
  before(:each) do
225
327
  @manipulated_instance = lock_manager.instance_variable_get(:@servers).first
226
- @manipulated_instance.instance_variable_get(:@redis).script(:flush)
328
+ @manipulated_instance.instance_variable_get(:@redis).with { |conn|
329
+ conn.call('SCRIPT', 'FLUSH')
330
+ }
227
331
  end
228
332
 
229
- it 'does not raise a Redis::CommandError: NOSCRIPT error' do
333
+ it 'does not raise a RedisClient::CommandError: NOSCRIPT error' do
230
334
  expect {
231
335
  lock_manager.lock(resource_key, ttl)
232
336
  }.to_not raise_error
@@ -242,11 +346,15 @@ RSpec.describe Redlock::Client do
242
346
  # This time we do not pass it through to Redis, in order to simulate a passing
243
347
  # call to LOAD SCRIPT followed by another NOSCRIPT error. Imagine someone
244
348
  # repeatedly calling SCRIPT FLUSH on our Redis instance.
245
- expect(@manipulated_instance).to receive(:load_scripts)
349
+ expect(@manipulated_instance).to receive(:load_scripts).exactly(8).times
246
350
 
247
351
  expect {
248
352
  lock_manager.lock(resource_key, ttl)
249
- }.to raise_error(/NOSCRIPT/)
353
+ }.to raise_error(Redlock::LockAcquisitionError) do |e|
354
+ expect(e.errors[0]).to be_a(RedisClient::CommandError)
355
+ expect(e.errors[0].message).to match(/NOSCRIPT/)
356
+ expect(e.errors.count).to eq 1
357
+ end
250
358
  end
251
359
  end
252
360
 
@@ -367,6 +475,154 @@ RSpec.describe Redlock::Client do
367
475
  end
368
476
  end
369
477
 
478
+ describe 'get_remaining_ttl_for_resource' do
479
+ context 'when lock is valid' do
480
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
481
+
482
+ it 'gets the remaining ttl of a lock' do
483
+ ttl = 20_000
484
+ @lock_info = lock_manager.lock(resource_key, ttl)
485
+ remaining_ttl = lock_manager.get_remaining_ttl_for_resource(resource_key)
486
+ expect(remaining_ttl).to be_within(300).of(ttl)
487
+ end
488
+
489
+ context 'when servers respond with varying ttls' do
490
+ let (:servers) {
491
+ [
492
+ "redis://#{redis1_host}:#{redis1_port}",
493
+ "redis://#{redis2_host}:#{redis2_port}",
494
+ "redis://#{redis3_host}:#{redis3_port}"
495
+ ]
496
+ }
497
+ let (:redlock) { Redlock::Client.new(servers) }
498
+ after(:each) { redlock.unlock(@lock_info) if @lock_info }
499
+
500
+ it 'returns the minimum ttl value' do
501
+ ttl = 20_000
502
+ @lock_info = redlock.lock(resource_key, ttl)
503
+
504
+ # Mock redis server responses to return different ttls
505
+ returned_ttls = [20_000, 15_000, 10_000]
506
+ redlock.instance_variable_get(:@servers).each_with_index do |server, index|
507
+ allow(server).to(receive(:get_remaining_ttl))
508
+ .with(resource_key)
509
+ .and_return([@lock_info[:value], returned_ttls[index]])
510
+ end
511
+
512
+ remaining_ttl = redlock.get_remaining_ttl_for_lock(@lock_info)
513
+
514
+ # Assert that the TTL is closest to the closest to the correct value
515
+ expect(remaining_ttl).to be_within(300).of(returned_ttls[1])
516
+ end
517
+ end
518
+ end
519
+
520
+ context 'when lock is not valid' do
521
+ it 'returns nil' do
522
+ lock_info = lock_manager.lock(resource_key, ttl)
523
+ lock_manager.unlock(lock_info)
524
+ remaining_ttl = lock_manager.get_remaining_ttl_for_resource(resource_key)
525
+ expect(remaining_ttl).to be_nil
526
+ end
527
+ end
528
+
529
+ context 'when server goes away' do
530
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
531
+
532
+ it 'does not raise an error on connection issues' do
533
+ @lock_info = lock_manager.lock(resource_key, ttl)
534
+
535
+ # Replace redis with unreachable instance
536
+ redis_instance = lock_manager.instance_variable_get(:@servers).first
537
+ _old_redis = redis_instance.instance_variable_get(:@redis)
538
+ redis_instance.instance_variable_set(:@redis, unreachable_redis)
539
+
540
+ expect {
541
+ remaining_ttl = lock_manager.get_remaining_ttl_for_resource(resource_key)
542
+ expect(remaining_ttl).to be_nil
543
+ }.to_not raise_error
544
+ end
545
+ end
546
+
547
+ context 'when a server comes back' do
548
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
549
+
550
+ it 'recovers from connection issues' do
551
+ @lock_info = lock_manager.lock(resource_key, ttl)
552
+
553
+ # Replace redis with unreachable instance
554
+ redis_instance = lock_manager.instance_variable_get(:@servers).first
555
+ old_redis = redis_instance.instance_variable_get(:@redis)
556
+ redis_instance.instance_variable_set(:@redis, unreachable_redis)
557
+
558
+ expect(lock_manager.get_remaining_ttl_for_resource(resource_key)).to be_nil
559
+
560
+ # Restore redis
561
+ redis_instance.instance_variable_set(:@redis, old_redis)
562
+ expect(lock_manager.get_remaining_ttl_for_resource(resource_key)).to be_truthy
563
+ end
564
+ end
565
+ end
566
+
567
+ describe 'get_remaining_ttl_for_lock' do
568
+ context 'when lock is valid' do
569
+ it 'gets the remaining ttl of a lock' do
570
+ ttl = 20_000
571
+ lock_info = lock_manager.lock(resource_key, ttl)
572
+ remaining_ttl = lock_manager.get_remaining_ttl_for_lock(lock_info)
573
+ expect(remaining_ttl).to be_within(300).of(ttl)
574
+ lock_manager.unlock(lock_info)
575
+ end
576
+ end
577
+
578
+ context 'when lock is not valid' do
579
+ it 'returns nil' do
580
+ lock_info = lock_manager.lock(resource_key, ttl)
581
+ lock_manager.unlock(lock_info)
582
+ remaining_ttl = lock_manager.get_remaining_ttl_for_lock(lock_info)
583
+ expect(remaining_ttl).to be_nil
584
+ end
585
+ end
586
+ end
587
+
588
+ describe 'locked?' do
589
+ context 'when lock is available' do
590
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
591
+
592
+ it 'returns true' do
593
+ @lock_info = lock_manager.lock(resource_key, ttl)
594
+ expect(lock_manager).to be_locked(resource_key)
595
+ end
596
+ end
597
+
598
+ context 'when lock is not available' do
599
+ it 'returns false' do
600
+ lock_info = lock_manager.lock(resource_key, ttl)
601
+ lock_manager.unlock(lock_info)
602
+ expect(lock_manager).not_to be_locked(resource_key)
603
+ end
604
+ end
605
+ end
606
+
607
+ describe 'valid_lock?' do
608
+ context 'when lock is available' do
609
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
610
+
611
+ it 'returns true' do
612
+ @lock_info = lock_manager.lock(resource_key, ttl)
613
+ expect(lock_manager).to be_valid_lock(@lock_info)
614
+ end
615
+ end
616
+
617
+ context 'when lock is not available' do
618
+ it 'returns false' do
619
+ lock_info = lock_manager.lock(resource_key, ttl)
620
+ lock_manager.unlock(lock_info)
621
+ expect(lock_manager).not_to be_valid_lock(lock_info)
622
+ end
623
+ end
624
+ end
625
+
370
626
  describe '#default_time_source' do
371
627
  context 'when CLOCK_MONOTONIC is available (MRI, JRuby)' do
372
628
  it 'returns a callable using Process.clock_gettime()' do
data/spec/testing_spec.rb CHANGED
@@ -11,7 +11,7 @@ RSpec.describe Redlock::Client do
11
11
  describe '(testing mode)' do
12
12
  describe 'try_lock_instances' do
13
13
  context 'when testing with bypass mode' do
14
- before { lock_manager.testing_mode = :bypass }
14
+ before { Redlock::Client.testing_mode = :bypass }
15
15
 
16
16
  it 'bypasses the redis servers' do
17
17
  expect(lock_manager).to_not receive(:try_lock_instances_without_testing)
@@ -22,7 +22,7 @@ RSpec.describe Redlock::Client do
22
22
  end
23
23
 
24
24
  context 'when testing with fail mode' do
25
- before { lock_manager.testing_mode = :fail }
25
+ before { Redlock::Client.testing_mode = :fail }
26
26
 
27
27
  it 'fails' do
28
28
  expect(lock_manager).to_not receive(:try_lock_instances_without_testing)
@@ -33,7 +33,7 @@ RSpec.describe Redlock::Client do
33
33
  end
34
34
 
35
35
  context 'when testing is disabled' do
36
- before { lock_manager.testing_mode = nil }
36
+ before { Redlock::Client.testing_mode = nil }
37
37
 
38
38
  it 'works as usual' do
39
39
  expect(lock_manager).to receive(:try_lock_instances_without_testing)
metadata CHANGED
@@ -1,35 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redlock
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leandro Moreira
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-02-29 00:00:00.000000000 Z
11
+ date: 2023-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: redis
14
+ name: redis-client
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: 3.0.0
20
- - - "<"
17
+ - - "~>"
21
18
  - !ruby/object:Gem::Version
22
- version: '5.0'
19
+ version: 0.14.1
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
- - - ">="
24
+ - - "~>"
28
25
  - !ruby/object:Gem::Version
29
- version: 3.0.0
30
- - - "<"
26
+ version: 0.14.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: connection_pool
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
31
32
  - !ruby/object:Gem::Version
32
- version: '5.0'
33
+ version: '2.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.2'
33
41
  - !ruby/object:Gem::Dependency
34
42
  name: coveralls
35
43
  requirement: !ruby/object:Gem::Requirement
@@ -44,6 +52,26 @@ dependencies:
44
52
  - - "~>"
45
53
  - !ruby/object:Gem::Version
46
54
  version: '0.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: json
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.3.0
62
+ - - "~>"
63
+ - !ruby/object:Gem::Version
64
+ version: 2.3.1
65
+ type: :development
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 2.3.0
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: 2.3.1
47
75
  - !ruby/object:Gem::Dependency
48
76
  name: rake
49
77
  requirement: !ruby/object:Gem::Requirement
@@ -84,20 +112,6 @@ dependencies:
84
112
  - - ">="
85
113
  - !ruby/object:Gem::Version
86
114
  version: 3.0.0
87
- - !ruby/object:Gem::Dependency
88
- name: connection_pool
89
- requirement: !ruby/object:Gem::Requirement
90
- requirements:
91
- - - "~>"
92
- - !ruby/object:Gem::Version
93
- version: '2.2'
94
- type: :development
95
- prerelease: false
96
- version_requirements: !ruby/object:Gem::Requirement
97
- requirements:
98
- - - "~>"
99
- - !ruby/object:Gem::Version
100
- version: '2.2'
101
115
  description: Distributed lock using Redis written in Ruby. Highly inspired by https://github.com/antirez/redlock-rb.
102
116
  email:
103
117
  - leandro.ribeiro.moreira@gmail.com
@@ -105,12 +119,12 @@ executables: []
105
119
  extensions: []
106
120
  extra_rdoc_files: []
107
121
  files:
122
+ - ".github/workflows/ci.yml"
108
123
  - ".gitignore"
109
124
  - ".rspec"
110
- - ".travis.yml"
125
+ - CHANGELOG.md
111
126
  - CONTRIBUTORS
112
127
  - Gemfile
113
- - Gemfile.lock
114
128
  - LICENSE
115
129
  - Makefile
116
130
  - README.md
@@ -119,6 +133,7 @@ files:
119
133
  - docker-compose.yml
120
134
  - lib/redlock.rb
121
135
  - lib/redlock/client.rb
136
+ - lib/redlock/scripts.rb
122
137
  - lib/redlock/testing.rb
123
138
  - lib/redlock/version.rb
124
139
  - redlock.gemspec
@@ -144,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
144
159
  - !ruby/object:Gem::Version
145
160
  version: '0'
146
161
  requirements: []
147
- rubygems_version: 3.1.2
162
+ rubygems_version: 3.3.7
148
163
  signing_key:
149
164
  specification_version: 4
150
165
  summary: Distributed lock using Redis written in Ruby.
data/.travis.yml DELETED
@@ -1,10 +0,0 @@
1
- language: ruby
2
- services:
3
- - redis-server
4
- rvm:
5
- - 2.2.2
6
- - 2.5.6
7
- - 2.6.4
8
- script: bundle exec rspec spec
9
- sudo: false
10
- cache: bundler
data/Gemfile.lock DELETED
@@ -1,56 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- redlock (1.2.0)
5
- redis (>= 3.0.0, < 5.0)
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- connection_pool (2.2.2)
11
- coveralls (0.8.22)
12
- json (>= 1.8, < 3)
13
- simplecov (~> 0.16.1)
14
- term-ansicolor (~> 1.3)
15
- thor (~> 0.19.4)
16
- tins (~> 1.6)
17
- diff-lcs (1.3)
18
- docile (1.3.1)
19
- json (2.1.0)
20
- rake (13.0.1)
21
- redis (4.1.1)
22
- rspec (3.5.0)
23
- rspec-core (~> 3.5.0)
24
- rspec-expectations (~> 3.5.0)
25
- rspec-mocks (~> 3.5.0)
26
- rspec-core (3.5.4)
27
- rspec-support (~> 3.5.0)
28
- rspec-expectations (3.5.0)
29
- diff-lcs (>= 1.2.0, < 2.0)
30
- rspec-support (~> 3.5.0)
31
- rspec-mocks (3.5.0)
32
- diff-lcs (>= 1.2.0, < 2.0)
33
- rspec-support (~> 3.5.0)
34
- rspec-support (3.5.0)
35
- simplecov (0.16.1)
36
- docile (~> 1.1)
37
- json (>= 1.8, < 3)
38
- simplecov-html (~> 0.10.0)
39
- simplecov-html (0.10.2)
40
- term-ansicolor (1.6.0)
41
- tins (~> 1.0)
42
- thor (0.19.4)
43
- tins (1.16.3)
44
-
45
- PLATFORMS
46
- ruby
47
-
48
- DEPENDENCIES
49
- connection_pool (~> 2.2)
50
- coveralls (~> 0.8)
51
- rake (~> 13.0, >= 11.1.2)
52
- redlock!
53
- rspec (~> 3, >= 3.0.0)
54
-
55
- BUNDLED WITH
56
- 1.17.2