Skip to content

Commit 284beca

Browse files
committed
Use SET NX/EX for new master locking instead of Lua script
1 parent d7bd3c5 commit 284beca

File tree

5 files changed

+128
-113
lines changed

5 files changed

+128
-113
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
## [Unreleased]
66
### Changed
77
- Remove support for Ruby < 2.3
8+
- Add `Lock::ResilientModern` that uses GET with NX/EX instead of Lua script to set master lock
89

910
## [4.5.0] - 2021-09-25
1011
### Added

lib/resque/scheduler/lock.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# vim:fileencoding=utf-8
2-
%w(base basic resilient).each do |file|
2+
%w(base basic resilient resilient_modern).each do |file|
33
require "resque/scheduler/lock/#{file}"
44
end
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# vim:fileencoding=utf-8
2+
require_relative 'base'
3+
4+
module Resque
5+
module Scheduler
6+
module Lock
7+
class ResilientModern < Resilient
8+
def acquire!
9+
Resque.redis.set(key, value, nx: true, ex: timeout)
10+
end
11+
end
12+
end
13+
end
14+
end

lib/resque/scheduler/locking.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
# were missed when it starts up again.
5151

5252
require_relative 'lock'
53+
require 'rubygems/version'
5354

5455
module Resque
5556
module Scheduler
@@ -59,7 +60,11 @@ def master_lock
5960
end
6061

6162
def supports_lua?
62-
redis_master_version >= 2.5
63+
redis_master_version >= Gem::Version.new('2.5')
64+
end
65+
66+
def supports_get_x_options?
67+
redis_master_version >= Gem::Version.new('2.6.12')
6368
end
6469

6570
def master?
@@ -83,7 +88,9 @@ def release_master_lock
8388
private
8489

8590
def build_master_lock
86-
if supports_lua?
91+
if supports_get_x_options?
92+
Resque::Scheduler::Lock::ResilientModern.new(master_lock_key)
93+
elsif supports_lua?
8794
Resque::Scheduler::Lock::Resilient.new(master_lock_key)
8895
else
8996
Resque::Scheduler::Lock::Basic.new(master_lock_key)
@@ -97,7 +104,7 @@ def master_lock_key
97104
end
98105

99106
def redis_master_version
100-
Resque.data_store.redis.info['redis_version'].to_f
107+
Gem::Version.new(Resque.data_store.redis.info['redis_version'])
101108
end
102109
end
103110
end

test/scheduler_locking_test.rb

Lines changed: 102 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,77 @@ def lock_is_not_held(lock)
77
end
88
end
99

10+
module LockSharedTests
11+
# rubocop:disable Metrics/MethodLength
12+
# rubocop:disable Metrics/AbcSize
13+
def self.included(mod)
14+
mod.class_eval do
15+
test 'you should not have the lock if someone else holds it' do
16+
lock_is_not_held(@lock)
17+
18+
assert !@lock.locked?
19+
end
20+
21+
test 'you should not be able to acquire the lock if someone ' \
22+
'else holds it' do
23+
lock_is_not_held(@lock)
24+
25+
assert !@lock.acquire!
26+
end
27+
28+
test 'the lock should receive a TTL on acquiring' do
29+
@lock.acquire!
30+
31+
assert Resque.redis.ttl(@lock.key) > 0, 'lock should expire'
32+
end
33+
34+
test 'releasing should release the master lock' do
35+
assert @lock.acquire!, 'should have acquired the master lock'
36+
assert @lock.locked?, 'should be locked'
37+
38+
@lock.release!
39+
40+
assert !@lock.locked?, 'should not be locked'
41+
end
42+
43+
test 'checking the lock should increase the TTL if we hold it' do
44+
@lock.acquire!
45+
Resque.redis.setex(@lock.key, 10, @lock.value)
46+
47+
@lock.locked?
48+
49+
assert Resque.redis.ttl(@lock.key) > 10, 'TTL should have been updated'
50+
end
51+
52+
test 'checking the lock should not increase the TTL if we do not hold it' do
53+
Resque.redis.setex(@lock.key, 10, @lock.value)
54+
lock_is_not_held(@lock)
55+
56+
@lock.locked?
57+
58+
assert Resque.redis.ttl(@lock.key) <= 10,
59+
'TTL should not have been updated'
60+
end
61+
62+
test 'setting the lock timeout changes the key TTL if we hold it' do
63+
@lock.acquire!
64+
65+
@lock.stubs(:locked?).returns(true)
66+
@lock.timeout = 120
67+
ttl = Resque.redis.ttl(@lock.key)
68+
assert_send [ttl, :>, 100]
69+
70+
@lock.stubs(:locked?).returns(true)
71+
@lock.timeout = 180
72+
ttl = Resque.redis.ttl(@lock.key)
73+
assert_send [ttl, :>, 120]
74+
end
75+
end
76+
end
77+
# rubocop:enable Metrics/AbcSize
78+
# rubocop:enable Metrics/MethodLength
79+
end
80+
1081
context '#master_lock_key' do
1182
setup do
1283
@subject = Class.new { extend Resque::Scheduler::Locking }
@@ -94,14 +165,22 @@ def lock_is_not_held(lock)
94165
assert_equal @subject.master_lock.class, Resque::Scheduler::Lock::Basic
95166
end
96167

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

