qless 0.9.3 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/Gemfile +9 -3
  2. data/README.md +70 -25
  3. data/Rakefile +125 -9
  4. data/exe/install_phantomjs +21 -0
  5. data/lib/qless.rb +115 -76
  6. data/lib/qless/config.rb +11 -9
  7. data/lib/qless/failure_formatter.rb +43 -0
  8. data/lib/qless/job.rb +201 -102
  9. data/lib/qless/job_reservers/ordered.rb +7 -1
  10. data/lib/qless/job_reservers/round_robin.rb +16 -6
  11. data/lib/qless/job_reservers/shuffled_round_robin.rb +9 -2
  12. data/lib/qless/lua/qless-lib.lua +2463 -0
  13. data/lib/qless/lua/qless.lua +2012 -0
  14. data/lib/qless/lua_script.rb +63 -12
  15. data/lib/qless/middleware/memory_usage_monitor.rb +62 -0
  16. data/lib/qless/middleware/metriks.rb +45 -0
  17. data/lib/qless/middleware/redis_reconnect.rb +6 -3
  18. data/lib/qless/middleware/requeue_exceptions.rb +94 -0
  19. data/lib/qless/middleware/retry_exceptions.rb +38 -9
  20. data/lib/qless/middleware/sentry.rb +3 -7
  21. data/lib/qless/middleware/timeout.rb +64 -0
  22. data/lib/qless/queue.rb +90 -55
  23. data/lib/qless/server.rb +177 -130
  24. data/lib/qless/server/views/_job.erb +33 -15
  25. data/lib/qless/server/views/completed.erb +11 -0
  26. data/lib/qless/server/views/layout.erb +70 -11
  27. data/lib/qless/server/views/overview.erb +93 -53
  28. data/lib/qless/server/views/queue.erb +9 -8
  29. data/lib/qless/server/views/queues.erb +18 -1
  30. data/lib/qless/subscriber.rb +37 -22
  31. data/lib/qless/tasks.rb +5 -10
  32. data/lib/qless/test_helpers/worker_helpers.rb +55 -0
  33. data/lib/qless/version.rb +3 -1
  34. data/lib/qless/worker.rb +4 -413
  35. data/lib/qless/worker/base.rb +247 -0
  36. data/lib/qless/worker/forking.rb +245 -0
  37. data/lib/qless/worker/serial.rb +41 -0
  38. metadata +135 -52
  39. data/lib/qless/qless-core/cancel.lua +0 -101
  40. data/lib/qless/qless-core/complete.lua +0 -233
  41. data/lib/qless/qless-core/config.lua +0 -56
  42. data/lib/qless/qless-core/depends.lua +0 -65
  43. data/lib/qless/qless-core/deregister_workers.lua +0 -12
  44. data/lib/qless/qless-core/fail.lua +0 -117
  45. data/lib/qless/qless-core/failed.lua +0 -83
  46. data/lib/qless/qless-core/get.lua +0 -37
  47. data/lib/qless/qless-core/heartbeat.lua +0 -51
  48. data/lib/qless/qless-core/jobs.lua +0 -41
  49. data/lib/qless/qless-core/pause.lua +0 -18
  50. data/lib/qless/qless-core/peek.lua +0 -165
  51. data/lib/qless/qless-core/pop.lua +0 -314
  52. data/lib/qless/qless-core/priority.lua +0 -32
  53. data/lib/qless/qless-core/put.lua +0 -169
  54. data/lib/qless/qless-core/qless-lib.lua +0 -2354
  55. data/lib/qless/qless-core/qless.lua +0 -1862
  56. data/lib/qless/qless-core/queues.lua +0 -58
  57. data/lib/qless/qless-core/recur.lua +0 -190
  58. data/lib/qless/qless-core/retry.lua +0 -73
  59. data/lib/qless/qless-core/stats.lua +0 -92
  60. data/lib/qless/qless-core/tag.lua +0 -100
  61. data/lib/qless/qless-core/track.lua +0 -79
  62. data/lib/qless/qless-core/unfail.lua +0 -54
  63. data/lib/qless/qless-core/unpause.lua +0 -12
  64. data/lib/qless/qless-core/workers.lua +0 -69
  65. data/lib/qless/wait_until.rb +0 -19
@@ -1,4 +1,6 @@
1
- require "json"
1
+ # Encoding: utf-8
2
+
3
+ require 'json'
2
4
 
