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 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 queue jobs when you locked them. Resque-workers-lock locks on a workers-level and will requeue the locked jobs. When the enqueue_lock is set to false, this plugin will not prevent you to queue 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! Also, this plugin will still offer you the ability to lock at enqueue-level just like resque-lock does (see example).
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
- # Lock method has the same arguments as the self.perform
26
- def self.lock_workers(domain, arg2, arg3)
27
- return domain
28
- end
29
-
30
- # Turn off standard resque-lock functionality
31
- def self.lock_enqueue(domain, arg2, arg3)
32
- false
33
- end
34
-
35
- # Perform method with some arguments
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 requeue'ed there is a small delay (1 second by default) before the worker places the job actually 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.
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
- 5.0
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
@@ -8,4 +8,8 @@ Rake::TestTask.new do |t|
8
8
  t.libs << 'lib'
9
9
  t.pattern = 'test/**/*_test.rb'
10
10
  t.verbose = false
11
+ end
12
+
13
+ task "resque:setup" do
14
+ require_relative "test/unique_job"
11
15
  end
@@ -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('enqueuelock:*').collect { |x| Resque.redis.del(x) }.count
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 queue lock key
18
- def lock_enqueue(*args)
19
- "#{name}-#{args.to_s}"
20
- end
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 before_perform_lock(*args)
38
+ def before_perform_workers_lock(*args)
54
39
  if lock_workers(*args)
55
- nx = Resque.redis.setnx(get_lock_workers(*args), true)
56
- if nx == false
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 after_dequeue_lock(*args)
66
- # Clear the lock when dequeueed
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 around_perform_lock(*args)
71
- begin
72
- yield
73
- ensure
74
- # Clear the lock. (even with errors)
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 on_failure_lock(exception, *args)
61
+ def on_failure_workers_lock(exception, *args)
81
62
  # Clear the lock on DirtyExit
82
- Resque.redis.del(get_lock_workers(*args))
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
- class LockTest < Test::Unit::TestCase
6
- class UniqueJob
7
- extend Resque::Plugins::Workers::Lock
8
- @queue = :lock_test
6
+ require_relative 'unique_job'
9
7
 
10
- def self.lock_workers(*)
11
- self.name
12
- end
8
+ class LockTest < Test::Unit::TestCase
9
+
13
10
 
14
- def self.perform params
15
- File.open(params['output_file'], 'a') do |output_file|
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.times do
98
- return if yield
99
- sleep 1
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
@@ -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.6'
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-11-26 00:00:00.000000000 Z
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: