From a00b0012bd81b2a8a954db3144aea337905840ce Mon Sep 17 00:00:00 2001 From: Adrian Gomez Date: Thu, 5 May 2022 11:06:07 -0300 Subject: [PATCH] Use a connection pool for redis --- Gemfile | 2 -- Gemfile.lock | 49 ++++++++++++---------------- lib/simple_redlock.rb | 52 ++++++++++++++++++++++++++++-- lib/simple_redlock/lockable.rb | 12 +++++-- lib/simple_redlock/locker.rb | 51 ++++++++++++++--------------- simple_redlock.gemspec | 4 +++ spec/simple_redlock/locker_spec.rb | 9 +++--- 7 files changed, 111 insertions(+), 68 deletions(-) diff --git a/Gemfile b/Gemfile index 988aa5e..180f8cc 100644 --- a/Gemfile +++ b/Gemfile @@ -2,5 +2,3 @@ source 'https://rubygems.org' # Specify your gem's dependencies in simple_redlock.gemspec gemspec - -gem 'activesupport' diff --git a/Gemfile.lock b/Gemfile.lock index 744f7f6..e76bdd3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/lib/simple_redlock.rb b/lib/simple_redlock.rb index d578634..ba7880b 100644 --- a/lib/simple_redlock.rb +++ b/lib/simple_redlock.rb @@ -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 diff --git a/lib/simple_redlock/lockable.rb b/lib/simple_redlock/lockable.rb index 0b9c9c5..ba35983 100644 --- a/lib/simple_redlock/lockable.rb +++ b/lib/simple_redlock/lockable.rb @@ -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 diff --git a/lib/simple_redlock/locker.rb b/lib/simple_redlock/locker.rb index 0c10f84..e2488ca 100644 --- a/lib/simple_redlock/locker.rb +++ b/lib/simple_redlock/locker.rb @@ -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]) @@ -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 @@ -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 @@ -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 diff --git a/simple_redlock.gemspec b/simple_redlock.gemspec index 2f9874b..7616dba 100644 --- a/simple_redlock.gemspec +++ b/simple_redlock.gemspec @@ -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 diff --git a/spec/simple_redlock/locker_spec.rb b/spec/simple_redlock/locker_spec.rb index 52a91f0..e509b37 100644 --- a/spec/simple_redlock/locker_spec.rb +++ b/spec/simple_redlock/locker_spec.rb @@ -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) }