3
5
  module Qless
4
6
  # A configuration class associated with a qless client
@@ -6,24 +8,24 @@ module Qless
6
8
  def initialize(client)
7
9
  @client = client
8
10
  end
9
-
11
+
10
12
  def [](key)
11
- @client._config.call([], ['get', key])
13
+ @client.call('config.get', key)
12
14
  end
13
-
15
+
14
16
  def []=(key, value)
15
- @client._config.call([], ['set', key, value])
17
+ @client.call('config.set', key, value)
16
18
  end
17
-
19
+
18
20
  # Get the specified `qless` configuration option, or if
19
21
  # none is provided, get the complete current configuration
20
22
  def all
21
- return JSON.parse(@client._config.call([], ['get']))
23
+ JSON.parse(@client.call('config.get'))
22
24
  end
23
-
25
+
24
26
  # Restore this option to the default (remove this option)
25
27
  def clear(option)
26
- @client._config.call([], ['unset', option])
28
+ @client.call('config.unset', option)
27
29
  end
28
30
  end
29
31
  end
@@ -0,0 +1,43 @@
1
+ # Encoding: utf-8
2
+
3
+ module Qless
4
+ # A helper for formatting failure messages
5
+ class FailureFormatter
6
+ Failure = Struct.new(:group, :message) do
7
+ # allow de-structring assignment
8
+ def to_ary
9
+ [group, message]
10
+ end
11
+ end
12
+
13
+ def initialize
14
+ @replacements = { Dir.pwd => '.' }
15
+ @replacements[ENV['GEM_HOME']] = '<GEM_HOME>' if ENV.key?('GEM_HOME')
16
+ end
17
+
18
+ def format(job, error, lines_to_remove = caller(2))
19
+ group = "#{job.klass_name}:#{error.class}"
20
+ message = "#{truncated_message(error)}\n\n" +
21
+ "#{format_failure_backtrace(error.backtrace, lines_to_remove)}"
22
+ Failure.new(group, message)
23
+ end
24
+
25
+ private
26
+
27
+ # TODO: pull this out into a config option.
28
+ MAX_ERROR_MESSAGE_SIZE = 10_000
29
+ def truncated_message(error)
30
+ return error.message if error.message.length <= MAX_ERROR_MESSAGE_SIZE
31
+ error.message.slice(0, MAX_ERROR_MESSAGE_SIZE) +
32
+ "\n... (truncated due to length)"
33
+ end
34
+
35
+ def format_failure_backtrace(error_backtrace, lines_to_remove)
36
+ (error_backtrace - lines_to_remove).map do |line|
37
+ @replacements.reduce(line) do |formatted, (original, new)|
38
+ formatted.sub(original, new)
39
+ end
40
+ end.join("\n")
41
+ end
42
+ end
43
+ end
@@ -1,9 +1,13 @@
1
- require "qless"
2
- require "qless/queue"
3
- require "redis"
4
- require "json"
1
+ # Encoding: utf-8
2
+
3
+ require 'qless'
4
+ require 'qless/queue'
5
+ require 'qless/lua_script'
6
+ require 'redis'
7
+ require 'json'
5
8
 
6
9
  module Qless
10
+ # The base for both Job and RecurringJob
7
11
  class BaseJob
8
12
  attr_reader :client
9
13
 
@@ -13,18 +17,35 @@ module Qless
13
17
  end
14
18
 
15
19
  def klass
16
- @klass ||= @klass_name.split('::').inject(Object) { |context, name| context.const_get(name) }
20
+ @klass ||= @klass_name.split('::').reduce(Object) do |context, name|
21
+ context.const_get(name)
22
+ end
17
23
  end
18
24
 
19
25
  def queue
20
26
  @queue ||= Queue.new(@queue_name, @client)
21
27
  end
28
+
29
+ def ==(other)
30
+ self.class == other.class &&
31
+ jid == other.jid &&
32
+ client == other.client
33
+ end
34
+ alias eql? ==
35
+
36
+ def hash
37
+ self.class.hash ^ jid.hash ^ client.hash
38
+ end
22
39
  end
23
40
 
41
+ # A Qless job
24
42
  class Job < BaseJob