100171
assert_equal(
101172
@subject.master_lock.class, Resque::Scheduler::Lock::Resilient
102173
)
103174
end
104175

176+
test 'should use the resilient lock mechanism for >= Redis 2.6.12' do
177+
Resque.data_store.redis.stubs(:info).returns('redis_version' => '2.8.0')
178+
179+
assert_equal(
180+
@subject.master_lock.class, Resque::Scheduler::Lock::ResilientModern
181+
)
182+
end
183+
105184
test 'should be the master if the lock is held' do
106185
@subject.master_lock.acquire!
107186
assert @subject.master?, 'should be master'
@@ -153,52 +232,7 @@ def lock_is_not_held(lock)
153232
@lock.release!
154233
end
155234

156-
test 'you should not have the lock if someone else holds it' do
157-
lock_is_not_held(@lock)
158-
159-
assert !@lock.locked?
160-
end
161-
162-
test 'you should not be able to acquire the lock if someone ' \
163-
'else holds it' do
164-
lock_is_not_held(@lock)
165-
166-
assert !@lock.acquire!
167-
end
168-
169-
test 'the lock should receive a TTL on acquiring' do
170-
@lock.acquire!
171-
172-
assert Resque.redis.ttl(@lock.key) > 0, 'lock should expire'
173-
end
174-
175-
test 'releasing should release the master lock' do
176-
assert @lock.acquire!, 'should have acquired the master lock'
177-
assert @lock.locked?, 'should be locked'
178-
179-
@lock.release!
180-
181-
assert !@lock.locked?, 'should not be locked'
182-
end
183-
184-
test 'checking the lock should increase the TTL if we hold it' do
185-
@lock.acquire!
186-
Resque.redis.setex(@lock.key, 10, @lock.value)
187-
188-
@lock.locked?
189-
190-
assert Resque.redis.ttl(@lock.key) > 10, 'TTL should have been updated'
191-
end
192-
193-
test 'checking the lock should not increase the TTL if we do not hold it' do
194-
Resque.redis.setex(@lock.key, 10, @lock.value)
195-
lock_is_not_held(@lock)
196-
197-
@lock.locked?
198-
199-
assert Resque.redis.ttl(@lock.key) <= 10,
200-
'TTL should not have been updated'
201-
end
235+
include LockSharedTests
202236
end
203237

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

219-
test 'you should not have the lock if someone else holds it' do
220-
lock_is_not_held(@lock)
221-
222-
assert !@lock.locked?, 'you should not have the lock'
223-
end
253+
include LockSharedTests
224254

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

236-
test 'you should not be able to acquire the lock if someone ' \
237-
'else holds it' do
238-
lock_is_not_held(@lock)
239-
240-
assert !@lock.acquire!
241-
end
242-
243-
test 'the lock should receive a TTL on acquiring' do
244-
@lock.acquire!
245-
246-
assert Resque.redis.ttl(@lock.key) > 0, 'lock should expire'
247-
end
248-
249-
test 'releasing should release the master lock' do
250-
assert @lock.acquire!, 'should have acquired the master lock'
251-
assert @lock.locked?, 'should be locked'
252-
253-
@lock.release!
254-
255-
assert !@lock.locked?, 'should not be locked'
256-
end
257-
258-
test 'checking the lock should increase the TTL if we hold it' do
259-
@lock.acquire!
260-
Resque.redis.setex(@lock.key, 10, @lock.value)
261-
262-
@lock.locked?
263-
264-
assert Resque.redis.ttl(@lock.key) > 10, 'TTL should have been updated'
265-
end
266-
267-
test 'checking the lock should not increase the TTL if we do ' \
268-
'not hold it' do
269-
Resque.redis.setex(@lock.key, 10, @lock.value)
270-
lock_is_not_held(@lock)
271-
272-
@lock.locked?
273-
274-
assert Resque.redis.ttl(@lock.key) <= 10,
275-
'TTL should not have been updated'
276-
end
277-
278-
test 'setting the lock timeout changes the key TTL if we hold it' do
279-
@lock.acquire!
280-
281-
@lock.stubs(:locked?).returns(true)
282-
@lock.timeout = 120
283-
ttl = Resque.redis.ttl(@lock.key)
284-
assert_send [ttl, :>, 100]
285-
286-
@lock.stubs(:locked?).returns(true)
287-
@lock.timeout = 180
288-
ttl = Resque.redis.ttl(@lock.key)
289-
assert_send [ttl, :>, 120]
290-
end
291-
292266
test 'setting lock timeout is a noop if not held' do
293267
@lock.acquire!
294268
@lock.timeout = 100
@@ -325,3 +299,22 @@ def lock_is_not_held(lock)
325299
end
326300
end
327301
end
302+
303+
context 'Resque::Scheduler::Lock::ResilientModern' do
304+
include LockTestHelper
305+
306+
if !Resque::Scheduler.supports_get_x_options?
307+
puts '*** Skipping Resque::Scheduler::Lock::ResilientModern ' \
308+
'tests, as they require Redis >= 2.6.2.'
309+
else
310+
setup do
311+
@lock = Resque::Scheduler::Lock::ResilientModern.new('test_resilient_modern_lock')
312+
end
313+
314+
teardown do
315+
@lock.release!
316+
end
317+
318+
include LockSharedTests
319+
end
320+
end

0 commit comments

Comments
 (0)