Skip to content

Commit

Permalink
Merge pull request #122 from sirwolfgang/main
Browse files Browse the repository at this point in the history
Upgrade to RedisClient
  • Loading branch information
leandromoreira authored Feb 9, 2023
2 parents f2f9cab + 5c251cf commit b6e3031
Show file tree
Hide file tree
Showing 9 changed files with 37 additions and 97 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
coverage/
.bundle/
dump.rdb
Gemfile.lock
62 changes: 0 additions & 62 deletions Gemfile.lock

This file was deleted.

3 changes: 0 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,3 @@ build:

publish:
docker-compose run --rm test gem push `ls -lt *gem | head -n 1 | awk '{ print $$9 }'`

updateLock:
docker-compose run --rm test bundle lock --update
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This is an implementation of a proposed [distributed lock algorithm with Redis](

## Compatibility

Redlock works with Redis versions 2.6 or later.
Redlock works with Redis versions 6.0 or later.

## Installation

Expand Down Expand Up @@ -151,7 +151,7 @@ lock_manager.get_remaining_ttl_for_lock(lock_info)

lock_manager.unlock(lock_info)
lock_manager.get_remaining_ttl_for_lock(lock_info)
#=> nil
#=> nil
```

Use `get_remaining_ttl_for_resource` if you do not hold a lock, but want to know the remaining TTL on a locked resource:
Expand All @@ -164,21 +164,21 @@ lock_info = lock_manager.lock(resource, 2000)
lock_manager.locked?(resource)
#=> true
lock_manager.get_remaining_ttl_for_resource(resource)
#=> 1975
#=> 1975

# Sometime later
lock_manager.locked?(resource)
#=> false
lock_manager.get_remaining_ttl_for_resource(resource)
#=> nil
#=> nil
```

## Redis client configuration

`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.

```ruby
servers = [ 'redis://localhost:6379', Redis.new(:url => 'redis://someotherhost:6379') ]
servers = [ 'redis://localhost:6379', RedisClient.new(:url => 'redis://someotherhost:6379') ]
redlock = Redlock::Client.new(servers)
```

Expand Down
20 changes: 11 additions & 9 deletions lib/redlock/client.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require 'redis'
require 'redis-client'
require 'securerandom'

module Redlock
Expand Down Expand Up @@ -160,31 +160,31 @@ def initialize(connection)
if connection.respond_to?(:client)
@redis = connection
else
@redis = Redis.new(connection)
@redis = RedisClient.new(connection)
end
@redis.extend(ConnectionPoolLike)
end
end

def lock(resource, val, ttl, allow_new_lock)
recover_from_script_flush do
@redis.with { |conn| conn.evalsha Scripts::LOCK_SCRIPT_SHA, [resource], [val, ttl, allow_new_lock] }
@redis.call('EVALSHA', Scripts::LOCK_SCRIPT_SHA, 1, resource, val, ttl, allow_new_lock)
end
end

def unlock(resource, val)
recover_from_script_flush do
@redis.with { |conn| conn.evalsha Scripts::UNLOCK_SCRIPT_SHA, [resource], [val] }
@redis.call('EVALSHA', Scripts::UNLOCK_SCRIPT_SHA, 1, resource, val)
end
rescue
# Nothing to do, unlocking is just a best-effort attempt.
end

def get_remaining_ttl(resource)
recover_from_script_flush do
@redis.with { |conn| conn.evalsha Scripts::PTTL_SCRIPT_SHA, keys: [resource] }
@redis.call('EVALSHA', Scripts::PTTL_SCRIPT_SHA, 1, resource)
end
rescue Redis::BaseConnectionError
rescue RedisClient::ConnectionError
nil
end

Expand All @@ -197,16 +197,18 @@ def load_scripts
Scripts::PTTL_SCRIPT
]

scripts.each do |script|
@redis.with { |conn| conn.script(:load, script) }
@redis.with do |connnection|
scripts.each do |script|
connnection.call('SCRIPT', 'LOAD', script)
end
end
end

def recover_from_script_flush
retry_on_noscript = true
begin
yield
rescue Redis::CommandError => e
rescue RedisClient::CommandError => e
# When somebody has flushed the Redis instance's script cache, we might
# want to reload our scripts. Only attempt this once, though, to avoid
# going into an infinite loop.
Expand Down
2 changes: 1 addition & 1 deletion lib/redlock/testing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class RedisInstance

def load_scripts
load_scripts_without_testing unless Redlock::Client.testing_mode == :bypass
rescue Redis::CommandError
rescue RedisClient::CommandError
# FakeRedis doesn't have #script, but doesn't need it either.
raise unless defined?(::FakeRedis)
rescue NoMethodError
Expand Down
2 changes: 1 addition & 1 deletion lib/redlock/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Redlock
VERSION = '1.3.2'
VERSION = '2.0.0'
end
2 changes: 1 addition & 1 deletion redlock.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ['lib']

spec.add_dependency 'redis', '>= 3.0.0', '< 6.0'
spec.add_dependency 'redis-client'

spec.add_development_dependency 'connection_pool', '~> 2.2'
spec.add_development_dependency 'coveralls', '~> 0.8'
Expand Down
32 changes: 17 additions & 15 deletions spec/client_spec.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
require 'spec_helper'
require 'securerandom'
require 'redis'
require 'connection_pool'

RSpec.describe Redlock::Client do
# It is recommended to have at least 3 servers in production
let(:lock_manager_opts) { { retry_count: 3 } }
let(:lock_manager) { Redlock::Client.new(Redlock::Client::DEFAULT_REDIS_URLS, lock_manager_opts) }
let(:redis_client) { Redis.new(url: "redis://#{redis1_host}:#{redis1_port}") }
let(:redis_client) { RedisClient.new(url: "redis://#{redis1_host}:#{redis1_port}") }
let(:resource_key) { SecureRandom.hex(3) }
let(:ttl) { 1000 }
let(:redis1_host) { ENV["REDIS1_HOST"] || "localhost" }
Expand All @@ -17,7 +16,7 @@
let(:redis3_host) { ENV["REDIS3_HOST"] || "127.0.0.1" }
let(:redis3_port) { ENV["REDIS3_PORT"] || "6379" }
let(:unreachable_redis) {
redis = Redis.new(url: 'redis://localhost:46864')
redis = RedisClient.new(url: 'redis://localhost:46864')
def redis.with
yield self
end
Expand All @@ -26,18 +25,18 @@ def redis.with

describe 'initialize' do
it 'accepts both redis URLs and Redis objects' do
servers = [ "redis://#{redis1_host}:#{redis1_port}", Redis.new(url: "redis://#{redis2_host}:#{redis2_port}") ]
servers = [ "redis://#{redis1_host}:#{redis1_port}", RedisClient.new(url: "redis://#{redis2_host}:#{redis2_port}") ]
redlock = Redlock::Client.new(servers)

redlock_servers = redlock.instance_variable_get(:@servers).map do |s|
s.instance_variable_get(:@redis).connection[:host]
s.instance_variable_get(:@redis).config.host
end

expect(redlock_servers).to match_array([redis1_host, redis2_host])
end

it 'accepts ConnectionPool objects' do
pool = ConnectionPool.new { Redis.new(url: "redis://#{redis1_host}:#{redis1_port}") }
pool = ConnectionPool.new { RedisClient.new(url: "redis://#{redis1_host}:#{redis1_port}") }
redlock = Redlock::Client.new([pool])

lock_info = lock_manager.lock(resource_key, ttl)
Expand All @@ -46,12 +45,15 @@ def redis.with
end

it 'does not load scripts' do
redis_client.script(:flush)
redis_client.call('SCRIPT', 'FLUSH')

pool = ConnectionPool.new { Redis.new(url: "redis://#{redis1_host}:#{redis1_port}") }
pool = ConnectionPool.new { RedisClient.new(url: "redis://#{redis1_host}:#{redis1_port}") }
redlock = Redlock::Client.new([pool])

expect(redis_client.info["number_of_cached_scripts"]).to eq("0")
raw_info = redis_client.call('INFO')
number_of_cached_scripts = raw_info[/number_of_cached_scripts\:\d+/].split(':').last

expect(number_of_cached_scripts).to eq("0")
end
end

Expand All @@ -74,7 +76,7 @@ def redis.with
it 'interprets lock time as milliseconds' do
ttl = 20000
@lock_info = lock_manager.lock(resource_key, ttl)
expect(redis_client.pttl(resource_key)).to be_within(200).of(ttl)
expect(redis_client.call('PTTL', resource_key)).to be_within(200).of(ttl)
end

it 'can extend its own lock' do
Expand Down Expand Up @@ -106,7 +108,7 @@ def redis.with

lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_only_if_locked: true)
expect(lock_info).not_to be_nil
expect(redis_client.pttl(resource_key)).to be_within(200).of(ttl)
expect(redis_client.call('PTTL', resource_key)).to be_within(200).of(ttl)
end

context 'when extend_only_if_locked flag is not given' do
Expand Down Expand Up @@ -249,7 +251,7 @@ def redis.with

expect {
lock_manager.lock(resource_key, ttl)
}.to raise_error(Redis::CannotConnectError)
}.to raise_error(RedisClient::CannotConnectError)
end
end

Expand All @@ -261,7 +263,7 @@ def redis.with
redis_instance.instance_variable_set(:@redis, unreachable_redis)
expect {
lock_manager.lock(resource_key, ttl)
}.to raise_error(Redis::CannotConnectError)
}.to raise_error(RedisClient::CannotConnectError)
redis_instance.instance_variable_set(:@redis, old_redis)
expect(lock_manager.lock(resource_key, ttl)).to be_truthy
end
Expand All @@ -270,10 +272,10 @@ def redis.with
context 'when script cache has been flushed' do
before(:each) do
@manipulated_instance = lock_manager.instance_variable_get(:@servers).first
@manipulated_instance.instance_variable_get(:@redis).script(:flush)
@manipulated_instance.instance_variable_get(:@redis).call('SCRIPT', 'FLUSH')
end

it 'does not raise a Redis::CommandError: NOSCRIPT error' do
it 'does not raise a RedisClient::CommandError: NOSCRIPT error' do
expect {
lock_manager.lock(resource_key, ttl)
}.to_not raise_error
Expand Down

0 comments on commit b6e3031

Please sign in to comment.