redlock 1.3.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +3 -3
- data/.gitignore +1 -0
- data/CHANGELOG.md +40 -0
- data/Makefile +3 -6
- data/README.md +23 -7
- data/lib/redlock/client.rb +91 -18
- data/lib/redlock/testing.rb +1 -1
- data/lib/redlock/version.rb +1 -1
- data/redlock.gemspec +12 -11
- data/spec/client_spec.rb +201 -26
- data/spec/testing_spec.rb +3 -3
- metadata +13 -22
- data/Gemfile.lock +0 -62
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 229c8a5a2a6c074810df969cd50e20bac41f8bf47e3d5714fe21975888702fb0
|
|
4
|
+
data.tar.gz: d9e1079662c2ef7ddd23d3e03696c48d6b9ae7fe37c0b65760c3a90be07bc239
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6a3436508b649c79682b310ff57094aa06a532ebde60b193d720dcc88116166a9bacc5e60701515528ef9cf8b7946e246c2bc8341a75eb948cc23ecd1a7be8ff
|
|
7
|
+
data.tar.gz: d2fc1c15c559a684f87b08ccd1e7e3adc9988323111803c1ca66f2df5381890c09802218dc86477e1de2099bd7aafca1e5e368c9f15fd85ea9f5d433846c0928
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -2,9 +2,9 @@ name: Ruby CI
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
|
-
branches: [
|
|
5
|
+
branches: [ main ]
|
|
6
6
|
pull_request:
|
|
7
|
-
branches: [
|
|
7
|
+
branches: [ main ]
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
10
10
|
test:
|
|
@@ -13,7 +13,7 @@ jobs:
|
|
|
13
13
|
|
|
14
14
|
strategy:
|
|
15
15
|
matrix:
|
|
16
|
-
ruby-version: [3.1, "3.0", "2.7", "
|
|
16
|
+
ruby-version: [3.2, 3.1, "3.0", "2.7", "ruby-head"]
|
|
17
17
|
|
|
18
18
|
steps:
|
|
19
19
|
- uses: actions/checkout@v2
|
data/.gitignore
CHANGED
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/Makefile
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
default: test
|
|
2
2
|
test:
|
|
3
|
-
docker
|
|
3
|
+
docker compose run --rm test
|
|
4
4
|
|
|
5
5
|
build:
|
|
6
|
-
docker
|
|
6
|
+
docker compose run --rm test gem build redlock.gemspec
|
|
7
7
|
|
|
8
8
|
publish:
|
|
9
|
-
docker
|
|
10
|
-
|
|
11
|
-
updateLock:
|
|
12
|
-
docker-compose run --rm test bundle lock --update
|
|
9
|
+
docker compose run --rm test gem push `ls -lt *gem | head -n 1 | awk '{ print $$9 }'`
|
data/README.md
CHANGED
|
@@ -15,7 +15,8 @@ This is an implementation of a proposed [distributed lock algorithm with Redis](
|
|
|
15
15
|
|
|
16
16
|
## Compatibility
|
|
17
17
|
|
|
18
|
-
|
|
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.
|
|
19
20
|
|
|
20
21
|
## Installation
|
|
21
22
|
|
|
@@ -151,7 +152,7 @@ lock_manager.get_remaining_ttl_for_lock(lock_info)
|
|
|
151
152
|
|
|
152
153
|
lock_manager.unlock(lock_info)
|
|
153
154
|
lock_manager.get_remaining_ttl_for_lock(lock_info)
|
|
154
|
-
#=> nil
|
|
155
|
+
#=> nil
|
|
155
156
|
```
|
|
156
157
|
|
|
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:
|
|
@@ -164,25 +165,40 @@ lock_info = lock_manager.lock(resource, 2000)
|
|
|
164
165
|
lock_manager.locked?(resource)
|
|
165
166
|
#=> true
|
|
166
167
|
lock_manager.get_remaining_ttl_for_resource(resource)
|
|
167
|
-
#=> 1975
|
|
168
|
+
#=> 1975
|
|
168
169
|
|
|
169
170
|
# Sometime later
|
|
170
171
|
lock_manager.locked?(resource)
|
|
171
172
|
#=> false
|
|
172
173
|
lock_manager.get_remaining_ttl_for_resource(resource)
|
|
173
|
-
#=> nil
|
|
174
|
+
#=> nil
|
|
174
175
|
```
|
|
175
176
|
|
|
176
177
|
## Redis client configuration
|
|
177
178
|
|
|
178
|
-
`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.
|
|
179
|
+
`Redlock::Client` expects URLs, or configurations or Redis objects on initialization. Redis objects should be used for configuring the connection in more detail, i.e. setting username and password.
|
|
179
180
|
|
|
180
181
|
```ruby
|
|
181
|
-
servers = [ 'redis://localhost:6379',
|
|
182
|
+
servers = [ 'redis://localhost:6379', RedisClient.new(:url => 'redis://someotherhost:6379') ]
|
|
182
183
|
redlock = Redlock::Client.new(servers)
|
|
183
184
|
```
|
|
184
185
|
|
|
185
|
-
|
|
186
|
+
To utilize `Redlock::Client` with sentinels you can pass an instance of `RedisClient` or just a configuration hash as part of the servers array during initialization.
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
config = {
|
|
190
|
+
name: "mymaster",
|
|
191
|
+
sentinels: [
|
|
192
|
+
{ host: "127.0.0.1", port: 26380 },
|
|
193
|
+
{ host: "127.0.0.1", port: 26381 },
|
|
194
|
+
],
|
|
195
|
+
role: :master
|
|
196
|
+
}
|
|
197
|
+
client = RedisClient.sentinel(**config).new_client
|
|
198
|
+
servers = [ config, client ]
|
|
199
|
+
redlock = Redlock::Client.new(servers)
|
|
200
|
+
```
|
|
201
|
+
Redlock supports the same configuration hash as `RedisClient`.
|
|
186
202
|
|
|
187
203
|
## Redlock configuration
|
|
188
204
|
|
data/lib/redlock/client.rb
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'monitor'
|
|
2
|
+
require 'redis-client'
|
|
2
3
|
require 'securerandom'
|
|
3
4
|
|
|
4
5
|
module Redlock
|
|
5
6
|
include Scripts
|
|
6
7
|
|
|
8
|
+
class LockAcquisitionError < LockError
|
|
9
|
+
attr_reader :errors
|
|
10
|
+
|
|
11
|
+
def initialize(message, errors)
|
|
12
|
+
super(message)
|
|
13
|
+
@errors = errors
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
7
17
|
class Client
|
|
8
18
|
DEFAULT_REDIS_HOST = ENV["DEFAULT_REDIS_HOST"] || "localhost"
|
|
9
19
|
DEFAULT_REDIS_PORT = ENV["DEFAULT_REDIS_PORT"] || "6379"
|
|
@@ -147,34 +157,62 @@ module Redlock
|
|
|
147
157
|
private
|
|
148
158
|
|
|
149
159
|
class RedisInstance
|
|
150
|
-
module ConnectionPoolLike
|
|
151
|
-
def with
|
|
152
|
-
yield self
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
|
|
156
160
|
def initialize(connection)
|
|
157
|
-
|
|
161
|
+
@monitor = Monitor.new
|
|
162
|
+
|
|
163
|
+
if connection.respond_to?(:call)
|
|
158
164
|
@redis = connection
|
|
159
165
|
else
|
|
160
166
|
if connection.respond_to?(:client)
|
|
161
167
|
@redis = connection
|
|
168
|
+
elsif connection.respond_to?(:key?)
|
|
169
|
+
@redis = initialize_client(connection)
|
|
162
170
|
else
|
|
163
|
-
@redis =
|
|
171
|
+
@redis = connection
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def initialize_client(options)
|
|
177
|
+
if options.key?(:sentinels)
|
|
178
|
+
if url = options.delete(:url)
|
|
179
|
+
uri = URI.parse(url)
|
|
180
|
+
if !options.key?(:name) && uri.host
|
|
181
|
+
options[:name] = uri.host
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
if !options.key?(:password) && uri.password && !uri.password.empty?
|
|
185
|
+
options[:password] = uri.password
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
if !options.key?(:username) && uri.user && !uri.user.empty?
|
|
189
|
+
options[:username] = uri.user
|
|
190
|
+
end
|
|
164
191
|
end
|
|
165
|
-
|
|
192
|
+
|
|
193
|
+
RedisClient.sentinel(**options).new_client
|
|
194
|
+
else
|
|
195
|
+
RedisClient.config(**options).new_client
|
|
166
196
|
end
|
|
167
197
|
end
|
|
168
198
|
|
|
199
|
+
def synchronize
|
|
200
|
+
@monitor.synchronize { @redis.then { |connection| yield(connection) } }
|
|
201
|
+
end
|
|
202
|
+
|
|
169
203
|
def lock(resource, val, ttl, allow_new_lock)
|
|
170
204
|
recover_from_script_flush do
|
|
171
|
-
|
|
205
|
+
synchronize { |conn|
|
|
206
|
+
conn.call('EVALSHA', Scripts::LOCK_SCRIPT_SHA, 1, resource, val, ttl, allow_new_lock)
|
|
207
|
+
}
|
|
172
208
|
end
|
|
173
209
|
end
|
|
174
210
|
|
|
175
211
|
def unlock(resource, val)
|
|
176
212
|
recover_from_script_flush do
|
|
177
|
-
|
|
213
|
+
synchronize { |conn|
|
|
214
|
+
conn.call('EVALSHA', Scripts::UNLOCK_SCRIPT_SHA, 1, resource, val)
|
|
215
|
+
}
|
|
178
216
|
end
|
|
179
217
|
rescue
|
|
180
218
|
# Nothing to do, unlocking is just a best-effort attempt.
|
|
@@ -182,9 +220,11 @@ module Redlock
|
|
|
182
220
|
|
|
183
221
|
def get_remaining_ttl(resource)
|
|
184
222
|
recover_from_script_flush do
|
|
185
|
-
|
|
223
|
+
synchronize { |conn|
|
|
224
|
+
conn.call('EVALSHA', Scripts::PTTL_SCRIPT_SHA, 1, resource)
|
|
225
|
+
}
|
|
186
226
|
end
|
|
187
|
-
rescue
|
|
227
|
+
rescue RedisClient::ConnectionError
|
|
188
228
|
nil
|
|
189
229
|
end
|
|
190
230
|
|
|
@@ -197,8 +237,24 @@ module Redlock
|
|
|
197
237
|
Scripts::PTTL_SCRIPT
|
|
198
238
|
]
|
|
199
239
|
|
|
200
|
-
|
|
201
|
-
|
|
240
|
+
synchronize do |connnection|
|
|
241
|
+
scripts.each do |script|
|
|
242
|
+
connnection.call('SCRIPT', 'LOAD', script)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Exception classes that may be raised for NOSCRIPT errors.
|
|
248
|
+
# RedisClient::CommandError is raised when using redis-client directly.
|
|
249
|
+
# Redis::CommandError (or its subclass Redis::NoScriptError) is raised
|
|
250
|
+
# when using the redis gem wrapper.
|
|
251
|
+
# See: https://github.com/leandromoreira/redlock-rb/issues/124
|
|
252
|
+
# See: https://github.com/leandromoreira/redlock-rb/issues/148
|
|
253
|
+
def script_error_classes
|
|
254
|
+
@script_error_classes ||= begin
|
|
255
|
+
classes = [RedisClient::CommandError]
|
|
256
|
+
classes << Redis::CommandError if defined?(Redis::CommandError)
|
|
257
|
+
classes
|
|
202
258
|
end
|
|
203
259
|
end
|
|
204
260
|
|
|
@@ -206,7 +262,7 @@ module Redlock
|
|
|
206
262
|
retry_on_noscript = true
|
|
207
263
|
begin
|
|
208
264
|
yield
|
|
209
|
-
rescue
|
|
265
|
+
rescue *script_error_classes => e
|
|
210
266
|
# When somebody has flushed the Redis instance's script cache, we might
|
|
211
267
|
# want to reload our scripts. Only attempt this once, though, to avoid
|
|
212
268
|
# going into an infinite loop.
|
|
@@ -224,6 +280,7 @@ module Redlock
|
|
|
224
280
|
def try_lock_instances(resource, ttl, options)
|
|
225
281
|
retry_count = options[:retry_count] || @retry_count
|
|
226
282
|
tries = options[:extend] ? 1 : (retry_count + 1)
|
|
283
|
+
last_error = nil
|
|
227
284
|
|
|
228
285
|
tries.times do |attempt_number|
|
|
229
286
|
# Wait a random delay before retrying.
|
|
@@ -231,8 +288,12 @@ module Redlock
|
|
|
231
288
|
|
|
232
289
|
lock_info = lock_instances(resource, ttl, options)
|
|
233
290
|
return lock_info if lock_info
|
|
291
|
+
rescue => e
|
|
292
|
+
last_error = e
|
|
234
293
|
end
|
|
235
294
|
|
|
295
|
+
raise last_error if last_error
|
|
296
|
+
|
|
236
297
|
false
|
|
237
298
|
end
|
|
238
299
|
|
|
@@ -253,9 +314,15 @@ module Redlock
|
|
|
253
314
|
def lock_instances(resource, ttl, options)
|
|
254
315
|
value = (options[:extend] || { value: SecureRandom.uuid })[:value]
|
|
255
316
|
allow_new_lock = options[:extend_only_if_locked] ? 'no' : 'yes'
|
|
317
|
+
errors = []
|
|
256
318
|
|
|
257
319
|
locked, time_elapsed = timed do
|
|
258
|
-
@servers.
|
|
320
|
+
@servers.count do |s|
|
|
321
|
+
s.lock(resource, value, ttl, allow_new_lock)
|
|
322
|
+
rescue => e
|
|
323
|
+
errors << e
|
|
324
|
+
false
|
|
325
|
+
end
|
|
259
326
|
end
|
|
260
327
|
|
|
261
328
|
validity = ttl - time_elapsed - drift(ttl)
|
|
@@ -264,6 +331,12 @@ module Redlock
|
|
|
264
331
|
{ validity: validity, resource: resource, value: value }
|
|
265
332
|
else
|
|
266
333
|
@servers.each { |s| s.unlock(resource, value) }
|
|
334
|
+
|
|
335
|
+
if errors.size >= @quorum
|
|
336
|
+
err_msgs = errors.map { |e| "#{e.class}: #{e.message}" }.join("\n")
|
|
337
|
+
raise LockAcquisitionError.new("Too many Redis errors prevented lock acquisition:\n#{err_msgs}", errors)
|
|
338
|
+
end
|
|
339
|
+
|
|
267
340
|
false
|
|
268
341
|
end
|
|
269
342
|
end
|
data/lib/redlock/testing.rb
CHANGED
|
@@ -41,7 +41,7 @@ module Redlock
|
|
|
41
41
|
|
|
42
42
|
def load_scripts
|
|
43
43
|
load_scripts_without_testing unless Redlock::Client.testing_mode == :bypass
|
|
44
|
-
rescue
|
|
44
|
+
rescue RedisClient::CommandError
|
|
45
45
|
# FakeRedis doesn't have #script, but doesn't need it either.
|
|
46
46
|
raise unless defined?(::FakeRedis)
|
|
47
47
|
rescue NoMethodError
|
data/lib/redlock/version.rb
CHANGED
data/redlock.gemspec
CHANGED
|
@@ -5,24 +5,25 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
|
5
5
|
require 'redlock/version'
|
|
6
6
|
|
|
7
7
|
Gem::Specification.new do |spec|
|
|
8
|
-
spec.name
|
|
9
|
-
spec.version
|
|
10
|
-
spec.authors
|
|
11
|
-
spec.email
|
|
12
|
-
spec.summary
|
|
13
|
-
spec.description
|
|
14
|
-
spec.homepage
|
|
15
|
-
spec.license
|
|
8
|
+
spec.name = 'redlock'
|
|
9
|
+
spec.version = Redlock::VERSION
|
|
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'
|
|
15
|
+
spec.license = 'BSD-2-Clause'
|
|
16
|
+
spec.required_ruby_version = '>= 2.7.0'
|
|
16
17
|
|
|
17
18
|
spec.files = `git ls-files -z`.split("\x0")
|
|
18
19
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
19
20
|
spec.require_paths = ['lib']
|
|
20
21
|
|
|
21
|
-
spec.add_dependency 'redis', '>=
|
|
22
|
+
spec.add_dependency 'redis-client', '>= 0.14.1', '< 1.0.0'
|
|
22
23
|
|
|
23
24
|
spec.add_development_dependency 'connection_pool', '~> 2.2'
|
|
24
|
-
spec.add_development_dependency '
|
|
25
|
-
spec.add_development_dependency 'json', '>= 2.3.0'
|
|
25
|
+
spec.add_development_dependency 'coveralls_reborn', '~> 0.29'
|
|
26
|
+
spec.add_development_dependency 'json', '>= 2.3.0'
|
|
26
27
|
spec.add_development_dependency 'rake', '>= 11.1.2', '~> 13.0'
|
|
27
28
|
spec.add_development_dependency 'rspec', '~> 3', '>= 3.0.0'
|
|
28
29
|
end
|
data/spec/client_spec.rb
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
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(:
|
|
10
|
-
|
|
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" }
|
|
@@ -17,41 +30,51 @@ RSpec.describe Redlock::Client do
|
|
|
17
30
|
let(:redis3_host) { ENV["REDIS3_HOST"] || "127.0.0.1" }
|
|
18
31
|
let(:redis3_port) { ENV["REDIS3_PORT"] || "6379" }
|
|
19
32
|
let(:unreachable_redis) {
|
|
20
|
-
|
|
21
|
-
def redis.with
|
|
22
|
-
yield self
|
|
23
|
-
end
|
|
24
|
-
redis
|
|
33
|
+
RedisClient.new(url: 'redis://localhost:46864')
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
describe 'initialize' do
|
|
28
37
|
it 'accepts both redis URLs and Redis objects' do
|
|
29
|
-
servers = [ "redis://#{redis1_host}:#{redis1_port}",
|
|
38
|
+
servers = [ "redis://#{redis1_host}:#{redis1_port}", RedisClient.new(url: "redis://#{redis2_host}:#{redis2_port}") ]
|
|
30
39
|
redlock = Redlock::Client.new(servers)
|
|
31
40
|
|
|
32
41
|
redlock_servers = redlock.instance_variable_get(:@servers).map do |s|
|
|
33
|
-
s.instance_variable_get(:@redis).
|
|
42
|
+
s.instance_variable_get(:@redis).config.host
|
|
34
43
|
end
|
|
35
44
|
|
|
36
45
|
expect(redlock_servers).to match_array([redis1_host, redis2_host])
|
|
37
46
|
end
|
|
38
47
|
|
|
39
48
|
it 'accepts ConnectionPool objects' do
|
|
40
|
-
pool = ConnectionPool.new {
|
|
41
|
-
|
|
49
|
+
pool = ConnectionPool.new { RedisClient.new(url: "redis://#{redis1_host}:#{redis1_port}") }
|
|
50
|
+
_redlock = Redlock::Client.new([pool])
|
|
51
|
+
|
|
52
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
|
53
|
+
expect(lock_info).to be_a(Hash)
|
|
54
|
+
expect(resource_key).to_not be_lockable(lock_manager, ttl)
|
|
55
|
+
lock_manager.unlock(lock_info)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'accepts Configuration hashes' do
|
|
59
|
+
config = { url: "redis://#{redis1_host}:#{redis1_port}" }
|
|
60
|
+
_redlock = Redlock::Client.new([config])
|
|
42
61
|
|
|
43
62
|
lock_info = lock_manager.lock(resource_key, ttl)
|
|
63
|
+
expect(lock_info).to be_a(Hash)
|
|
44
64
|
expect(resource_key).to_not be_lockable(lock_manager, ttl)
|
|
45
65
|
lock_manager.unlock(lock_info)
|
|
46
66
|
end
|
|
47
67
|
|
|
48
68
|
it 'does not load scripts' do
|
|
49
|
-
redis_client.
|
|
69
|
+
redis_client.call('SCRIPT', 'FLUSH')
|
|
70
|
+
|
|
71
|
+
pool = ConnectionPool.new { RedisClient.new(url: "redis://#{redis1_host}:#{redis1_port}") }
|
|
72
|
+
_redlock = Redlock::Client.new([pool])
|
|
50
73
|
|
|
51
|
-
|
|
52
|
-
|
|
74
|
+
raw_info = redis_client.call('INFO')
|
|
75
|
+
number_of_cached_scripts = raw_info[/number_of_cached_scripts\:\d+/].split(':').last
|
|
53
76
|
|
|
54
|
-
expect(
|
|
77
|
+
expect(number_of_cached_scripts).to eq("0")
|
|
55
78
|
end
|
|
56
79
|
end
|
|
57
80
|
|
|
@@ -59,6 +82,44 @@ RSpec.describe Redlock::Client do
|
|
|
59
82
|
context 'when lock is available' do
|
|
60
83
|
after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
|
|
61
84
|
|
|
85
|
+
context 'when redis connection error occurs' do
|
|
86
|
+
let(:servers_with_quorum) {
|
|
87
|
+
[
|
|
88
|
+
"redis://#{redis1_host}:#{redis1_port}",
|
|
89
|
+
"redis://#{redis2_host}:#{redis2_port}",
|
|
90
|
+
unreachable_redis
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let(:servers_without_quorum) {
|
|
95
|
+
[
|
|
96
|
+
"redis://#{redis1_host}:#{redis1_port}",
|
|
97
|
+
unreachable_redis,
|
|
98
|
+
unreachable_redis
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
it 'locks if majority of redis instances are available' do
|
|
103
|
+
redlock = Redlock::Client.new(servers_with_quorum)
|
|
104
|
+
|
|
105
|
+
expect(redlock.lock(resource_key, ttl)).to be_truthy
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'fails to acquire a lock if majority of Redis instances are not available' do
|
|
109
|
+
redlock = Redlock::Client.new(servers_without_quorum)
|
|
110
|
+
|
|
111
|
+
expect {
|
|
112
|
+
redlock.lock(resource_key, ttl)
|
|
113
|
+
}.to raise_error do |error|
|
|
114
|
+
expect(error).to be_a(Redlock::LockAcquisitionError)
|
|
115
|
+
expect(error.message).to include('Too many Redis errors prevented lock acquisition')
|
|
116
|
+
expect(error.message).to include('RedisClient::CannotConnectError')
|
|
117
|
+
expect(error.message).to include('Connection refused')
|
|
118
|
+
expect(error.errors.size).to eq(2)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
62
123
|
it 'locks' do
|
|
63
124
|
@lock_info = lock_manager.lock(resource_key, ttl)
|
|
64
125
|
|
|
@@ -74,7 +135,7 @@ RSpec.describe Redlock::Client do
|
|
|
74
135
|
it 'interprets lock time as milliseconds' do
|
|
75
136
|
ttl = 20000
|
|
76
137
|
@lock_info = lock_manager.lock(resource_key, ttl)
|
|
77
|
-
expect(redis_client.
|
|
138
|
+
expect(redis_client.call('PTTL', resource_key)).to be_within(200).of(ttl)
|
|
78
139
|
end
|
|
79
140
|
|
|
80
141
|
it 'can extend its own lock' do
|
|
@@ -106,7 +167,7 @@ RSpec.describe Redlock::Client do
|
|
|
106
167
|
|
|
107
168
|
lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_only_if_locked: true)
|
|
108
169
|
expect(lock_info).not_to be_nil
|
|
109
|
-
expect(redis_client.
|
|
170
|
+
expect(redis_client.call('PTTL', resource_key)).to be_within(200).of(ttl)
|
|
110
171
|
end
|
|
111
172
|
|
|
112
173
|
context 'when extend_only_if_locked flag is not given' do
|
|
@@ -195,7 +256,7 @@ RSpec.describe Redlock::Client do
|
|
|
195
256
|
2000
|
|
196
257
|
end
|
|
197
258
|
|
|
198
|
-
lock_manager = Redlock::Client.new(
|
|
259
|
+
lock_manager = Redlock::Client.new(redis_urls_or_clients, retry_count: 1, retry_delay: retry_delay)
|
|
199
260
|
another_lock_info = lock_manager.lock(resource_key, ttl)
|
|
200
261
|
|
|
201
262
|
expect(lock_manager).to receive(:sleep) do |sleep|
|
|
@@ -249,7 +310,10 @@ RSpec.describe Redlock::Client do
|
|
|
249
310
|
|
|
250
311
|
expect {
|
|
251
312
|
lock_manager.lock(resource_key, ttl)
|
|
252
|
-
}.to raise_error(
|
|
313
|
+
}.to raise_error(Redlock::LockAcquisitionError) do |e|
|
|
314
|
+
expect(e.errors[0]).to be_a(RedisClient::CannotConnectError)
|
|
315
|
+
expect(e.errors.count).to eq 1
|
|
316
|
+
end
|
|
253
317
|
end
|
|
254
318
|
end
|
|
255
319
|
|
|
@@ -261,7 +325,10 @@ RSpec.describe Redlock::Client do
|
|
|
261
325
|
redis_instance.instance_variable_set(:@redis, unreachable_redis)
|
|
262
326
|
expect {
|
|
263
327
|
lock_manager.lock(resource_key, ttl)
|
|
264
|
-
}.to raise_error(
|
|
328
|
+
}.to raise_error(Redlock::LockAcquisitionError) do |e|
|
|
329
|
+
expect(e.errors[0]).to be_a(RedisClient::CannotConnectError)
|
|
330
|
+
expect(e.errors.count).to eq 1
|
|
331
|
+
end
|
|
265
332
|
redis_instance.instance_variable_set(:@redis, old_redis)
|
|
266
333
|
expect(lock_manager.lock(resource_key, ttl)).to be_truthy
|
|
267
334
|
end
|
|
@@ -270,10 +337,12 @@ RSpec.describe Redlock::Client do
|
|
|
270
337
|
context 'when script cache has been flushed' do
|
|
271
338
|
before(:each) do
|
|
272
339
|
@manipulated_instance = lock_manager.instance_variable_get(:@servers).first
|
|
273
|
-
@manipulated_instance.instance_variable_get(:@redis).
|
|
340
|
+
@manipulated_instance.instance_variable_get(:@redis).with { |conn|
|
|
341
|
+
conn.call('SCRIPT', 'FLUSH')
|
|
342
|
+
}
|
|
274
343
|
end
|
|
275
344
|
|
|
276
|
-
it 'does not raise a
|
|
345
|
+
it 'does not raise a RedisClient::CommandError: NOSCRIPT error' do
|
|
277
346
|
expect {
|
|
278
347
|
lock_manager.lock(resource_key, ttl)
|
|
279
348
|
}.to_not raise_error
|
|
@@ -289,11 +358,15 @@ RSpec.describe Redlock::Client do
|
|
|
289
358
|
# This time we do not pass it through to Redis, in order to simulate a passing
|
|
290
359
|
# call to LOAD SCRIPT followed by another NOSCRIPT error. Imagine someone
|
|
291
360
|
# repeatedly calling SCRIPT FLUSH on our Redis instance.
|
|
292
|
-
expect(@manipulated_instance).to receive(:load_scripts)
|
|
361
|
+
expect(@manipulated_instance).to receive(:load_scripts).exactly(8).times
|
|
293
362
|
|
|
294
363
|
expect {
|
|
295
364
|
lock_manager.lock(resource_key, ttl)
|
|
296
|
-
}.to raise_error(
|
|
365
|
+
}.to raise_error(Redlock::LockAcquisitionError) do |e|
|
|
366
|
+
expect(e.errors[0]).to be_a(RedisClient::CommandError)
|
|
367
|
+
expect(e.errors[0].message).to match(/NOSCRIPT/)
|
|
368
|
+
expect(e.errors.count).to eq 1
|
|
369
|
+
end
|
|
297
370
|
end
|
|
298
371
|
end
|
|
299
372
|
|
|
@@ -304,6 +377,108 @@ RSpec.describe Redlock::Client do
|
|
|
304
377
|
end
|
|
305
378
|
end
|
|
306
379
|
|
|
380
|
+
context 'when using the redis gem (Redis::CommandError)' do
|
|
381
|
+
# The redis gem raises Redis::CommandError (or Redis::NoScriptError) instead of
|
|
382
|
+
# RedisClient::CommandError. This tests that both error hierarchies are handled.
|
|
383
|
+
# See: https://github.com/leandromoreira/redlock-rb/issues/124
|
|
384
|
+
# See: https://github.com/leandromoreira/redlock-rb/issues/148
|
|
385
|
+
|
|
386
|
+
before(:all) do
|
|
387
|
+
# Define Redis::CommandError if not already defined (simulates redis gem being loaded)
|
|
388
|
+
unless defined?(Redis::CommandError)
|
|
389
|
+
module Redis
|
|
390
|
+
class CommandError < StandardError; end
|
|
391
|
+
end
|
|
392
|
+
@redis_command_error_defined_by_test = true
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
after(:all) do
|
|
397
|
+
# Clean up the mock class if we defined it
|
|
398
|
+
if @redis_command_error_defined_by_test
|
|
399
|
+
Redis.send(:remove_const, :CommandError)
|
|
400
|
+
Object.send(:remove_const, :Redis) if Redis.constants.empty?
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
describe '#script_error_classes' do
|
|
405
|
+
it 'includes RedisClient::CommandError' do
|
|
406
|
+
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
|
407
|
+
expect(redis_instance.send(:script_error_classes)).to include(RedisClient::CommandError)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
it 'includes Redis::CommandError when defined' do
|
|
411
|
+
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
|
412
|
+
# Clear memoization to pick up the newly defined class
|
|
413
|
+
redis_instance.instance_variable_set(:@script_error_classes, nil)
|
|
414
|
+
expect(redis_instance.send(:script_error_classes)).to include(Redis::CommandError)
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
context 'when Redis::CommandError NOSCRIPT is raised' do
|
|
419
|
+
it 'recovers by reloading scripts' do
|
|
420
|
+
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
|
421
|
+
# Clear memoization to ensure Redis::CommandError is included
|
|
422
|
+
redis_instance.instance_variable_set(:@script_error_classes, nil)
|
|
423
|
+
|
|
424
|
+
call_count = 0
|
|
425
|
+
allow(redis_instance).to receive(:synchronize).and_wrap_original do |original_method, &block|
|
|
426
|
+
call_count += 1
|
|
427
|
+
if call_count == 1
|
|
428
|
+
# First call raises Redis::CommandError with NOSCRIPT
|
|
429
|
+
raise Redis::CommandError, 'NOSCRIPT No matching script'
|
|
430
|
+
else
|
|
431
|
+
original_method.call(&block)
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
expect(redis_instance).to receive(:load_scripts).and_call_original
|
|
436
|
+
|
|
437
|
+
# Should recover and successfully lock
|
|
438
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
|
439
|
+
expect(lock_info).to be_truthy
|
|
440
|
+
lock_manager.unlock(lock_info) if lock_info
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
it 'does not retry more than once per operation' do
|
|
444
|
+
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
|
445
|
+
redis_instance.instance_variable_set(:@script_error_classes, nil)
|
|
446
|
+
|
|
447
|
+
# Always raise Redis::CommandError NOSCRIPT
|
|
448
|
+
allow(redis_instance).to receive(:synchronize).and_raise(
|
|
449
|
+
Redis::CommandError, 'NOSCRIPT No matching script'
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
load_scripts_count = 0
|
|
453
|
+
allow(redis_instance).to receive(:load_scripts) { load_scripts_count += 1 }
|
|
454
|
+
|
|
455
|
+
expect {
|
|
456
|
+
lock_manager.lock(resource_key, ttl)
|
|
457
|
+
}.to raise_error(Redlock::LockAcquisitionError)
|
|
458
|
+
|
|
459
|
+
# Should have called load_scripts once per retry attempt (8 times total:
|
|
460
|
+
# lock + unlock for each of retry_count+1 attempts, but unlock swallows errors)
|
|
461
|
+
expect(load_scripts_count).to eq(8)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
it 're-raises non-NOSCRIPT Redis::CommandError' do
|
|
465
|
+
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
|
466
|
+
redis_instance.instance_variable_set(:@script_error_classes, nil)
|
|
467
|
+
|
|
468
|
+
allow(redis_instance).to receive(:synchronize).and_raise(
|
|
469
|
+
Redis::CommandError, 'ERR some other error'
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
expect {
|
|
473
|
+
lock_manager.lock(resource_key, ttl)
|
|
474
|
+
}.to raise_error(Redlock::LockAcquisitionError) do |e|
|
|
475
|
+
expect(e.errors[0]).to be_a(Redis::CommandError)
|
|
476
|
+
expect(e.errors[0].message).to eq('ERR some other error')
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
307
482
|
describe 'block syntax' do
|
|
308
483
|
context 'when lock is available' do
|
|
309
484
|
it 'locks' do
|
|
@@ -473,7 +648,7 @@ RSpec.describe Redlock::Client do
|
|
|
473
648
|
|
|
474
649
|
# Replace redis with unreachable instance
|
|
475
650
|
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
|
476
|
-
|
|
651
|
+
_old_redis = redis_instance.instance_variable_get(:@redis)
|
|
477
652
|
redis_instance.instance_variable_set(:@redis, unreachable_redis)
|
|
478
653
|
|
|
479
654
|
expect {
|
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 {
|
|
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 {
|
|
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 {
|
|
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,34 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: redlock
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Leandro Moreira
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: redis
|
|
13
|
+
name: redis-client
|
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
18
|
+
version: 0.14.1
|
|
20
19
|
- - "<"
|
|
21
20
|
- !ruby/object:Gem::Version
|
|
22
|
-
version:
|
|
21
|
+
version: 1.0.0
|
|
23
22
|
type: :runtime
|
|
24
23
|
prerelease: false
|
|
25
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
26
25
|
requirements:
|
|
27
26
|
- - ">="
|
|
28
27
|
- !ruby/object:Gem::Version
|
|
29
|
-
version:
|
|
28
|
+
version: 0.14.1
|
|
30
29
|
- - "<"
|
|
31
30
|
- !ruby/object:Gem::Version
|
|
32
|
-
version:
|
|
31
|
+
version: 1.0.0
|
|
33
32
|
- !ruby/object:Gem::Dependency
|
|
34
33
|
name: connection_pool
|
|
35
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -45,19 +44,19 @@ dependencies:
|
|
|
45
44
|
- !ruby/object:Gem::Version
|
|
46
45
|
version: '2.2'
|
|
47
46
|
- !ruby/object:Gem::Dependency
|
|
48
|
-
name:
|
|
47
|
+
name: coveralls_reborn
|
|
49
48
|
requirement: !ruby/object:Gem::Requirement
|
|
50
49
|
requirements:
|
|
51
50
|
- - "~>"
|
|
52
51
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '0.
|
|
52
|
+
version: '0.29'
|
|
54
53
|
type: :development
|
|
55
54
|
prerelease: false
|
|
56
55
|
version_requirements: !ruby/object:Gem::Requirement
|
|
57
56
|
requirements:
|
|
58
57
|
- - "~>"
|
|
59
58
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: '0.
|
|
59
|
+
version: '0.29'
|
|
61
60
|
- !ruby/object:Gem::Dependency
|
|
62
61
|
name: json
|
|
63
62
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -65,9 +64,6 @@ dependencies:
|
|
|
65
64
|
- - ">="
|
|
66
65
|
- !ruby/object:Gem::Version
|
|
67
66
|
version: 2.3.0
|
|
68
|
-
- - "~>"
|
|
69
|
-
- !ruby/object:Gem::Version
|
|
70
|
-
version: 2.3.1
|
|
71
67
|
type: :development
|
|
72
68
|
prerelease: false
|
|
73
69
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -75,9 +71,6 @@ dependencies:
|
|
|
75
71
|
- - ">="
|
|
76
72
|
- !ruby/object:Gem::Version
|
|
77
73
|
version: 2.3.0
|
|
78
|
-
- - "~>"
|
|
79
|
-
- !ruby/object:Gem::Version
|
|
80
|
-
version: 2.3.1
|
|
81
74
|
- !ruby/object:Gem::Dependency
|
|
82
75
|
name: rake
|
|
83
76
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -128,9 +121,9 @@ files:
|
|
|
128
121
|
- ".github/workflows/ci.yml"
|
|
129
122
|
- ".gitignore"
|
|
130
123
|
- ".rspec"
|
|
124
|
+
- CHANGELOG.md
|
|
131
125
|
- CONTRIBUTORS
|
|
132
126
|
- Gemfile
|
|
133
|
-
- Gemfile.lock
|
|
134
127
|
- LICENSE
|
|
135
128
|
- Makefile
|
|
136
129
|
- README.md
|
|
@@ -150,7 +143,6 @@ homepage: https://github.com/leandromoreira/redlock-rb
|
|
|
150
143
|
licenses:
|
|
151
144
|
- BSD-2-Clause
|
|
152
145
|
metadata: {}
|
|
153
|
-
post_install_message:
|
|
154
146
|
rdoc_options: []
|
|
155
147
|
require_paths:
|
|
156
148
|
- lib
|
|
@@ -158,15 +150,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
158
150
|
requirements:
|
|
159
151
|
- - ">="
|
|
160
152
|
- !ruby/object:Gem::Version
|
|
161
|
-
version:
|
|
153
|
+
version: 2.7.0
|
|
162
154
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
163
155
|
requirements:
|
|
164
156
|
- - ">="
|
|
165
157
|
- !ruby/object:Gem::Version
|
|
166
158
|
version: '0'
|
|
167
159
|
requirements: []
|
|
168
|
-
rubygems_version:
|
|
169
|
-
signing_key:
|
|
160
|
+
rubygems_version: 4.0.3
|
|
170
161
|
specification_version: 4
|
|
171
162
|
summary: Distributed lock using Redis written in Ruby.
|
|
172
163
|
test_files:
|
data/Gemfile.lock
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
PATH
|
|
2
|
-
remote: .
|
|
3
|
-
specs:
|
|
4
|
-
redlock (1.3.2)
|
|
5
|
-
redis (>= 3.0.0, < 6.0)
|
|
6
|
-
|
|
7
|
-
GEM
|
|
8
|
-
remote: https://rubygems.org/
|
|
9
|
-
specs:
|
|
10
|
-
connection_pool (2.3.0)
|
|
11
|
-
coveralls (0.8.23)
|
|
12
|
-
json (>= 1.8, < 3)
|
|
13
|
-
simplecov (~> 0.16.1)
|
|
14
|
-
term-ansicolor (~> 1.3)
|
|
15
|
-
thor (>= 0.19.4, < 2.0)
|
|
16
|
-
tins (~> 1.6)
|
|
17
|
-
diff-lcs (1.5.0)
|
|
18
|
-
docile (1.4.0)
|
|
19
|
-
json (2.3.1)
|
|
20
|
-
rake (13.0.6)
|
|
21
|
-
redis (5.0.5)
|
|
22
|
-
redis-client (>= 0.9.0)
|
|
23
|
-
redis-client (0.10.0)
|
|
24
|
-
connection_pool
|
|
25
|
-
rspec (3.11.0)
|
|
26
|
-
rspec-core (~> 3.11.0)
|
|
27
|
-
rspec-expectations (~> 3.11.0)
|
|
28
|
-
rspec-mocks (~> 3.11.0)
|
|
29
|
-
rspec-core (3.11.0)
|
|
30
|
-
rspec-support (~> 3.11.0)
|
|
31
|
-
rspec-expectations (3.11.1)
|
|
32
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
|
33
|
-
rspec-support (~> 3.11.0)
|
|
34
|
-
rspec-mocks (3.11.1)
|
|
35
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
|
36
|
-
rspec-support (~> 3.11.0)
|
|
37
|
-
rspec-support (3.11.1)
|
|
38
|
-
simplecov (0.16.1)
|
|
39
|
-
docile (~> 1.1)
|
|
40
|
-
json (>= 1.8, < 3)
|
|
41
|
-
simplecov-html (~> 0.10.0)
|
|
42
|
-
simplecov-html (0.10.2)
|
|
43
|
-
sync (0.5.0)
|
|
44
|
-
term-ansicolor (1.7.1)
|
|
45
|
-
tins (~> 1.0)
|
|
46
|
-
thor (1.2.1)
|
|
47
|
-
tins (1.31.1)
|
|
48
|
-
sync
|
|
49
|
-
|
|
50
|
-
PLATFORMS
|
|
51
|
-
ruby
|
|
52
|
-
|
|
53
|
-
DEPENDENCIES
|
|
54
|
-
connection_pool (~> 2.2)
|
|
55
|
-
coveralls (~> 0.8)
|
|
56
|
-
json (~> 2.3.1, >= 2.3.0)
|
|
57
|
-
rake (~> 13.0, >= 11.1.2)
|
|
58
|
-
redlock!
|
|
59
|
-
rspec (~> 3, >= 3.0.0)
|
|
60
|
-
|
|
61
|
-
BUNDLED WITH
|
|
62
|
-
2.3.7
|