Skip to content

Commit

Permalink
Use a connection pool for redis
Browse files Browse the repository at this point in the history
  • Loading branch information
adrian-gomez committed May 5, 2022
1 parent eda400f commit a00b001
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 68 deletions.
2 changes: 0 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,3 @@ source 'https://rubygems.org'

# Specify your gem's dependencies in simple_redlock.gemspec
gemspec

gem 'activesupport'
49 changes: 20 additions & 29 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,39 @@ PATH
remote: .
specs:
simple_redlock (0.1.0)
connection_pool (~> 2.2.5)
hiredis (~> 0.6.3)
redis (~> 4.6.0)

GEM
remote: https://rubygems.org/
specs:
activesupport (6.0.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
zeitwerk (~> 2.2)
concurrent-ruby (1.1.5)
diff-lcs (1.3)
i18n (1.7.0)
concurrent-ruby (~> 1.0)
minitest (5.13.0)
rake (13.0.3)
rspec (3.9.0)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
rspec-mocks (~> 3.9.0)
rspec-core (3.9.0)
rspec-support (~> 3.9.0)
rspec-expectations (3.9.0)
connection_pool (2.2.5)
diff-lcs (1.5.0)
hiredis (0.6.3)
rake (13.0.6)
redis (4.6.0)
rspec (3.11.0)
rspec-core (~> 3.11.0)
rspec-expectations (~> 3.11.0)
rspec-mocks (~> 3.11.0)
rspec-core (3.11.0)
rspec-support (~> 3.11.0)
rspec-expectations (3.11.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.0)
rspec-support (~> 3.11.0)
rspec-mocks (3.11.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-support (3.9.0)
thread_safe (0.3.6)
tzinfo (1.2.5)
thread_safe (~> 0.1)
zeitwerk (2.2.1)
rspec-support (~> 3.11.0)
rspec-support (3.11.0)

PLATFORMS
ruby

DEPENDENCIES
activesupport
rake (~> 13.0)
rspec (~> 3.0)
simple_redlock!

BUNDLED WITH
2.0.1
2.3.10
52 changes: 49 additions & 3 deletions lib/simple_redlock.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,57 @@
require 'active_support/core_ext/numeric/time'
require 'connection_pool'
require 'redis'
require 'redis/connection/hiredis'

require_relative 'simple_redlock/locker'
require_relative 'simple_redlock/lockable'

module SimpleRedlock
mattr_accessor :redis_url

def self.configure(&block)
yield self
end

def self.redis_url=(redis_url)
@redis_url = redis_url
end

def self.redis_url
@redis_url
end

def self.redis_pool_timeout=(redis_pool_timeout)
@redis_pool_timeout = redis_pool_timeout
end

def self.redis_pool_timeout
@redis_pool_timeout ||= 1
end

def self.redis_pool_size=(redis_pool_size)
@redis_pool_size = redis_pool_size
end

def self.redis_pool_size
@redis_pool_size ||= 5
end

def self.redis_pool=(redis_pool)
@redis_pool = redis_pool
end

def self.redis_pool
@redis_pool ||= ConnectionPool.new(timeout: redis_pool_timeout, size: redis_pool_size) do
Redis.new(url: redis_url, driver: :hiredis)
end
end

def self.testing!
SimpleRedlock::Locker.class_eval do
def lock(*args)
true
end

def unlock(*args)
end
end
end
end
12 changes: 9 additions & 3 deletions lib/simple_redlock/lockable.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
module SimpleRedlock
module Lockable
def exclusively(key, options = {}, &block)
transaction do
redis_lock.with_lock!(resource: exclusive_key(key), **options) do
def exclusively(key, open_transaction: true, dont_reload: false, **options, &block)
redis_lock.with_lock!(resource: exclusive_key(key), **options) do
if open_transaction
transaction do
reload unless dont_reload
yield
end
else
reload unless dont_reload
yield
end
end
Expand Down
51 changes: 24 additions & 27 deletions lib/simple_redlock/locker.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# frozen_string_literal: true

require 'securerandom'

LockError = Class.new(StandardError)

module SimpleRedlock
class Locker
DEFAULT_RETRY_COUNT = 25
DEFAULT_TTL = 5.seconds
DEFAULT_TTL = 5 # in seconds
UNLOCK_SCRIPT = <<~LUA
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
Expand All @@ -14,23 +16,14 @@ class Locker
end
LUA

# Create a lock manager using Redis Set.
# Params:
# +options+:: Override the default value for `retry_count`
# * `retry_count` how many times we try to lock a resource (default: 5)
def initialize(options = {})
@retry_count = options[:retry_count] || DEFAULT_RETRY_COUNT
@redis = redis
end

# Locks a resource, and passes the locked status to the block.
# The lock is unlocked after execution
# Params:
# +resource+:: the resource (or key) string to be locked.
# +ttl+:: the time-to-live in duration for the lock.
# +ttl+:: the time-to-live in seconds duration for the lock.
# +block+:: block to be executed after successful lock acquisition.
def with_lock(resource:, value: SecureRandom.hex, ttl: DEFAULT_TTL)
locked = lock_resource(resource, value, ttl.to_i * 1000)
def with_lock(resource:, value: SecureRandom.hex, ttl: DEFAULT_TTL, retry_count: DEFAULT_RETRY_COUNT)
locked = lock_resource(resource, value, ttl.to_i * 1000, retry_count)

yield locked
locked
Expand All @@ -42,10 +35,10 @@ def with_lock(resource:, value: SecureRandom.hex, ttl: DEFAULT_TTL)
# The lock is unlocked after execution
# Params:
# +resource+:: the resource (or key) string to be locked.
# +ttl+:: the time-to-live in duration for the lock.
# +ttl+:: the time-to-live in seconds duration for the lock.
# +block+:: block to be executed after successful lock acquisition.
def with_lock!(resource:, value: SecureRandom.hex, ttl: DEFAULT_TTL)
with_lock(resource: resource, value: value, ttl: ttl) do |locked|
def with_lock!(resource:, value: SecureRandom.hex, ttl: DEFAULT_TTL, retry_count: DEFAULT_RETRY_COUNT)
with_lock(resource: resource, value: value, ttl: ttl, retry_count: retry_count) do |locked|
fail LockError, "Could not acquire lock for #{resource}" unless locked

return yield
Expand All @@ -54,34 +47,38 @@ def with_lock!(resource:, value: SecureRandom.hex, ttl: DEFAULT_TTL)
unlock(resource, value)
end

# Locks a resource for a given time. Retries the lock @retry_count number of times.
# Locks a resource for a given time. Retries the lock retry_count number of times.
# Params:
# +resource+:: the key string to be locked.
# +value+:: a unique string that checks lock ownership
# +ttl+:: The time-to-live in duration for the lock.
def lock_resource(resource, value, ttl)
@retry_count.times do
# +ttl+:: The time-to-live in miliseconds duration for the lock.
def lock_resource(resource, value, ttl, retry_count)
retry_count.times do |i|
locked = lock(resource, value, ttl)
return locked if locked

# Random delay before retrying
sleep(rand(ttl / @retry_count).to_f / 1000)
sleep(rand(ttl / retry_count).to_f / 1000)
end

false
end

def redis
Thread.current[:redis] ||= Redis.new(url: SimpleRedlock.redis_url)
end

def lock(key, value, ttl)
@redis.set(key, value, nx: true, px: ttl) == true
redis_pool.with do |redis|
redis.set(key, value, nx: true, px: ttl) == true
end
end

def unlock(key, value)
@redis.eval(UNLOCK_SCRIPT, keys: [key], argv: [value])
redis_pool.with do |redis|
redis.eval(UNLOCK_SCRIPT, keys: [key], argv: [value])
end
rescue StandardError
end

def redis_pool
SimpleRedlock.redis_pool
end
end
end
4 changes: 4 additions & 0 deletions simple_redlock.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ['lib']

spec.add_dependency 'redis', '~> 4.6.0'
spec.add_dependency 'hiredis', '~> 0.6.3'
spec.add_dependency 'connection_pool', '~> 2.2.5'

spec.add_development_dependency 'rake', '~> 13.0'
spec.add_development_dependency 'rspec', '~> 3.0'
end
9 changes: 5 additions & 4 deletions spec/simple_redlock/locker_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@

RSpec.describe SimpleRedlock::Locker do
let(:redis_double) { double('Redis') }
let(:redis_lock) { described_class.new(retry_count: retry_count) }
let(:redis_lock) { described_class.new }
let(:retry_count) { 20 }
before { allow_any_instance_of(described_class).to receive(:redis).and_return(redis_double) }

before { allow_any_instance_of(ConnectionPool).to receive(:with).and_yield(redis_double) }

describe '#lock_resource' do
let(:key) { 'key' }
let(:value) { 'value' }
let(:ttl) { 2.seconds }
subject(:locked) { redis_lock.lock_resource(key, value, ttl) }
let(:ttl) { 2 }
subject(:locked) { redis_lock.lock_resource(key, value, ttl, retry_count) }

context 'when the lock is available' do
before { allow(redis_double).to receive_messages(set: true) }
Expand Down

0 comments on commit a00b001

Please sign in to comment.