Skip to content

Commit 96930f2

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

File tree

5 files changed

+124
-113
lines changed

5 files changed

+124
-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 non-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: 98 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,73 @@ def lock_is_not_held(lock)
77
end
88
end
99

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

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

100167
assert_equal(
101168
@subject.master_lock.class, Resque::Scheduler::Lock::Resilient
102169
)
103170
end
104171

172+
test 'should use the resilient lock mechanism for >= Redis 2.6.12' do
173+
Resque.data_store.redis.stubs(:info).returns('redis_version' => '2.8.0')
174+
175+
assert_equal(
176+
@subject.master_lock.class, Resque::Scheduler::Lock::ResilientModern
177+
)
178+
end
179+
105180
test 'should be the master if the lock is held' do
106181
@subject.master_lock.acquire!
107182
assert @subject.master?, 'should be master'
@@ -153,52 +228,7 @@ def lock_is_not_held(lock)
153228
@lock.release!
154229
end
155230

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
231+
include LockSharedTests
202232
end
203233

204234
context 'Resque::Scheduler::Lock::Resilient' do
@@ -216,11 +246,7 @@ def lock_is_not_held(lock)
216246
@lock.release!
217247
end
218248

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
249+
include LockSharedTests
224250

225251
test 'refreshes sha cache when the sha cannot be found on ' \
226252
'the redis server' do
@@ -233,62 +259,6 @@ def lock_is_not_held(lock)
233259
assert_false @lock.acquire!
234260
end
235261

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-
292262
test 'setting lock timeout is a noop if not held' do
293263
@lock.acquire!
294264
@lock.timeout = 100
@@ -325,3 +295,22 @@ def lock_is_not_held(lock)
325295
end
326296
end
327297
end
298+
299+
context 'Resque::Scheduler::Lock::ResilientModern' do
300+
include LockTestHelper
301+
302+
if !Resque::Scheduler.supports_get_x_options?
303+
puts '*** Skipping Resque::Scheduler::Lock::ResilientModern ' \
304+
'tests, as they require Redis >= 2.6.2.'
305+
else
306+
setup do
307+
@lock = Resque::Scheduler::Lock::ResilientModern.new('test_resilient_modern_lock')
308+
end
309+
310+
teardown do
311+
@lock.release!
312+
end
313+
314+
include LockSharedTests
315+
end
316+
end

0 commit comments

Comments
 (0)