25
- attr_reader :jid, :expires_at, :state, :queue_name, :worker_name, :failure, :klass_name, :tracked, :dependencies, :dependents
43
+ attr_reader :jid, :expires_at, :state, :queue_name, :worker_name, :failure, :spawned_from_jid
44
+ attr_reader :klass_name, :tracked, :dependencies, :dependents
26
45
  attr_reader :original_retries, :retries_left, :raw_queue_history
46
+ attr_reader :state_changed
27
47
  attr_accessor :data, :priority, :tags
48
+ alias_method(:state_changed?, :state_changed)
28
49
 
29
50
  MiddlewareMisconfiguredError = Class.new(StandardError)
30
51
 
@@ -35,14 +56,29 @@ module Qless
35
56
  end
36
57
 
37
58
  def perform
59
+ # If we can't find the class, we should fail the job, not try to process
60
+ begin
61
+ klass
62
+ rescue NameError
63
+ return fail("#{queue_name}-NameError", "Cannot find #{klass_name}")
64
+ end
65
+
66
+ # log a real process executing job -- before we start processing
67
+ log("started by pid:#{Process.pid}")
68
+
38
69
  middlewares = Job.middlewares_on(klass)
39
70
 
40
71
  if middlewares.last == SupportsMiddleware
41
72
  klass.around_perform(self)
42
73
  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."
74
+ raise MiddlewareMisconfiguredError, 'The middleware chain for ' +
75
+ "#{klass} (#{middlewares.inspect}) is misconfigured." +
76
+ 'Qless::Job::SupportsMiddleware must be extended onto your job' +
77
+ 'class first if you want to use any middleware.'
78
+ elsif !klass.respond_to?(:perform)
79
+ # If the klass doesn't have a :perform method, we should raise an error
80
+ fail("#{queue_name}-method-missing",
81
+ "#{klass_name} has no perform method")
46
82
  else
47
83
  klass.perform(self)
48
84
  end
@@ -50,41 +86,46 @@ module Qless
50
86
 
51
87
  def self.build(client, klass, attributes = {})
52
88
  defaults = {
53
- "jid" => Qless.generate_jid,
54
- "data" => {},
55
- "klass" => klass.to_s,
56
- "priority" => 0,
57
- "tags" => [],
58
- "worker" => "mock_worker",
59
- "expires" => Time.now + (60 * 60), # an hour from now
60
- "state" => "running",
61
- "tracked" => false,
62
- "queue" => "mock_queue",
63
- "retries" => 5,
64
- "remaining" => 5,
65
- "failure" => {},
66
- "history" => [],
67
- "dependencies" => [],
68
- "dependents" => []
89
+ 'jid' => Qless.generate_jid,
90
+ 'spawned_from_jid' => nil,
91
+ 'data' => {},
92
+ 'klass' => klass.to_s,
93
+ 'priority' => 0,
94
+ 'tags' => [],
95
+ 'worker' => 'mock_worker',
96
+ 'expires' => Time.now + (60 * 60), # an hour from now
97
+ 'state' => 'running',
98
+ 'tracked' => false,
99
+ 'queue' => 'mock_queue',
100
+ 'retries' => 5,
101
+ 'remaining' => 5,
102
+ 'failure' => {},
103
+ 'history' => [],
104
+ 'dependencies' => [],
105
+ 'dependents' => []
69
106
  }
70
107
  attributes = defaults.merge(Qless.stringify_hash_keys(attributes))
71
- attributes["data"] = JSON.parse(JSON.dump attributes["data"])
108
+ attributes['data'] = JSON.dump(attributes['data'])
72
109
  new(client, attributes)
73
110
  end
74
111
 
75
112
  def self.middlewares_on(job_klass)
76
- job_klass.singleton_class.ancestors.select do |ancestor|
77
- ancestor.method_defined?(:around_perform)
113
+ singleton_klass = job_klass.singleton_class
114
+ singleton_klass.ancestors.select do |ancestor|
115
+ ancestor != singleton_klass && ancestor.method_defined?(:around_perform)
78
116
  end
79
117
  end
80
118
 
81
119
  def initialize(client, atts)
82
120
  super(client, atts.fetch('jid'))
83
121
  %w{jid data priority tags state tracked
84
- failure dependencies dependents}.each do |att|
85
- self.instance_variable_set("@#{att}".to_sym, atts.fetch(att))
122
+ failure dependencies dependents spawned_from_jid}.each do |att|
123
+ instance_variable_set(:"@#{att}", atts.fetch(att))
86
124
  end
87
125
 
126
+ # Parse the data string
127
+ @data = JSON.parse(@data)
128
+
88
129
  @expires_at = atts.fetch('expires')
89
130
  @klass_name = atts.fetch('klass')
90
131
  @queue_name = atts.fetch('queue')
@@ -103,9 +144,7 @@ module Qless
103
144
  end
104
145
 
105
146
  def priority=(priority)
106
- if @client._priority.call([], [@jid, priority])
107
- @priority = priority
108
- end
147
+ @priority = priority if @client.call('priority', @jid, priority)
109
148
  end
110
149
 
111
150
  def [](key)
@@ -137,19 +176,20 @@ module Qless
137
176
  end
138
177
 
139
178
  def history
140
- warn "WARNING: Qless::Job#history is deprecated; use Qless::Job#raw_queue_history instead" +
141
- "; called from:\n#{caller.first}\n"
179
+ warn 'WARNING: Qless::Job#history is deprecated; use' +
180
+ "Qless::Job#raw_queue_history instead; from:\n#{caller.first}"
142
181
  raw_queue_history
143
182
  end
144
183
 
145
184
  def queue_history
146
185
  @queue_history ||= @raw_queue_history.map do |history_event|
147
186
  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
187
+ # The only Numeric (Integer or Float) values we get in the history
188
+ # are timestamps
189
+ if value.is_a?(Numeric)
190
+ hash[key] = Time.at(value).utc
151
191
  else
152
- value
192
+ hash[key] = value
153
193
  end
154
194
  end
155
195
  end
@@ -159,9 +199,14 @@ module Qless
159
199
  @initially_put_at ||= history_timestamp('put', :min)
160
200
  end
161
201
 
202
+ def spawned_from
203
+ @spawned_from ||= @client.jobs[@spawned_from_jid]
204
+ end
205
+
162
206
  def to_hash
163
207
  {
164
208
  jid: jid,
209
+ spawned_from_jid: spawned_from_jid,
165
210
  expires_at: expires_at,
166
211
  state: state,
167
212
  queue_name: queue_name,
@@ -181,108 +226,129 @@ module Qless
181
226
  end
182
227
 
183
228
  # Move this from it's current queue into another
184
- def move(queue)
185
- note_state_change :move do
186
- @client._put.call([queue], [
187
- @jid, @klass_name, JSON.generate(@data), Time.now.to_f, 0
188
- ])
229
+ def requeue(queue, opts = {})
230
+ queue_name = case queue
231
+ when String, Symbol then queue
232
+ else queue.name
233
+ end
234
+
235
+ note_state_change :requeue do
236
+ @client.call('requeue', @client.worker_name, queue_name, @jid, @klass_name,
237
+ JSON.dump(opts.fetch(:data, @data)),
238
+ opts.fetch(:delay, 0),
239
+ 'priority', opts.fetch(:priority, @priority),
240
+ 'tags', JSON.dump(opts.fetch(:tags, @tags)),
241
+ 'retries', opts.fetch(:retries, @original_retries),
242
+ 'depends', JSON.dump(opts.fetch(:depends, @dependencies))
243
+ )
189
244
  end
190
245
  end
246
+ alias move requeue # for backwards compatibility
247
+
248
+ CantFailError = Class.new(Qless::LuaScriptError)
191
249
 
192
250
  # Fail a job
193
251
  def fail(group, message)
194
252
  note_state_change :fail do
195
- @client._fail.call([], [
253
+ @client.call(
254
+ 'fail',
196
255
  @jid,
197
256
  @worker_name,
198
257
  group, message,
199
- Time.now.to_f,
200
- JSON.generate(@data)]) || false
258
+ JSON.dump(@data)) || false
201
259
  end
260
+ rescue Qless::LuaScriptError => err
261
+ raise CantFailError.new(err.message)
202
262
  end
203
263
 
204
264
  # Heartbeat a job
205
- def heartbeat()
206
- @client._heartbeat.call([], [
265
+ def heartbeat
266
+ @expires_at = @client.call(
267
+ 'heartbeat',
207
268
  @jid,
208
269
  @worker_name,
209
- Time.now.to_f,
210
- JSON.generate(@data)]) || false
270
+ JSON.dump(@data))
211
271
  end
212
272
 
213
- CantCompleteError = Class.new(Qless::Error)
273
+ CantCompleteError = Class.new(Qless::LuaScriptError)
214
274
 
215
275
  # Complete a job
216
276
  # Options include
217
277
  # => next (String) the next queue
218
278
  # => delay (int) how long to delay it in the next queue
219
- def complete(nxt=nil, options={})
279
+ def complete(nxt = nil, options = {})
220
280
  note_state_change :complete do
221
- response = if nxt.nil?
222
- @client._complete.call([], [
223
- @jid, @worker_name, @queue_name, Time.now.to_f, JSON.generate(@data)])
281
+ if nxt.nil?
282
+ @client.call(
283
+ 'complete', @jid, @worker_name, @queue_name, JSON.dump(@data))
224
284
  else
225
- @client._complete.call([], [
226
- @jid, @worker_name, @queue_name, Time.now.to_f, JSON.generate(@data), 'next', nxt, 'delay',
227
- options.fetch(:delay, 0), 'depends', JSON.generate(options.fetch(:depends, []))])
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}"
285
+ @client.call('complete', @jid, @worker_name, @queue_name,
286
+ JSON.dump(@data), 'next', nxt, 'delay',
287
+ options.fetch(:delay, 0), 'depends',
288
+ JSON.dump(options.fetch(:depends, [])))
240
289
  end
