Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use SET NX/EX for new master locking approach #734

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
## [Unreleased]
### Changed
- Remove support for Ruby < 2.3
- Add `Lock::ResilientModern` that uses GET with NX/EX instead of Lua script to set master lock
carsonreinke marked this conversation as resolved.
Show resolved Hide resolved

## [4.5.0] - 2021-09-25
### Added
Expand Down
2 changes: 1 addition & 1 deletion lib/resque/scheduler/lock.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# vim:fileencoding=utf-8
%w(base basic resilient).each do |file|
%w(base basic resilient resilient_modern).each do |file|
require "resque/scheduler/lock/#{file}"
end
14 changes: 14 additions & 0 deletions lib/resque/scheduler/lock/resilient_modern.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# vim:fileencoding=utf-8
require_relative 'base'

module Resque
module Scheduler
module Lock
class ResilientModern < Resilient
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't come up with a better name

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does inheriting from Resilient give us?

I'd prefer inheriting from base and add a implementation for locked?. This would make it easier to remove the Resilient (and Basic) lock implementations in the future, which I can't imagine we wouldn't do now that we have this awesome new lock implementation (and redis 2.x is very old at this point).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iloveitaly The Resilient implementation offers the locked? method which retrieves and renews the lock. So, it looks like in Redis 6.2 a similar command was added.

@yaauie suggested I retain the existing implementation to avoid users downgrading to a worse master lock implementation if they upgraded resque-scheduler.

One possibility could be switching the Redis requirement to 6.2 and completely removing the inheritance and Lua script implementation.

def acquire!
Resque.redis.set(key, value, nx: true, ex: timeout)
end
end
end
end
end
13 changes: 10 additions & 3 deletions lib/resque/scheduler/locking.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
# were missed when it starts up again.

require_relative 'lock'
require 'rubygems/version'

module Resque
module Scheduler
Expand All @@ -59,7 +60,11 @@ def master_lock
end

def supports_lua?
redis_master_version >= 2.5
redis_master_version >= Gem::Version.new('2.5')
end

def supports_get_x_options?
redis_master_version >= Gem::Version.new('2.6.12')
end

def master?
Expand All @@ -83,7 +88,9 @@ def release_master_lock
private

def build_master_lock
if supports_lua?
if supports_get_x_options?
Resque::Scheduler::Lock::ResilientModern.new(master_lock_key)
elsif supports_lua?
Resque::Scheduler::Lock::Resilient.new(master_lock_key)
else
Resque::Scheduler::Lock::Basic.new(master_lock_key)
Expand All @@ -97,7 +104,7 @@ def master_lock_key
end

def redis_master_version
Resque.data_store.redis.info['redis_version'].to_f
Gem::Version.new(Resque.data_store.redis.info['redis_version'])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the change to using Gem::Version here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iloveitaly this offers "native" version number parsing/comparison which was needed to see when the Redis feature was available. Specifically, the patch level comparison.

end
end
end
Expand Down
211 changes: 102 additions & 109 deletions test/scheduler_locking_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,77 @@ def lock_is_not_held(lock)
end
end

module LockSharedTests
# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/AbcSize
def self.included(mod)
mod.class_eval do
test 'you should not have the lock if someone else holds it' do
lock_is_not_held(@lock)

assert [email protected]?
end

test 'you should not be able to acquire the lock if someone ' \
'else holds it' do
lock_is_not_held(@lock)

assert [email protected]!
end

test 'the lock should receive a TTL on acquiring' do
@lock.acquire!

assert Resque.redis.ttl(@lock.key) > 0, 'lock should expire'
end

test 'releasing should release the master lock' do
assert @lock.acquire!, 'should have acquired the master lock'
assert @lock.locked?, 'should be locked'

@lock.release!

assert [email protected]?, 'should not be locked'
end

test 'checking the lock should increase the TTL if we hold it' do
@lock.acquire!
Resque.redis.setex(@lock.key, 10, @lock.value)

@lock.locked?

assert Resque.redis.ttl(@lock.key) > 10, 'TTL should have been updated'
end

test 'checking the lock should not increase the TTL if we do not hold it' do
Resque.redis.setex(@lock.key, 10, @lock.value)
lock_is_not_held(@lock)

@lock.locked?

assert Resque.redis.ttl(@lock.key) <= 10,
'TTL should not have been updated'
end

test 'setting the lock timeout changes the key TTL if we hold it' do
@lock.acquire!

@lock.stubs(:locked?).returns(true)
@lock.timeout = 120
ttl = Resque.redis.ttl(@lock.key)
assert_send [ttl, :>, 100]

@lock.stubs(:locked?).returns(true)
@lock.timeout = 180
ttl = Resque.redis.ttl(@lock.key)
assert_send [ttl, :>, 120]
end
end
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
end

context '#master_lock_key' do
setup do
@subject = Class.new { extend Resque::Scheduler::Locking }
Expand Down Expand Up @@ -94,14 +165,22 @@ def lock_is_not_held(lock)
assert_equal @subject.master_lock.class, Resque::Scheduler::Lock::Basic
end

