resque-workers-lock 1.6 → 1.7
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +41 -15
- data/Rakefile +4 -0
- data/lib/resque/plugins/workers/lock.rb +19 -39
- data/test/lock_test.rb +55 -24
- data/test/unique_job.rb +26 -0
- metadata +4 -2
data/README.md
CHANGED
@@ -12,48 +12,74 @@ If resque jobs have the same lock applied this means that those jobs cannot be p
|
|
12
12
|
By default the lock is the instance name + arguments (just like the classic resque-lock). Override this lock to lock on specific arguments.
|
13
13
|
|
14
14
|
## How does it differ from resque-lock?
|
15
|
-
Resque-lock will not let you
|
15
|
+
Resque-lock will not let you enqueue jobs when you locked them. Resque-workers-lock locks on a workers-level and will requeue the locked jobs. If a worker takes on a job that is already being processed by another worker it will put the job back up in the queue!
|
16
16
|
|
17
17
|
## Example
|
18
18
|
This example shows how you can use the workers-lock to prevent two jobs with the same domain to be processed simultaneously.
|
19
|
+
|
19
20
|
``` ruby
|
20
21
|
require 'resque/plugins/workers/lock'
|
21
22
|
|
22
23
|
class Parser
|
23
24
|
extend Resque::Plugins::Workers::Lock
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
26
|
+
# Lock method has the same arguments as the self.perform
|
27
|
+
def self.lock_workers(domain, arg2, arg3)
|
28
|
+
return domain
|
29
|
+
end
|
30
|
+
|
31
|
+
# This is the time in seconds that the worker lock should be considered valid.
|
32
|
+
# The default is one hour (3600 seconds).
|
33
|
+
def self.worker_lock_timeout(domain, arg2, arg3)
|
34
|
+
3600
|
35
|
+
end
|
36
|
+
|
37
|
+
# Perform method with some arguments
|
36
38
|
def self.perform(domain, arg2, arg3)
|
37
39
|
# do the work
|
38
40
|
end
|
39
41
|
end
|
40
42
|
```
|
43
|
+
|
41
44
|
In this example `domain` is used to specify certain types of jobs that are not allowed to run at the same time. For example: if you create three jobs with the domain argument google.com, google.com and yahoo.com, the two google.com jobs will never run at the same time.
|
42
45
|
|
43
46
|
## One queue
|
44
47
|
Best results with one big queue instead of multiple queues.
|
45
48
|
|
46
49
|
## Requeue loop
|
47
|
-
When a job is
|
50
|
+
When a job is requeued there is a small delay (1 second by default) before the worker places the job back in the queue. Let's say you have two jobs left, and one job is taking 15 seconds on the first worker and the other similar job is being blocked by the second worker. The second worker will continuously try to put the job back in the queue and it will try to process it again (racing for 15 seconds untill the other job has finished). This only happens when there are no other (not locked) jobs in the queue.
|
48
51
|
|
49
52
|
To overwrite this delay in your class:
|
50
53
|
``` ruby
|
51
54
|
def self.requeue_perform_delay
|
52
|
-
|
55
|
+
5.0
|
53
56
|
end
|
54
57
|
```
|
55
58
|
|
56
59
|
Please note that setting this value to 5 seconds will keep the worker idle for 5 seconds when the job is locked.
|
57
60
|
|
58
|
-
## Possibilities to prevent the loop
|
61
|
+
## Possibilities to prevent the loop
|
59
62
|
Do a delayed resque (re)queue. However, this will have approximately the same results and will require a large extra chunk of code and rake configurations.
|
63
|
+
|
64
|
+
## Run workers for the test
|
65
|
+
To run the tests using `rake test` properly, make sure there are a few workers running:
|
66
|
+
```
|
67
|
+
$ redis-server
|
68
|
+
$ VVERBOSE=1 COUNT=4 QUEUE=* rake resque:work
|
69
|
+
```
|
70
|
+
|
71
|
+
```
|
72
|
+
➜ resque-workers-lock git:(master) ✗ rake test
|
73
|
+
Run options:
|
74
|
+
|
75
|
+
# Running tests:
|
76
|
+
|
77
|
+
...
|
78
|
+
|
79
|
+
Finished tests in 10.426519s, 0.2877 tests/s, 0.3836 assertions/s.
|
80
|
+
|
81
|
+
3 tests, 4 assertions, 0 failures, 0 errors, 0 skips
|
82
|
+
```
|
83
|
+
|
84
|
+
## Authors/Contributors
|
85
|
+
[nicholaides](https://github.com/nicholaides)
|
data/Rakefile
CHANGED
@@ -4,9 +4,7 @@ module Resque
|
|
4
4
|
alias_method :orig_remove_queue, :remove_queue
|
5
5
|
|
6
6
|
def remove_queue(queue)
|
7
|
-
Resque.redis.keys('
|
8
|
-
Resque.redis.keys('workerslock:*').collect { |x| Resque.redis.del(x) }.count
|
9
|
-
|
7
|
+
Resque.redis.keys('workerslock:*').each{ |x| Resque.redis.del(x) }
|
10
8
|
orig_remove_queue(queue)
|
11
9
|
end
|
12
10
|
|
@@ -14,13 +12,11 @@ module Resque
|
|
14
12
|
module Workers
|
15
13
|
module Lock
|
16
14
|
|
17
|
-
# Override in your job to control the
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
def get_lock_enqueue(*args)
|
23
|
-
"enqueuelock:"+lock_enqueue(*args).to_s
|
15
|
+
# Override in your job to control the worker lock experiation time. This
|
16
|
+
# is the time in seconds that the lock should be considered valid. The
|
17
|
+
# default is one hour (3600 seconds).
|
18
|
+
def worker_lock_timeout(*)
|
19
|
+
3600
|
24
20
|
end
|
25
21
|
|
26
22
|
# Override in your job to control the workers lock key.
|
@@ -37,50 +33,34 @@ module Resque
|
|
37
33
|
1.0
|
38
34
|
end
|
39
35
|
|
40
|
-
|
41
|
-
# Called with the job args before a job is placed on the queue.
|
42
|
-
# If the hook returns false, the job will not be placed on the queue.
|
43
|
-
def before_enqueue_lock(*args)
|
44
|
-
if lock_enqueue(*args) == false
|
45
|
-
return true
|
46
|
-
else
|
47
|
-
return Resque.redis.setnx(get_lock_enqueue(*args), true)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
36
|
# Called with the job args before perform.
|
52
37
|
# If it raises Resque::Job::DontPerform, the job is aborted.
|
53
|
-
def
|
38
|
+
def before_perform_workers_lock(*args)
|
54
39
|
if lock_workers(*args)
|
55
|
-
|
56
|
-
|
40
|
+
if Resque.redis.setnx(get_lock_workers(*args), true)
|
41
|
+
Resque.redis.expire(get_lock_workers(*args), worker_lock_timeout(*args))
|
42
|
+
else
|
57
43
|
sleep(requeue_perform_delay)
|
58
|
-
Resque.redis.del(get_lock_enqueue(*args))
|
59
44
|
Resque.enqueue(self, *args)
|
60
45
|
raise Resque::Job::DontPerform
|
61
46
|
end
|
62
47
|
end
|
63
48
|
end
|
64
49
|
|
65
|
-
def
|
66
|
-
|
67
|
-
Resque.redis.del(get_lock_enqueue(*args))
|
50
|
+
def clear_workers_lock(*args)
|
51
|
+
Resque.redis.del(get_lock_workers(*args))
|
68
52
|
end
|
69
53
|
|
70
|
-
def
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
Resque.redis.del(get_lock_workers(*args))
|
76
|
-
Resque.redis.del(get_lock_enqueue(*args))
|
77
|
-
end
|
54
|
+
def around_perform_workers_lock(*args)
|
55
|
+
yield
|
56
|
+
ensure
|
57
|
+
# Clear the lock. (even with errors)
|
58
|
+
clear_workers_lock(*args)
|
78
59
|
end
|
79
60
|
|
80
|
-
def
|
61
|
+
def on_failure_workers_lock(exception, *args)
|
81
62
|
# Clear the lock on DirtyExit
|
82
|
-
|
83
|
-
Resque.redis.del(get_lock_enqueue(*args))
|
63
|
+
clear_workers_lock(*args)
|
84
64
|
end
|
85
65
|
|
86
66
|
end
|
data/test/lock_test.rb
CHANGED
@@ -1,24 +1,15 @@
|
|
1
1
|
require 'test/unit'
|
2
|
-
require 'resque/plugins/workers/lock'
|
2
|
+
require File.expand_path('../../lib/resque/plugins/workers/lock', __FILE__)
|
3
3
|
require 'tempfile'
|
4
|
+
require 'timeout'
|
4
5
|
|
5
|
-
|
6
|
-
class UniqueJob
|
7
|
-
extend Resque::Plugins::Workers::Lock
|
8
|
-
@queue = :lock_test
|
6
|
+
require_relative 'unique_job'
|
9
7
|
|
10
|
-
|
11
|
-
|
12
|
-
end
|
8
|
+
class LockTest < Test::Unit::TestCase
|
9
|
+
|
13
10
|
|
14
|
-
|
15
|
-
|
16
|
-
output_file.puts params['job']
|
17
|
-
output_file.flush
|
18
|
-
sleep 1
|
19
|
-
output_file.puts params['job']
|
20
|
-
end
|
21
|
-
end
|
11
|
+
def setup
|
12
|
+
Resque.redis.del(UniqueJob.get_lock_workers)
|
22
13
|
end
|
23
14
|
|
24
15
|
def test_lint
|
@@ -31,8 +22,49 @@ class LockTest < Test::Unit::TestCase
|
|
31
22
|
assert_locking_works_with jobs: 2, workers: 2
|
32
23
|
end
|
33
24
|
|
25
|
+
def test_worker_locks_timeout
|
26
|
+
output_file = Tempfile.new 'output_file'
|
27
|
+
|
28
|
+
Resque.enqueue UniqueJob, job: 'interrupted-job', output_file: output_file.path, sleep: 1000
|
29
|
+
|
30
|
+
worker_pid = start_worker
|
31
|
+
wait_until(10){ lock_has_been_acquired }
|
32
|
+
kill_worker(worker_pid)
|
33
|
+
|
34
|
+
Resque.enqueue UniqueJob, job: 'completing-job', output_file: output_file.path, sleep: 0
|
35
|
+
process_jobs workers: 1, timeout: UniqueJob.worker_lock_timeout + 2
|
36
|
+
|
37
|
+
lines = File.readlines(output_file).map(&:chomp)
|
38
|
+
assert_equal ['starting interrupted-job', 'starting completing-job', 'finished completing-job'], lines
|
39
|
+
end
|
40
|
+
|
34
41
|
private
|
35
42
|
|
43
|
+
def lock_has_been_acquired
|
44
|
+
Resque.redis.exists(UniqueJob.get_lock_workers)
|
45
|
+
end
|
46
|
+
|
47
|
+
def kill_worker(worker_pid)
|
48
|
+
Process.kill("TERM", worker_pid)
|
49
|
+
Process.waitpid(worker_pid)
|
50
|
+
end
|
51
|
+
|
52
|
+
def start_worker
|
53
|
+
fork.tap do |pid|
|
54
|
+
if !pid
|
55
|
+
worker = Resque::Worker.new('*')
|
56
|
+
worker.term_child = true
|
57
|
+
worker.reconnect
|
58
|
+
worker.work(0.5)
|
59
|
+
exit!
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def assert_worker_lock_exists(job_class, *args)
|
65
|
+
assert Resque.redis.exists(job_class.get_lock_workers(*args), "lock does not exist")
|
66
|
+
end
|
67
|
+
|
36
68
|
def assert_locking_works_with options
|
37
69
|
jobs = (1..options[:jobs]).map{|job| "Job #{job}" }
|
38
70
|
output_file = Tempfile.new 'output_file'
|
@@ -45,10 +77,8 @@ class LockTest < Test::Unit::TestCase
|
|
45
77
|
|
46
78
|
lines = File.readlines(output_file).map(&:chomp)
|
47
79
|
lines.each_slice(2) do |a,b|
|
48
|
-
assert_equal a,b, "#{a} was interrupted by #{b}"
|
80
|
+
assert_equal a.split.last,b.split.last, "#{a} was interrupted by #{b}"
|
49
81
|
end
|
50
|
-
ensure
|
51
|
-
output_file.close
|
52
82
|
end
|
53
83
|
|
54
84
|
def process_jobs options
|
@@ -67,6 +97,7 @@ class LockTest < Test::Unit::TestCase
|
|
67
97
|
else
|
68
98
|
pids = [] # Don't kill from child's ensure
|
69
99
|
worker = Resque::Worker.new('*')
|
100
|
+
worker.term_child = true
|
70
101
|
worker.reconnect
|
71
102
|
worker.work(0.5)
|
72
103
|
exit!
|
@@ -94,11 +125,11 @@ class LockTest < Test::Unit::TestCase
|
|
94
125
|
end
|
95
126
|
|
96
127
|
def wait_until(timeout)
|
97
|
-
timeout
|
98
|
-
|
99
|
-
|
128
|
+
Timeout::timeout(timeout) do
|
129
|
+
loop do
|
130
|
+
return if yield
|
131
|
+
sleep 1
|
132
|
+
end
|
100
133
|
end
|
101
|
-
|
102
|
-
raise "Timout occured"
|
103
134
|
end
|
104
135
|
end
|
data/test/unique_job.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require File.expand_path('../../lib/resque/plugins/workers/lock', __FILE__)
|
2
|
+
|
3
|
+
class UniqueJob
|
4
|
+
extend Resque::Plugins::Workers::Lock
|
5
|
+
@queue = :lock_test
|
6
|
+
|
7
|
+
def self.worker_lock_timeout(*)
|
8
|
+
5
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.lock_workers(*)
|
12
|
+
self.name
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.append_output filename, string
|
16
|
+
File.open(filename, 'a') do |output_file|
|
17
|
+
output_file.puts string
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.perform params
|
22
|
+
append_output params['output_file'], "starting #{params['job']}"
|
23
|
+
sleep(params['sleep'] || 1)
|
24
|
+
append_output params['output_file'], "finished #{params['job']}"
|
25
|
+
end
|
26
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: resque-workers-lock
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '1.
|
4
|
+
version: '1.7'
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Bart Olsthoorn
|
9
|
+
- Mike Nicholaides
|
9
10
|
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
|
-
date: 2012-
|
13
|
+
date: 2012-12-19 00:00:00.000000000 Z
|
13
14
|
dependencies:
|
14
15
|
- !ruby/object:Gem::Dependency
|
15
16
|
name: resque
|
@@ -59,6 +60,7 @@ files:
|
|
59
60
|
- LICENSE
|
60
61
|
- lib/resque/plugins/workers/lock.rb
|
61
62
|
- test/lock_test.rb
|
63
|
+
- test/unique_job.rb
|
62
64
|
homepage: http://github.com/bartolsthoorn/resque-workers-lock
|
63
65
|
licenses: []
|
64
66
|
post_install_message:
|