241
290
  end
242
- end
243
-
244
- def state_changed?
245
- @state_changed
291
+ rescue Qless::LuaScriptError => err
292
+ raise CantCompleteError.new(err.message)
246
293
  end
247
294
 
248
295
  def cancel
249
296
  note_state_change :cancel do
250
- @client._cancel.call([], [@jid])
297
+ @client.call('cancel', @jid)
251
298
  end
252
299
  end
253
300
 
254
- def track()
255
- @client._track.call([], ['track', @jid, Time.now.to_f])
301
+ def track
302
+ @client.call('track', 'track', @jid)
256
303
  end
257
304
 
258
305
  def untrack
259
- @client._track.call([], ['untrack', @jid, Time.now.to_f])
306
+ @client.call('track', 'untrack', @jid)
260
307
  end
261
308
 
262
309
  def tag(*tags)
263
- @client._tag.call([], ['add', @jid, Time.now.to_f] + tags)
310
+ @client.call('tag', 'add', @jid, *tags)
264
311
  end
265
312
 
266
313
  def untag(*tags)
267
- @client._tag.call([], ['remove', @jid, Time.now.to_f] + tags)
314
+ @client.call('tag', 'remove', @jid, *tags)
268
315
  end
269
316
 
270
- def retry(delay=0)
317
+ def retry(delay = 0, group = nil, message = nil)
271
318
  note_state_change :retry do
272
- results = @client._retry.call([], [@jid, @queue_name, @worker_name, Time.now.to_f, delay])
273
- results.nil? ? false : results
319
+ if group.nil?
320
+ results = @client.call(
321
+ 'retry', @jid, @queue_name, @worker_name, delay)
322
+ results.nil? ? false : results
323
+ else
324
+ results = @client.call(
325
+ 'retry', @jid, @queue_name, @worker_name, delay, group, message)
326
+ results.nil? ? false : results
327
+ end
274
328
  end
275
329
  end
276
330
 
277
331
  def depend(*jids)
278
- !!@client._depends.call([], [@jid, 'on'] + jids)
332
+ !!@client.call('depends', @jid, 'on', *jids)
279
333
  end
280
334
 
281
335
  def undepend(*jids)
282
- !!@client._depends.call([], [@jid, 'off'] + jids)
336
+ !!@client.call('depends', @jid, 'off', *jids)
337
+ end
338
+
339
+ def timeout
340
+ @client.call('timeout', @jid)
283
341
  end
284
342
 
285
- [:fail, :complete, :cancel, :move, :retry].each do |event|
343
+ def log(message, data = nil)
344
+ if data
345
+ @client.call('log', @jid, message, JSON.dump(data))
346
+ else
347
+ @client.call('log', @jid, message)
348
+ end
349
+ end
350
+
351
+ [:fail, :complete, :cancel, :requeue, :retry].each do |event|
286
352
  define_method :"before_#{event}" do |&block|
287
353
  @before_callbacks[event] << block
288
354
  end
@@ -291,8 +357,8 @@ module Qless
291
357
  @after_callbacks[event].unshift block
292
358
  end
293
359
  end
