qless 0.9.2 → 0.9.3

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.
Files changed (41) hide show
  1. data/Gemfile +2 -0
  2. data/README.md +42 -3
  3. data/Rakefile +26 -2
  4. data/{bin → exe}/qless-web +3 -2
  5. data/lib/qless.rb +55 -28
  6. data/lib/qless/config.rb +1 -3
  7. data/lib/qless/job.rb +127 -22
  8. data/lib/qless/job_reservers/round_robin.rb +3 -1
  9. data/lib/qless/job_reservers/shuffled_round_robin.rb +14 -0
  10. data/lib/qless/lua_script.rb +42 -0
  11. data/lib/qless/middleware/redis_reconnect.rb +24 -0
  12. data/lib/qless/middleware/retry_exceptions.rb +43 -0
  13. data/lib/qless/middleware/sentry.rb +70 -0
  14. data/lib/qless/qless-core/cancel.lua +89 -59
  15. data/lib/qless/qless-core/complete.lua +16 -1
  16. data/lib/qless/qless-core/config.lua +12 -0
  17. data/lib/qless/qless-core/deregister_workers.lua +12 -0
  18. data/lib/qless/qless-core/fail.lua +24 -14
  19. data/lib/qless/qless-core/heartbeat.lua +2 -1
  20. data/lib/qless/qless-core/pause.lua +18 -0
  21. data/lib/qless/qless-core/pop.lua +24 -3
  22. data/lib/qless/qless-core/put.lua +14 -1
  23. data/lib/qless/qless-core/qless-lib.lua +2354 -0
  24. data/lib/qless/qless-core/qless.lua +1862 -0
  25. data/lib/qless/qless-core/retry.lua +1 -1
  26. data/lib/qless/qless-core/unfail.lua +54 -0
  27. data/lib/qless/qless-core/unpause.lua +12 -0
  28. data/lib/qless/queue.rb +45 -21
  29. data/lib/qless/server.rb +38 -39
  30. data/lib/qless/server/static/css/docs.css +21 -1
  31. data/lib/qless/server/views/_job.erb +5 -5
  32. data/lib/qless/server/views/overview.erb +14 -9
  33. data/lib/qless/subscriber.rb +48 -0
  34. data/lib/qless/version.rb +1 -1
  35. data/lib/qless/wait_until.rb +19 -0
  36. data/lib/qless/worker.rb +243 -33
  37. metadata +49 -30
  38. data/bin/install_phantomjs +0 -7
  39. data/bin/qless-campfire +0 -106
  40. data/bin/qless-growl +0 -99
  41. data/lib/qless/lua.rb +0 -25
data/Gemfile CHANGED
@@ -6,3 +6,5 @@ gemspec
6
6
  group :development do
7
7
  gem 'debugger', :platform => :mri
8
8
  end
9
+
10
+ gem 'thin' # needed by qless-web binary
data/README.md CHANGED
@@ -20,7 +20,7 @@ put in. So if a worker is working on a job, and you move it, the worker's reques
20
20
  complete the job will be ignored.
21
21
 
22
22
  A job can be `canceled`, which means it disappears into the ether, and we'll never
23
- pay it any mind every again. A job can be `dropped`, which is when a worker fails
23
+ pay it any mind ever again. A job can be `dropped`, which is when a worker fails
24
24
  to heartbeat or complete the job in a timely fashion, or a job can be `failed`,
25
25
  which is when a host recognizes some systematically problematic state about the
26
26
  job. A worker should only fail a job if the error is likely not a transient one;
@@ -189,17 +189,20 @@ Then run the `qless:work` rake task:
189
189
  rake qless:work
190
190
  ```
191
191
 
192
- The following signals are supported:
192
+ The following signals are supported in the parent process:
193
193
 
194
194
  * TERM: Shutdown immediately, stop processing jobs.
195
195
  * INT: Shutdown immediately, stop processing jobs.
196
196
  * QUIT: Shutdown after the current job has finished processing.
197
197
  * USR1: Kill the forked child immediately, continue processing jobs.
198
- * USR2: Don't process any new jobs
198
+ * USR2: Don't process any new jobs, and dump the current backtrace.
199
199
  * CONT: Start processing jobs again after a USR2
200
200
 
201
201
  You should send these to the master process, not the child.
202
202
 
203
+ The child process supports the `USR2` signal, whch causes it to
204
+ dump its current backtrace.
205
+
203
206
  Workers also support middleware modules that can be used to inject
204
207
  logic before, after or around the processing of a single job in
205
208
  the child process. This can be useful, for example, when you need to
@@ -228,6 +231,42 @@ Qless::Worker.class_eval do
228
231
  end
229
232
  ```
230
233
 
234
+ Per-Job Middlewares
235
+ ===================
236
+
237
+ Qless also supports middleware on a per-job basis, when you have some
238
+ orthogonal logic to run in the context of some (but not all) jobs.
239
+
240
+ Per-job middlewares are defined the same as worker middlewares:
241
+
242
+ ``` ruby
243
+ module ReEstablishDBConnection
244
+ def around_perform(job)
245
+ MyORM.establish_connection
246
+ super
247
+ end
248
+ end
249
+ ```
250
+
251
+ To add them to a job class, you first have to make your job class
252
+ middleware-enabled by extending it with
253
+ `Qless::Job::SupportsMiddleware`, then extend your middleware
254
+ modules:
255
+
256
+ ``` ruby
257
+ class MyJobClass
258
+ extend Qless::Job::SupportsMiddleware
259
+ extend ReEstablishDBConnection
260
+ extend MyOtherAwesomeMiddleware
261
+
262
+ def self.perform(job)
263
+ end
264
+ end
265
+ ```
266
+
267
+ Note that `Qless::Job::SupportsMiddleware` must be extended onto your
268
+ job class _before_ any other middleware modules.
269
+
231
270
  Web Interface
232
271
  =============
233
272
 
data/Rakefile CHANGED
@@ -11,7 +11,8 @@ RSpec::Core::RakeTask.new(:spec) do |t|
11
11
  end
12
12
 
13
13
  # TODO: bump this up as test coverage increases. It was 90.29 when I last updated it on 2012-05-21.
14
- min_coverage_threshold = 90.0
14
+ # On travis where we skip JS tests, it's at 83.9 on 2013-01-15
15
+ min_coverage_threshold = 83.5
15
16
  desc "Checks the spec coverage and fails if it is less than #{min_coverage_threshold}%"
16
17
  task :check_coverage do
17
18
  percent = File.read("./coverage/coverage_percent.txt").to_f
@@ -24,5 +25,28 @@ end
24
25
 
25
26
  task default: [:spec, :check_coverage]
26
27
 
27
-
28
28
  require 'qless/tasks'
29
+
30
+ namespace :qless do
31
+ task :setup do
32
+ require 'qless'
33
+ queue = Qless::Client.new.queues["example"]
34
+ queue.client.redis.flushdb
35
+
36
+ ENV['QUEUES'] = queue.name
37
+ ENV['VVERBOSE'] = '1'
38
+
39
+ class ExampleJob
40
+ def self.perform(job)
41
+ sleep_time = job.data.fetch("sleep")
42
+ print "Sleeping for #{sleep_time}..."
43
+ sleep sleep_time
44
+ puts "done"
45
+ end
46
+ end
47
+
48
+ 20.times do |i|
49
+ queue.put(ExampleJob, sleep: i)
50
+ end
51
+ end
52
+ end
@@ -9,8 +9,9 @@ rescue LoadError
9
9
  end
10
10
 
11
11
  require 'qless/server'
12
+ client = Qless::Client.new
12
13
 
