resque-lonely_job 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # Resque::LonelyJob
2
2
 
3
- A [Resque](https://github.com/defunkt/resque) plugin. Requires Resque 1.20.0.
3
+ A [Resque](https://github.com/defunkt/resque) plugin. Requires Resque >= 1.20.0.
4
4
 
5
5
  Ensures that for a given queue, only one worker is working on a job at any given
6
- time.
6
+ time.
7
7
 
8
8
  This differs from [resque-lock](from https://github.com/defunkt/resque-lock) and
9
9
  [resque-loner](http://github.com/jayniz/resque-loner) in that the same job may
10
10
  be queued multiple times but you're guaranteed that first job queued will run to
11
- completion before subsequent jobs are run.
11
+ completion before subsequent jobs are run. In other words, job ordering is
12
+ preserved.
12
13
 
13
14
  ## Installation
14
15
 
@@ -26,7 +27,7 @@ Or install it yourself as:
26
27
 
27
28
  ## Usage
28
29
 
29
- Example #1:
30
+ #### Example #1
30
31
 
31
32
  require 'resque/plugins/lonely_job'
32
33
 
@@ -41,7 +42,9 @@ Example #1:
41
42
  end
42
43
  end
43
44
 
44
- Example #2: Let's say you want the serial constraint to apply at a more granular
45
+ #### Example #2
46
+
47
+ Let's say you want the serial constraint to apply at a more granular
45
48
  level. Instead of applying at the queue level, you can overwrite the .redis\_key
46
49
  method.
47
50
 
@@ -61,9 +64,40 @@ method.
61
64
 
62
65
  def self.perform(account_id, *args)
63
66
  # only one at a time in this block, no parallelism allowed for this
64
- # particular queue
67
+ # particular redis_key
65
68
  end
66
69
  end
70
+
71
+ *NOTE*: Without careful consideration of your problem domain, worker starvation
72
+ and/or unfairness is possible for jobs in this example. Imagine a scenario
73
+ where you have three jobs in the queue with two resque workers:
74
+
75
+ +---------------------------------------------------+
76
+ | :serial_work |
77
+ |---------------------------------------------------|
78
+ | | | | |
79
+ | redis_key: | redis_key: | redis_key: | ... |
80
+ | A | A | B | |
81
+ | | | | |
82
+ | job 1 | job 2 | job 3 | |
83
+ +---------------------------------------------------+
84
+ ^
85
+ |
86
+ Possible starvation +-----------+
87
+ for this job and
88
+ subsequent ones
89
+
90
+
91
+ When the first worker grabs job 1, it'll acquire the mutex for processing
92
+ redis\_key A. The second worker tries to grab the next job off the queue but
93
+ is unable to acquire the mutex for redis\_key A so it places job 2 back at the
94
+ head of the :serial\_work queue. Until worker 1 completes job 1 and releases
95
+ the mutex for redis\_key A, no work will be done in this queue.
96
+
97
+ This issue may be avoided by employing dynamic queues,
98
+ http://blog.kabisa.nl/2010/03/16/dynamic-queue-assignment-for-resque-jobs/,
99
+ where the queue is a one to one mapping to the redis\_key.
100
+
67
101
  ## Contributing
68
102
 
69
103
  1. Fork it
@@ -1,7 +1,7 @@
1
1
  module Resque
2
2
  module Plugins
3
3
  module LonelyJob
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.2"
5
5
  end
6
6
  end
7
7
  end
@@ -23,10 +23,18 @@ module Resque
23
23
  Resque.redis.del(redis_key(*args))
24
24
  end
25
25
 
26
+ # Unfortunately, there's not a Resque interface for lpush so we have to
27
+ # role our own. This is based on Resque.push but we don't need to
28
+ # call Resque.watch_queue as the queue should already exist if we're
29
+ # unable to get the lock.
30
+ def lpush(*args)
31
+ Resque.redis.lpush("queue:#{Resque.queue_from_class(self)}", Resque.encode(class: self, args: args))
32
+ end
33
+
26
34
  def before_perform(*args)
27
35
  unless can_lock_queue?(*args)
28
- # can't get the lock, so place self at the end of the queue
29
- Resque.enqueue(self, *args)
36
+ # can't get the lock, so place at the front of the queue
37
+ lpush(*args)
30
38
 
31
39
  # and don't perform
32
40
  raise Resque::Job::DontPerform
@@ -65,18 +65,19 @@ describe Resque::Plugins::LonelyJob do
65
65
  -> { job.perform }.should raise_error(Exception)
66
66
  end
67
67
 
68
- it 'should place self at the end of the queue if unable to acquire the lock' do
69
- Resque.size(:serial_work).should == 0
70
-
71
- job = Resque::Job.new(:serial_work, { 'class' => 'SerialJob', 'args' => %w[account_one job_one] })
68
+ it 'should place self at the beginning of the queue if unable to acquire the lock' do
69
+ job1 = Resque::Job.create(:serial_work, 'SerialJob', %w[account_one job_one])
70
+ job2 = Resque::Job.create(:serial_work, 'SerialJob', %w[account_one job_two])
72
71
 
73
72
  SerialJob.should_receive(:can_lock_queue?).and_return(false)
74
73
 
75
74
  # perform returns false when DontPerform exception is raised in
76
75
  # before_perform callback
77
- job.perform.should be_false
76
+ job1 = Resque.reserve(:serial_work)
77
+ job1.perform.should be_false
78
78
 
79
- Resque.size(:serial_work).should == 1
79
+ first_queue_element = Resque.reserve(:serial_work)
80
+ first_queue_element.should == job1
80
81
  end
81
82
  end
82
83
 
@@ -110,18 +111,19 @@ describe Resque::Plugins::LonelyJob do
110
111
  -> { job.perform }.should raise_error(Exception)
111
112
  end
112
113
 
113
- it 'should place self at the end of the queue if unable to acquire the lock' do
114
- Resque.size(:serial_work).should == 0
115
-
116
- job = Resque::Job.new(:serial_work, { 'class' => 'SerialJobWithCustomRedisKey', 'args' => %w[account_one job_one] })
114
+ it 'should place self at the beginning of the queue if unable to acquire the lock' do
115
+ job1 = Resque::Job.create(:serial_work, 'SerialJobWithCustomRedisKey', %w[account_one job_one])
116
+ job2 = Resque::Job.create(:serial_work, 'SerialJobWithCustomRedisKey', %w[account_one job_two])
117
117
 
118
118
  SerialJobWithCustomRedisKey.should_receive(:can_lock_queue?).and_return(false)
119
119
 
120
120
  # perform returns false when DontPerform exception is raised in
121
121
  # before_perform callback
122
- job.perform.should be_false
122
+ job1 = Resque.reserve(:serial_work)
123
+ job1.perform.should be_false
123
124
 
124
- Resque.size(:serial_work).should == 1
125
+ first_queue_element = Resque.reserve(:serial_work)
126
+ first_queue_element.should == job1
125
127
  end
126
128
  end
127
129
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resque-lonely_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-05-21 00:00:00.000000000 Z
12
+ date: 2012-05-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: resque
16
- requirement: &70323053547940 !ruby/object:Gem::Requirement
16
+ requirement: &70163618247460 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 1.20.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70323053547940
24
+ version_requirements: *70163618247460
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: mock_redis
27
- requirement: &70323053547380 !ruby/object:Gem::Requirement
27
+ requirement: &70163618260080 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 0.4.1
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70323053547380
35
+ version_requirements: *70163618260080
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rake
38
- requirement: &70323053546980 !ruby/object:Gem::Requirement
38
+ requirement: &70163618269840 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70323053546980
46
+ version_requirements: *70163618269840
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rspec
49
- requirement: &70323053546520 !ruby/object:Gem::Requirement
49
+ requirement: &70163618268580 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,7 +54,7 @@ dependencies:
54
54
  version: '0'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70323053546520
57
+ version_requirements: *70163618268580
58
58
  description: ! "Ensures that for a given queue, only one worker is working on a job
59
59
  at any given time.\n\nExample:\n\n require 'resque/plugins/lonely_job'\n\n class
60
60
  StrictlySerialJob\n extend Resque::Plugins::LonelyJob\n\n @queue = :serial_work\n\n