294
-
295
- private
360
+ alias before_move before_requeue
361
+ alias after_move after_requeue
296
362
 
297
363
  def note_state_change(event)
298
364
  @before_callbacks[event].each { |blk| blk.call(self) }
@@ -302,65 +368,98 @@ module Qless
302
368
  result
303
369
  end
304
370
 
371
+ private
372
+
305
373
  def history_timestamp(name, selector)
306
- queue_history.map { |q| q[name] }.compact.send(selector)
374
+ items = queue_history.select do |q|
375
+ q['what'] == name
376
+ end
377
+ items.map do |q|
378
+ q['when']
379
+ end.public_send(selector)
307
380
  end
308
381
  end
309
382
 
383
+ # Wraps a recurring job
310
384
  class RecurringJob < BaseJob
311
- attr_reader :jid, :data, :priority, :tags, :retries, :interval, :count, :queue_name, :klass_name
385
+ attr_reader :jid, :data, :priority, :tags, :retries, :interval, :count
386
+ attr_reader :queue_name, :klass_name, :backlog
312
387
 
313
388
  def initialize(client, atts)
314
389
  super(client, atts.fetch('jid'))
315
- %w{jid data priority tags retries interval count}.each do |att|
316
- self.instance_variable_set("@#{att}".to_sym, atts.fetch(att))
390
+ %w{jid data priority tags retries interval count backlog}.each do |att|
391
+ instance_variable_set("@#{att}".to_sym, atts.fetch(att))
317
392
  end
318
393
 
394
+ # Parse the data string
395
+ @data = JSON.parse(@data)
319
396
  @klass_name = atts.fetch('klass')
320
397
  @queue_name = atts.fetch('queue')
321
398
  @tags = [] if @tags == {}
322
399
  end
323
400
 
324
401
  def priority=(value)
325
- @client._recur.call([], ['update', @jid, 'priority', value])
402
+ @client.call('recur.update', @jid, 'priority', value)
326
403
  @priority = value
327
404
  end
328
405
 
329
406
  def retries=(value)
330
- @client._recur.call([], ['update', @jid, 'retries', value])
407
+ @client.call('recur.update', @jid, 'retries', value)
331
408
  @retries = value
332
409
  end
333
410
 
334
411
  def interval=(value)
335
- @client._recur.call([], ['update', @jid, 'interval', value])
412
+ @client.call('recur.update', @jid, 'interval', value)
336
413
  @interval = value
337
414
  end
338
415
 
339
416
  def data=(value)
340
- @client._recur.call([], ['update', @jid, 'data', JSON.generate(value)])
417
+ @client.call('recur.update', @jid, 'data', JSON.dump(value))
341
418
  @data = value
342
419
  end
343
420
 
344
421
  def klass=(value)
345
- @client._recur.call([], ['update', @jid, 'klass', value.to_s])
422
+ @client.call('recur.update', @jid, 'klass', value.to_s)
346
423
  @klass_name = value.to_s
347
424
  end
348
425
 
426
+ def backlog=(value)
427
+ @client.call('recur.update', @jid, 'backlog', value.to_s)
428
+ @backlog = value
429
+ end
430
+
349
431
  def move(queue)
350
- @client._recur.call([], ['update', @jid, 'queue', queue])
432
+ @client.call('recur.update', @jid, 'queue', queue)
351
433
  @queue_name = queue
352
434
  end
435
+ alias requeue move # for API parity with normal jobs
353
436
 
354
437
  def cancel
355
- @client._recur.call([], ['off', @jid])
438
+ @client.call('unrecur', @jid)
356
439
  end
357
440
 
358
441
  def tag(*tags)
359
- @client._recur.call([], ['tag', @jid] + tags)
442
+ @client.call('recur.tag', @jid, *tags)
360
443
  end
361
444
 
362
445
  def untag(*tags)
363
- @client._recur.call([], ['untag', @jid] + tags)
446
+ @client.call('recur.untag', @jid, *tags)
447
+ end
448
+
449
+ def last_spawned_jid
450
+ return nil if never_spawned?
451
+ "#{jid}-#{count}"
452
+ end
453
+
454
+ def last_spawned_job
455
+ return nil if never_spawned?
456
+ @client.jobs[last_spawned_jid]
457
+ end
458
+
459
+ private
460
+
461
+ def never_spawned?
462
+ count.zero?
364
463
  end
365
464
  end
366
465
  end