13
- Vegas::Runner.new(Qless::Server, 'qless-web', {
14
+ Vegas::Runner.new(Qless::Server.new(client), 'qless-web', {
14
15
  :before_run => lambda {|v|
15
16
  path = (ENV['RESQUECONFIG'] || v.args.first)
16
17
  load path.to_s.strip if path
@@ -20,4 +21,4 @@ Vegas::Runner.new(Qless::Server, 'qless-web', {
20
21
  # runner.logger.info "Using Redis connection '#{redis_conf}'"
21
22
  # Resque.redis = redis_conf
22
23
  # }
23
- end
24
+ end
@@ -3,15 +3,28 @@ require "redis"
3
3
  require "json"
4
4
  require "securerandom"
5
5
 
6
+ module Qless
7
+ # Define our error base class before requiring the other
8
+ # files so they can define subclasses.
9
+ Error = Class.new(StandardError)
10
+
11
+ # to maintain backwards compatibility with v2.x of that gem we need this constant because:
12
+ # * (lua.rb) the #evalsha method signature changed between v2.x and v3.x of the redis ruby gem
13
+ # * (worker.rb) in v3.x you have to reconnect to the redis server after forking the process
14
+ USING_LEGACY_REDIS_VERSION = ::Redis::VERSION.to_f < 3.0
15
+ end
16
+
6
17
  require "qless/version"
7
18
  require "qless/config"
8
19
  require "qless/queue"
9
20
  require "qless/job"
10
- require "qless/lua"
21
+ require "qless/lua_script"
11
22
 
12
23
  module Qless
13
24
  extend self
14
25
 
26
+ UnsupportedRedisVersionError = Class.new(Error)
27
+
15
28
  def generate_jid
16
29
  SecureRandom.uuid.gsub('-', '')
17
30
  end
@@ -27,27 +40,25 @@ module Qless
27
40
  @worker_name ||= [Socket.gethostname, Process.pid.to_s].join('-')
28
41
  end
29
42
 
30
- class UnsupportedRedisVersionError < StandardError; end
31
-
32
43
  class ClientJobs
33
44
  def initialize(client)
34
45
  @client = client
35
46
  end
36
-
47
+
37
48
  def complete(offset=0, count=25)
38
49
  @client._jobs.call([], ['complete', offset, count])
39
50
  end
40
-
51
+
41
52
  def tracked
42
53
  results = JSON.parse(@client._track.call([], []))
43
54
  results['jobs'] = results['jobs'].map { |j| Job.new(@client, j) }
44
55
  results
45
56
  end
46
-
57
+
47
58
  def tagged(tag, offset=0, count=25)
48
59
  JSON.parse(@client._tag.call([], ['get', tag, offset, count]))
49
60
  end
50
-
61
+
51
62
  def failed(t=nil, start=0, limit=25)
52
63
  if not t
53
64
  JSON.parse(@client._failed.call([], []))
@@ -57,7 +68,7 @@ module Qless
57
68
  results
58
69
  end
59
70
  end
60
-
71
+
61
72
  def [](id)
62
73
  results = @client._get.call([], [id])
63
74
  if results.nil?
@@ -70,41 +81,41 @@ module Qless
70
81
  Job.new(@client, JSON.parse(results))
71
82
  end
72
83
  end
73
-
84
+
74
85
  class ClientWorkers
75
86
  def initialize(client)
76
87
  @client = client
77
88
  end
78
-
89
+
79
90
  def counts
80
91
  JSON.parse(@client._workers.call([], [Time.now.to_i]))
81
92
  end
82
-
93
+
83
94
  def [](name)
84
95
  JSON.parse(@client._workers.call([], [Time.now.to_i, name]))
85
96
  end
86
97
  end
87
-
98
+
88
99
  class ClientQueues
89
100
  def initialize(client)
90
101
  @client = client
91
102
  end
92
-
103
+
93
104
  def counts
94
105
  JSON.parse(@client._queues.call([], [Time.now.to_i]))
95
106
  end
96
-
107
+
97
108
  def [](name)
98
109
  Queue.new(name, @client)
99
110
  end
100
111
  end
101
-
112
+
102
113
  class ClientEvents
103
114
  def initialize(redis)
104
115
  @redis = redis
105
116
  @actions = Hash.new()
106
117
  end
107
-
118
+
108
119
  def canceled(&block) ; @actions[:canceled ] = block; end
109
120
  def completed(&block); @actions[:completed] = block; end
110
121
  def failed(&block) ; @actions[:failed ] = block; end
@@ -113,7 +124,7 @@ module Qless
113
124
  def put(&block) ; @actions[:put ] = block; end
114
125
  def track(&block) ; @actions[:track ] = block; end
115
126
  def untrack(&block) ; @actions[:untrack ] = block; end
116
-
127
+
117
128
  def listen
118
129
  yield(self) if block_given?
119
130
  @redis.subscribe(:canceled, :completed, :failed, :popped, :stalled, :put, :track, :untrack) do |on|
@@ -125,19 +136,20 @@ module Qless
125
136
  end
126
137
  end
127
138
  end
128
-
139
+
129
140
  def stop
130
141
  @redis.unsubscribe
131
142
  end
132
143
  end
133
-
144
+
134
145
  class Client
135
146
  # Lua scripts
136
147
  attr_reader :_cancel, :_config, :_complete, :_fail, :_failed, :_get, :_heartbeat, :_jobs, :_peek, :_pop
137
148
  attr_reader :_priority, :_put, :_queues, :_recur, :_retry, :_stats, :_tag, :_track, :_workers, :_depends
149
+ attr_reader :_pause, :_unpause, :_deregister_workers
138
150
  # A real object
139
151
  attr_reader :config, :redis, :jobs, :queues, :workers
140
-
152
+
141
153
  def initialize(options = {})
142
154
  # This is the redis instance we're connected to
143
155
  @redis = options[:redis] || Redis.connect(options) # use connect so REDIS_URL will be honored
@@ -145,10 +157,11 @@ module Qless
145
157
  assert_minimum_redis_version("2.5.5")
146
158
  @config = Config.new(self)
147
159
  ['cancel', 'config', 'complete', 'depends', 'fail', 'failed', 'get', 'heartbeat', 'jobs', 'peek', 'pop',
148
- 'priority', 'put', 'queues', 'recur', 'retry', 'stats', 'tag', 'track', 'workers'].each do |f|
149
- self.instance_variable_set("@_#{f}", Lua.new(f, @redis))
160
+ 'priority', 'put', 'queues', 'recur', 'retry', 'stats', 'tag', 'track', 'workers', 'pause', 'unpause',
161
+ 'deregister_workers'].each do |f|
162
+ self.instance_variable_set("@_#{f}", Qless::LuaScript.new(f, @redis))
150
163
  end
151
-
164
+
152
165
  @jobs = ClientJobs.new(self)
153
166
  @queues = ClientQueues.new(self)
154
167
  @workers = ClientWorkers.new(self)
@@ -157,29 +170,43 @@ module Qless
157
170
  def inspect
158
171
  "<Qless::Client #{@options} >"
159
172
  end
160
-
173
+
161
174
  def events
162
175
  # Events needs its own redis instance of the same configuration, because
163
176
  # once it's subscribed, we can only use pub-sub-like commands. This way,
164
177
  # we still have access to the client in the normal case
165
178
  @events ||= ClientEvents.new(Redis.connect(@options))
166
179
  end
167
-
180
+
168
181
  def track(jid)
169
182
  @_track.call([], ['track', jid, Time.now.to_i])
170
183
  end
171
-
184
+
172
185
  def untrack(jid)
173
186
  @_track.call([], ['untrack', jid, Time.now.to_i])
174
187
  end
175
-
188
+
176
189
  def tags(offset=0, count=100)
177
190
  JSON.parse(@_tag.call([], ['top', offset, count]))
178
191
  end
192
+
193
+ def deregister_workers(*worker_names)
194
+ _deregister_workers.call([], worker_names)
195
+ end
196
+
197
+ def bulk_cancel(jids)
198
+ @_cancel.call([], jids)
199
+ end
200
+
201
+ def new_redis_connection
202
+ ::Redis.new(url: redis.id)
203
+ end
204
+
179
205
  private
180
206
 
181
207
  def assert_minimum_redis_version(version)
182
- redis_version = @redis.info.fetch("redis_version")
208
+ # remove the "-pre2" from "2.6.8-pre2"
209
+ redis_version = @redis.info.fetch("redis_version").split('-').first
183
210
  return if Gem::Version.new(redis_version) >= Gem::Version.new(version)
184
211
 
185
212
  raise UnsupportedRedisVersionError,
@@ -1,5 +1,3 @@
1
- require "qless/lua"
2
- require "redis"
3
1
  require "json"
4
2
 
5
3
  module Qless
@@ -28,4 +26,4 @@ module Qless
28
26
  @client._config.call([], ['unset', option])
29
27
  end
30
28
  end
31
- end
29
+ end
@@ -1,18 +1,19 @@
1
1
  require "qless"
2
2
  require "qless/queue"
3
- require "qless/lua"
4
3
  require "redis"
5
4
  require "json"
6
5
 
7
6
  module Qless
8
7
  class BaseJob
8
+ attr_reader :client
9
+
9
10
  def initialize(client, jid)
10
11
  @client = client
11
12
  @jid = jid
12
13
  end
13
14
 
14
15
  def klass
15
- @klass ||= @klass_name.split('::').inject(Kernel) { |context, name| context.const_get(name) }
16
+ @klass ||= @klass_name.split('::').inject(Object) { |context, name| context.const_get(name) }
16
17
  end
17
18
 
18
19
  def queue
@@ -21,12 +22,30 @@ module Qless
21
22
  end
22
23
 
23
24
  class Job < BaseJob
24
- attr_reader :jid, :expires_at, :state, :queue_name, :history, :worker_name, :failure, :klass_name, :tracked, :dependencies, :dependents
25
- attr_reader :original_retries, :retries_left
25
+ attr_reader :jid, :expires_at, :state, :queue_name, :worker_name, :failure, :klass_name, :tracked, :dependencies, :dependents
26
+ attr_reader :original_retries, :retries_left, :raw_queue_history
26
27
  attr_accessor :data, :priority, :tags
27
28
 
29
+ MiddlewareMisconfiguredError = Class.new(StandardError)
30
+
31
+ module SupportsMiddleware
32
+ def around_perform(job)
33
+ perform(job)
34
+ end
35
+ end
36
+
28
37
  def perform
29
- klass.perform(self)
38
+ middlewares = Job.middlewares_on(klass)
39
+
40
+ if middlewares.last == SupportsMiddleware
41
+ klass.around_perform(self)
42
+ elsif middlewares.any?
43
+ raise MiddlewareMisconfiguredError, "The middleware chain for #{klass} " +
44
+ "(#{middlewares.inspect}) is misconfigured. Qless::Job::SupportsMiddleware " +
45
+ "must be extended onto your job class first if you want to use any middleware."
46
+ else
47
+ klass.perform(self)
48
+ end
30
49
  end
31
50
 
32
51
  def self.build(client, klass, attributes = {})
@@ -49,29 +68,38 @@ module Qless
49
68
  "dependents" => []
50
69
  }
51
70
  attributes = defaults.merge(Qless.stringify_hash_keys(attributes))
52
- attributes["data"] = JSON.load(JSON.dump attributes["data"])
71
+ attributes["data"] = JSON.parse(JSON.dump attributes["data"])
53
72
  new(client, attributes)
54
73
  end
55
74
 
75
+ def self.middlewares_on(job_klass)
76
+ job_klass.singleton_class.ancestors.select do |ancestor|
77
+ ancestor.method_defined?(:around_perform)
78
+ end
79
+ end
80
+
56
81
  def initialize(client, atts)
57
82
  super(client, atts.fetch('jid'))
58
83
  %w{jid data priority tags state tracked
59
- failure history dependencies dependents}.each do |att|
84
+ failure dependencies dependents}.each do |att|
60
85
  self.instance_variable_set("@#{att}".to_sym, atts.fetch(att))
61
86
  end
62
87
 
63
- @expires_at = atts.fetch('expires')
64
- @klass_name = atts.fetch('klass')
65
- @queue_name = atts.fetch('queue')
66
- @worker_name = atts.fetch('worker')
67
- @original_retries = atts.fetch('retries')
68
- @retries_left = atts.fetch('remaining')
88
+ @expires_at = atts.fetch('expires')
89
+ @klass_name = atts.fetch('klass')
90
+ @queue_name = atts.fetch('queue')
91
+ @worker_name = atts.fetch('worker')
92
+ @original_retries = atts.fetch('retries')
93
+ @retries_left = atts.fetch('remaining')
94
+ @raw_queue_history = atts.fetch('history')
69
95
 
70
96
  # This is a silly side-effect of Lua doing JSON parsing
71
97
  @tags = [] if @tags == {}
72
98
  @dependents = [] if @dependents == {}
73
99
  @dependencies = [] if @dependencies == {}
74
100
  @state_changed = false
101
+ @before_callbacks = Hash.new { |h, k| h[k] = [] }
102
+ @after_callbacks = Hash.new { |h, k| h[k] = [] }
75
103
  end
76
104
 
77
105
  def priority=(priority)
@@ -93,7 +121,7 @@ module Qless
93
121
  end
94
122
 
95
123
  def description
96
- "#{@jid} (#{@klass_name} / #{@queue_name})"
124
+ "#{@klass_name} (#{@jid} / #{@queue_name} / #{@state})"
97
125
  end
98
126
 
99
127
  def inspect
@@ -104,9 +132,57 @@ module Qless
104
132
  @expires_at - Time.now.to_f
105
133
  end
106
134
 
135
+ def reconnect_to_redis
136
+ @client.redis.client.reconnect
137
+ end
138
+
139
+ def history
140
+ warn "WARNING: Qless::Job#history is deprecated; use Qless::Job#raw_queue_history instead" +
141
+ "; called from:\n#{caller.first}\n"
142
+ raw_queue_history
143
+ end
144
+
145
+ def queue_history
146
+ @queue_history ||= @raw_queue_history.map do |history_event|
147
+ history_event.each_with_object({}) do |(key, value), hash|
148
+ # The only Numeric (Integer or Float) values we get in the history are timestamps
149
+ hash[key] = if value.is_a?(Numeric)
150
+ Time.at(value).utc
151
+ else
152
+ value
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ def initially_put_at
159
+ @initially_put_at ||= history_timestamp('put', :min)
160
+ end
161
+
162
+ def to_hash
163
+ {
164
+ jid: jid,
165
+ expires_at: expires_at,
166
+ state: state,
167
+ queue_name: queue_name,
168
+ history: raw_queue_history,
169
+ worker_name: worker_name,
170
+ failure: failure,
171
+ klass_name: klass_name,
172
+ tracked: tracked,
173
+ dependencies: dependencies,
174
+ dependents: dependents,
175
+ original_retries: original_retries,
176
+ retries_left: retries_left,
177
+ data: data,
178
+ priority: priority,
179
+ tags: tags
180
+ }
181
+ end
182
+
107
183
  # Move this from it's current queue into another
108
184
  def move(queue)
109
- note_state_change do
185
+ note_state_change :move do
110
186
  @client._put.call([queue], [
111
187
  @jid, @klass_name, JSON.generate(@data), Time.now.to_f, 0
112
188
  ])
@@ -115,7 +191,7 @@ module Qless
115
191
 
116
192
  # Fail a job
117
193
  def fail(group, message)
118
- note_state_change do
194
+ note_state_change :fail do
119
195
  @client._fail.call([], [
120
196
  @jid,
121
197
  @worker_name,
@@ -134,13 +210,15 @@ module Qless
134
210
  JSON.generate(@data)]) || false
135
211
  end
136
212
 
213
+ CantCompleteError = Class.new(Qless::Error)
214
+
137
215
  # Complete a job
138
216
  # Options include
139
217
  # => next (String) the next queue
140
218
  # => delay (int) how long to delay it in the next queue
141
219
  def complete(nxt=nil, options={})
142
- response = note_state_change do
143
- if nxt.nil?
220
+ note_state_change :complete do
221
+ response = if nxt.nil?
144
222
  @client._complete.call([], [
145
223
  @jid, @worker_name, @queue_name, Time.now.to_f, JSON.generate(@data)])
146
224
  else
@@ -148,8 +226,19 @@ module Qless
148
226
  @jid, @worker_name, @queue_name, Time.now.to_f, JSON.generate(@data), 'next', nxt, 'delay',
149
227
  options.fetch(:delay, 0), 'depends', JSON.generate(options.fetch(:depends, []))])
150
228
  end
229
+
230
+ if response
231
+ response
232
+ else
233
+ description = if reloaded_instance = @client.jobs[@jid]
234
+ reloaded_instance.description
235
+ else
236
+ self.description + " -- can't be reloaded"
237
+ end
238
+
239
+ raise CantCompleteError, "Failed to complete #{description}"
240
+ end
151
241
  end
152
- response.nil? ? false : response
153
242
  end
154
243
 
155
244
  def state_changed?
@@ -157,7 +246,7 @@ module Qless
157
246
  end
158
247
 
159
248
  def cancel
160
- note_state_change do
249
+ note_state_change :cancel do
161
250
  @client._cancel.call([], [@jid])
162
251
  end
163
252
  end
@@ -179,7 +268,7 @@ module Qless
179
268
  end
180
269
 
181
270
  def retry(delay=0)
182
- note_state_change do
271
+ note_state_change :retry do
183
272
  results = @client._retry.call([], [@jid, @queue_name, @worker_name, Time.now.to_f, delay])
184
273
  results.nil? ? false : results
185
274
  end
@@ -193,13 +282,29 @@ module Qless
193
282
  !!@client._depends.call([], [@jid, 'off'] + jids)
194
283
  end
195
284
 
285
+ [:fail, :complete, :cancel, :move, :retry].each do |event|
286
+ define_method :"before_#{event}" do |&block|
287
+ @before_callbacks[event] << block
288
+ end
289
+
290
+ define_method :"after_#{event}" do |&block|
291
+ @after_callbacks[event].unshift block
292
+ end
293
+ end
294
+
196
295
  private
197
296
 
198
- def note_state_change
297
+ def note_state_change(event)
298
+ @before_callbacks[event].each { |blk| blk.call(self) }
199
299
  result = yield
200
300
  @state_changed = true
301
+ @after_callbacks[event].each { |blk| blk.call(self) }
201
302
  result
202
303
  end
304
+
305
+ def history_timestamp(name, selector)
306
+ queue_history.map { |q| q[name] }.compact.send(selector)
307
+ end
203
308
  end
204
309
 
205
310
  class RecurringJob < BaseJob