test 'should use the resilient lock mechanism for > Redis 2.4' do
Resque.redis.stubs(:info).returns('redis_version' => '2.5.12')
test 'should use the resilient lock mechanism for > Redis 2.4 and < 2.6.12' do
Resque.data_store.redis.stubs(:info).returns('redis_version' => '2.5.12')

assert_equal(
@subject.master_lock.class, Resque::Scheduler::Lock::Resilient
)
end

test 'should use the resilient lock mechanism for >= Redis 2.6.12' do
Resque.data_store.redis.stubs(:info).returns('redis_version' => '2.8.0')

assert_equal(
@subject.master_lock.class, Resque::Scheduler::Lock::ResilientModern
)
end

test 'should be the master if the lock is held' do
@subject.master_lock.acquire!
assert @subject.master?, 'should be master'
Expand Down Expand Up @@ -153,52 +232,7 @@ def lock_is_not_held(lock)
@lock.release!
end

test 'you should not have the lock if someone else holds it' do
lock_is_not_held(@lock)

assert [email protected]?
end

test 'you should not be able to acquire the lock if someone ' \
'else holds it' do
lock_is_not_held(@lock)

assert [email protected]!
end

test 'the lock should receive a TTL on acquiring' do
@lock.acquire!

assert Resque.redis.ttl(@lock.key) > 0, 'lock should expire'
end

test 'releasing should release the master lock' do
assert @lock.acquire!, 'should have acquired the master lock'
assert @lock.locked?, 'should be locked'

@lock.release!

assert [email protected]?, 'should not be locked'
end

test 'checking the lock should increase the TTL if we hold it' do
@lock.acquire!
Resque.redis.setex(@lock.key, 10, @lock.value)

@lock.locked?

assert Resque.redis.ttl(@lock.key) > 10, 'TTL should have been updated'
end

test 'checking the lock should not increase the TTL if we do not hold it' do
Resque.redis.setex(@lock.key, 10, @lock.value)
lock_is_not_held(@lock)

@lock.locked?

assert Resque.redis.ttl(@lock.key) <= 10,
'TTL should not have been updated'
end
include LockSharedTests
end

context 'Resque::Scheduler::Lock::Resilient' do
Expand All @@ -216,11 +250,7 @@ def lock_is_not_held(lock)
@lock.release!
end

test 'you should not have the lock if someone else holds it' do
lock_is_not_held(@lock)

assert [email protected]?, 'you should not have the lock'
end
include LockSharedTests

test 'refreshes sha cache when the sha cannot be found on ' \
'the redis server' do
Expand All @@ -233,62 +263,6 @@ def lock_is_not_held(lock)
assert_false @lock.acquire!
end

test 'you should not be able to acquire the lock if someone ' \
'else holds it' do
lock_is_not_held(@lock)

assert [email protected]!
end

test 'the lock should receive a TTL on acquiring' do
@lock.acquire!

assert Resque.redis.ttl(@lock.key) > 0, 'lock should expire'
end

test 'releasing should release the master lock' do
assert @lock.acquire!, 'should have acquired the master lock'
assert @lock.locked?, 'should be locked'

@lock.release!

assert [email protected]?, 'should not be locked'
end

test 'checking the lock should increase the TTL if we hold it' do
@lock.acquire!
Resque.redis.setex(@lock.key, 10, @lock.value)

@lock.locked?

assert Resque.redis.ttl(@lock.key) > 10, 'TTL should have been updated'
end

test 'checking the lock should not increase the TTL if we do ' \
'not hold it' do
Resque.redis.setex(@lock.key, 10, @lock.value)
lock_is_not_held(@lock)

@lock.locked?

assert Resque.redis.ttl(@lock.key) <= 10,
'TTL should not have been updated'
end

test 'setting the lock timeout changes the key TTL if we hold it' do
@lock.acquire!

@lock.stubs(:locked?).returns(true)
@lock.timeout = 120
ttl = Resque.redis.ttl(@lock.key)
assert_send [ttl, :>, 100]

@lock.stubs(:locked?).returns(true)
@lock.timeout = 180
ttl = Resque.redis.ttl(@lock.key)
assert_send [ttl, :>, 120]
end

test 'setting lock timeout is a noop if not held' do
@lock.acquire!
@lock.timeout = 100
Expand Down Expand Up @@ -325,3 +299,22 @@ def lock_is_not_held(lock)
end
end
end

context 'Resque::Scheduler::Lock::ResilientModern' do
include LockTestHelper

if !Resque::Scheduler.supports_get_x_options?
puts '*** Skipping Resque::Scheduler::Lock::ResilientModern ' \
'tests, as they require Redis >= 2.6.2.'
else
setup do
@lock = Resque::Scheduler::Lock::ResilientModern.new('test_resilient_modern_lock')
end

teardown do
@lock.release!
end

include LockSharedTests
end
end