qless 0.9.3 → 0.10.0

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 (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