reqless 0.0.1

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 (81) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +8 -0
  3. data/README.md +648 -0
  4. data/Rakefile +117 -0
  5. data/bin/docker-build-and-test +22 -0
  6. data/exe/reqless-web +11 -0
  7. data/lib/reqless/config.rb +31 -0
  8. data/lib/reqless/failure_formatter.rb +43 -0
  9. data/lib/reqless/job.rb +496 -0
  10. data/lib/reqless/job_reservers/ordered.rb +29 -0
  11. data/lib/reqless/job_reservers/round_robin.rb +46 -0
  12. data/lib/reqless/job_reservers/shuffled_round_robin.rb +21 -0
  13. data/lib/reqless/lua/reqless-lib.lua +2965 -0
  14. data/lib/reqless/lua/reqless.lua +2545 -0
  15. data/lib/reqless/lua_script.rb +90 -0
  16. data/lib/reqless/middleware/requeue_exceptions.rb +94 -0
  17. data/lib/reqless/middleware/retry_exceptions.rb +72 -0
  18. data/lib/reqless/middleware/sentry.rb +66 -0
  19. data/lib/reqless/middleware/timeout.rb +63 -0
  20. data/lib/reqless/queue.rb +189 -0
  21. data/lib/reqless/queue_priority_pattern.rb +16 -0
  22. data/lib/reqless/server/static/css/bootstrap-responsive.css +686 -0
  23. data/lib/reqless/server/static/css/bootstrap-responsive.min.css +12 -0
  24. data/lib/reqless/server/static/css/bootstrap.css +3991 -0
  25. data/lib/reqless/server/static/css/bootstrap.min.css +689 -0
  26. data/lib/reqless/server/static/css/codemirror.css +112 -0
  27. data/lib/reqless/server/static/css/docs.css +839 -0
  28. data/lib/reqless/server/static/css/jquery.noty.css +105 -0
  29. data/lib/reqless/server/static/css/noty_theme_twitter.css +137 -0
  30. data/lib/reqless/server/static/css/style.css +200 -0
  31. data/lib/reqless/server/static/favicon.ico +0 -0
  32. data/lib/reqless/server/static/img/glyphicons-halflings-white.png +0 -0
  33. data/lib/reqless/server/static/img/glyphicons-halflings.png +0 -0
  34. data/lib/reqless/server/static/js/bootstrap-alert.js +94 -0
  35. data/lib/reqless/server/static/js/bootstrap-scrollspy.js +125 -0
  36. data/lib/reqless/server/static/js/bootstrap-tab.js +130 -0
  37. data/lib/reqless/server/static/js/bootstrap-tooltip.js +270 -0
  38. data/lib/reqless/server/static/js/bootstrap-typeahead.js +285 -0
  39. data/lib/reqless/server/static/js/bootstrap.js +1726 -0
  40. data/lib/reqless/server/static/js/bootstrap.min.js +6 -0
  41. data/lib/reqless/server/static/js/codemirror.js +2972 -0
  42. data/lib/reqless/server/static/js/jquery.noty.js +220 -0
  43. data/lib/reqless/server/static/js/mode/javascript.js +360 -0
  44. data/lib/reqless/server/static/js/theme/cobalt.css +18 -0
  45. data/lib/reqless/server/static/js/theme/eclipse.css +25 -0
  46. data/lib/reqless/server/static/js/theme/elegant.css +10 -0
  47. data/lib/reqless/server/static/js/theme/lesser-dark.css +45 -0
  48. data/lib/reqless/server/static/js/theme/monokai.css +28 -0
  49. data/lib/reqless/server/static/js/theme/neat.css +9 -0
  50. data/lib/reqless/server/static/js/theme/night.css +21 -0
  51. data/lib/reqless/server/static/js/theme/rubyblue.css +21 -0
  52. data/lib/reqless/server/static/js/theme/xq-dark.css +46 -0
  53. data/lib/reqless/server/views/_job.erb +259 -0
  54. data/lib/reqless/server/views/_job_list.erb +8 -0
  55. data/lib/reqless/server/views/_pagination.erb +7 -0
  56. data/lib/reqless/server/views/about.erb +130 -0
  57. data/lib/reqless/server/views/completed.erb +11 -0
  58. data/lib/reqless/server/views/config.erb +14 -0
  59. data/lib/reqless/server/views/failed.erb +48 -0
  60. data/lib/reqless/server/views/failed_type.erb +18 -0
  61. data/lib/reqless/server/views/job.erb +17 -0
  62. data/lib/reqless/server/views/layout.erb +451 -0
  63. data/lib/reqless/server/views/overview.erb +137 -0
  64. data/lib/reqless/server/views/queue.erb +125 -0
  65. data/lib/reqless/server/views/queues.erb +45 -0
  66. data/lib/reqless/server/views/tag.erb +6 -0
  67. data/lib/reqless/server/views/throttles.erb +38 -0
  68. data/lib/reqless/server/views/track.erb +75 -0
  69. data/lib/reqless/server/views/worker.erb +34 -0
  70. data/lib/reqless/server/views/workers.erb +14 -0
  71. data/lib/reqless/server.rb +549 -0
  72. data/lib/reqless/subscriber.rb +74 -0
  73. data/lib/reqless/test_helpers/worker_helpers.rb +55 -0
  74. data/lib/reqless/throttle.rb +57 -0
  75. data/lib/reqless/version.rb +5 -0
  76. data/lib/reqless/worker/base.rb +237 -0
  77. data/lib/reqless/worker/forking.rb +215 -0
  78. data/lib/reqless/worker/serial.rb +41 -0
  79. data/lib/reqless/worker.rb +5 -0
  80. data/lib/reqless.rb +309 -0
  81. metadata +399 -0
@@ -0,0 +1,496 @@
1
+ # Encoding: utf-8
2
+
3
+ require 'reqless'
4
+ require 'reqless/queue'
5
+ require 'reqless/lua_script'
6
+ require 'redis'
7
+ require 'json'
8
+
9
+ module Reqless
10
+ # The base for both Job and RecurringJob
11
+ class BaseJob
12
+ attr_reader :client
13
+
14
+ def initialize(client, jid)
15
+ @client = client
16
+ @jid = jid
17
+ end
18
+
19
+ def klass
20
+ @klass ||= @klass_name.split('::').reduce(Object) do |context, name|
21
+ context.const_get(name, false)
22
+ end
23
+ end
24
+
25
+ def queue
26
+ @queue ||= Queue.new(@queue_name, @client)
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
39
+ end
40
+
41
+ # A Reqless job
42
+ class Job < BaseJob
43
+ attr_reader :jid, :expires_at, :state, :queue_name, :worker_name, :failure, :spawned_from_jid
44
+ attr_reader :klass_name, :tracked, :dependencies, :dependents
45
+ attr_reader :original_retries, :retries_left, :raw_queue_history
46
+ attr_reader :state_changed
47
+ attr_accessor :data, :priority, :tags, :throttles
48
+
49
+ alias_method(:state_changed?, :state_changed)
50
+
51
+ MiddlewareMisconfiguredError = Class.new(StandardError)
52
+
53
+ module SupportsMiddleware
54
+ def around_perform(job)
55
+ perform(job)
56
+ end
57
+ end
58
+
59
+ def perform
60
+ # If we can't find the class, we should fail the job, not try to process
61
+ begin
62
+ klass
63
+ rescue NameError
64
+ return fail("#{queue_name}-NameError", "Cannot find #{klass_name}")
65
+ end
66
+
67
+ # log a real process executing job -- before we start processing
68
+ log("started by pid:#{Process.pid}")
69
+
70
+ middlewares = Job.middlewares_on(klass)
71
+
72
+ if middlewares.last == SupportsMiddleware
73
+ klass.around_perform(self)
74
+ elsif middlewares.any?
75
+ raise MiddlewareMisconfiguredError, 'The middleware chain for ' +
76
+ "#{klass} (#{middlewares.inspect}) is misconfigured." +
77
+ 'Reqless::Job::SupportsMiddleware must be extended onto your job' +
78
+ 'class first if you want to use any middleware.'
79
+ elsif !klass.respond_to?(:perform)
80
+ # If the klass doesn't have a :perform method, we should raise an error
81
+ fail("#{queue_name}-method-missing",
82
+ "#{klass_name} has no perform method")
83
+ else
84
+ klass.perform(self)
85
+ end
86
+ end
87
+
88
+ def self.build(client, klass, attributes = {})
89
+ defaults = {
90
+ 'jid' => Reqless.generate_jid,
91
+ 'spawned_from_jid' => nil,
92
+ 'data' => {},
93
+ 'klass' => klass.to_s,
94
+ 'priority' => 0,
95
+ 'tags' => [],
96
+ 'worker' => 'mock_worker',
97
+ 'expires' => Time.now + (60 * 60), # an hour from now
98
+ 'state' => 'running',
99
+ 'tracked' => false,
100
+ 'queue' => 'mock_queue',
101
+ 'retries' => 5,
102
+ 'remaining' => 5,
103
+ 'failure' => {},
104
+ 'history' => [],
105
+ 'dependencies' => [],
106
+ 'dependents' => [],
107
+ 'throttles' => [],
108
+ }
109
+ attributes = defaults.merge(Reqless.stringify_hash_keys(attributes))
110
+ attributes['data'] = JSON.dump(attributes['data'])
111
+ new(client, attributes)
112
+ end
113
+
114
+ # Converts a hash of job options (as returned by job.to_hash) into the array
115
+ # format the reqless api expects.
116
+ def self.build_opts_array(opts)
117
+ result = []
118
+ result << JSON.generate(opts.fetch(:data, {}))
119
+ result.concat([opts.fetch(:delay, 0)])
120
+ result.concat(['priority', opts.fetch(:priority, 0)])
121
+ result.concat(['tags', JSON.generate(opts.fetch(:tags, []))])
122
+ result.concat(['retries', opts.fetch(:retries, 5)])
123
+ result.concat(['depends', JSON.generate(opts.fetch(:depends, []))])
124
+ result.concat(['throttles', JSON.generate(opts.fetch(:throttles, []))])
125
+ end
126
+
127
+ def self.middlewares_on(job_klass)
128
+ singleton_klass = job_klass.singleton_class
129
+ singleton_klass.ancestors.select do |ancestor|
130
+ ancestor != singleton_klass && ancestor.method_defined?(:around_perform)
131
+ end
132
+ end
133
+
134
+ def initialize(client, atts)
135
+ super(client, atts.fetch('jid'))
136
+ %w{
137
+ data failure dependencies dependents jid priority state tags throttles
138
+ tracked
139
+ }.each do |att|
140
+ instance_variable_set(:"@#{att}", atts.fetch(att))
141
+ # Redis doesn't handle nil values so well, sometimes instead returning false,
142
+ # so massage spawned_by_jid to consistent be nil or a jid
143
+ @spawned_from_jid = atts.fetch('spawned_from_jid', nil) || nil
144
+ end
145
+
146
+ # Parse the data string
147
+ @data = JSON.parse(@data)
148
+
149
+ @expires_at = atts.fetch('expires')
150
+ @klass_name = atts.fetch('klass')
151
+ @queue_name = atts.fetch('queue')
152
+ @worker_name = atts.fetch('worker')
153
+ @original_retries = atts.fetch('retries')
154
+ @retries_left = atts.fetch('remaining')
155
+ @raw_queue_history = atts.fetch('history')
156
+
157
+ # This is a silly side-effect of Lua doing JSON serialization
158
+ @tags = [] if @tags == {}
159
+ @dependents = [] if @dependents == {}
160
+ @dependencies = [] if @dependencies == {}
161
+ @state_changed = false
162
+ @before_callbacks = Hash.new { |h, k| h[k] = [] }
163
+ @after_callbacks = Hash.new { |h, k| h[k] = [] }
164
+ end
165
+
166
+ def priority=(priority)
167
+ @priority = priority if @client.call('job.setPriority', @jid, priority)
168
+ end
169
+
170
+ def [](key)
171
+ @data[key]
172
+ end
173
+
174
+ def []=(key, val)
175
+ @data[key] = val
176
+ end
177
+
178
+ def to_s
179
+ inspect
180
+ end
181
+
182
+ def description
183
+ "#{@klass_name} (#{@jid} / #{@queue_name} / #{@state})"
184
+ end
185
+
186
+ def inspect
187
+ "<Reqless::Job #{description}>"
188
+ end
189
+
190
+ def ttl
191
+ @expires_at - Time.now.to_f
192
+ end
193
+
194
+ def throttle_objects
195
+ throttles.map { |name| Throttle.new(name, client) }
196
+ end
197
+
198
+ def history
199
+ warn 'WARNING: Reqless::Job#history is deprecated; use' +
200
+ "Reqless::Job#raw_queue_history instead; from:\n#{caller.first}"
201
+ raw_queue_history
202
+ end
203
+
204
+ def queue_history
205
+ @queue_history ||= @raw_queue_history.map do |history_event|
206
+ history_event.each_with_object({}) do |(key, value), hash|
207
+ # The only Numeric (Integer or Float) values we get in the history
208
+ # are timestamps
209
+ if value.is_a?(Numeric)
210
+ hash[key] = Time.at(value).utc
211
+ else
212
+ hash[key] = value
213
+ end
214
+ end
215
+ end
216
+ end
217
+
218
+ def initially_put_at
219
+ @initially_put_at ||= history_timestamp('put', :min)
220
+ end
221
+
222
+ def spawned_from
223
+ return nil if @spawned_from_jid.nil?
224
+ @spawned_from ||= @client.jobs[@spawned_from_jid]
225
+ end
226
+
227
+ def to_hash
228
+ {
229
+ jid: jid,
230
+ spawned_from_jid: spawned_from_jid,
231
+ expires_at: expires_at,
232
+ state: state,
233
+ queue_name: queue_name,
234
+ history: raw_queue_history,
235
+ worker_name: worker_name,
236
+ failure: failure,
237
+ klass_name: klass_name,
238
+ tracked: tracked,
239
+ dependencies: dependencies,
240
+ dependents: dependents,
241
+ original_retries: original_retries,
242
+ retries_left: retries_left,
243
+ data: data,
244
+ priority: priority,
245
+ tags: tags,
246
+ throttles: throttles,
247
+ }
248
+ end
249
+
250
+ # Extract the enqueue options from the job
251
+ # @return [Hash] options
252
+ # @option options [Integer] :retries
253
+ # @option options [Integer] :priority
254
+ # @option options [Array<String>] :depends
255
+ # @option options [Array<String>] :tags
256
+ # @option options [Array<String>] throttles
257
+ # @option options [Hash] :data
258
+ def enqueue_opts
259
+ {
260
+ retries: original_retries,
261
+ priority: priority,
262
+ depends: dependents,
263
+ tags: tags,
264
+ throttles: throttles,
265
+ data: data,
266
+ }
267
+ end
268
+
269
+ # Move this from it's current queue into another
270
+ def requeue(queue, opts = {})
271
+ note_state_change :requeue do
272
+ @client.call('job.requeue', @client.worker_name, queue, @jid, @klass_name,
273
+ *self.class.build_opts_array(self.enqueue_opts.merge!(opts))
274
+ )
275
+ end
276
+ end
277
+ alias move requeue # for backwards compatibility
278
+
279
+ CantFailError = Class.new(Reqless::LuaScriptError)
280
+
281
+ # Fail a job
282
+ def fail(group, message)
283
+ note_state_change :fail do
284
+ @client.call(
285
+ 'job.fail',
286
+ @jid,
287
+ @worker_name,
288
+ group, message,
289
+ JSON.dump(@data)) || false
290
+ end
291
+ rescue Reqless::LuaScriptError => err
292
+ raise CantFailError.new(err.message)
293
+ end
294
+
295
+ # Heartbeat a job
296
+ def heartbeat
297
+ @expires_at = @client.call(
298
+ 'job.heartbeat',
299
+ @jid,
300
+ @worker_name,
301
+ JSON.dump(@data))
302
+ end
303
+
304
+ CantCompleteError = Class.new(Reqless::LuaScriptError)
305
+
306
+ # Complete a job
307
+ # Options include
308
+ # => next (String) the next queue
309
+ # => delay (int) how long to delay it in the next queue
310
+ def complete(nxt = nil, options = {})
311
+ note_state_change :complete do
312
+ if nxt.nil?
313
+ @client.call(
314
+ 'job.complete', @jid, @worker_name, @queue_name, JSON.dump(@data))
315
+ else
316
+ @client.call('job.completeAndRequeue', @jid, @worker_name, @queue_name,
317
+ JSON.dump(@data), 'next', nxt, 'delay',
318
+ options.fetch(:delay, 0), 'depends',
319
+ JSON.dump(options.fetch(:depends, [])))
320
+ end
321
+ end
322
+ rescue Reqless::LuaScriptError => err
323
+ raise CantCompleteError.new(err.message)
324
+ end
325
+
326
+ def cancel
327
+ note_state_change :cancel do
328
+ @client.call('job.cancel', @jid)
329
+ end
330
+ end
331
+
332
+ def track
333
+ @client.call('job.track', @jid)
334
+ end
335
+
336
+ def untrack
337
+ @client.call('job.untrack', @jid)
338
+ end
339
+
340
+ def tag(*tags)
341
+ JSON.parse(@client.call('job.addTag', @jid, *tags))
342
+ end
343
+
344
+ def untag(*tags)
345
+ JSON.parse(@client.call('job.removeTag', @jid, *tags))
346
+ end
347
+
348
+ def retry(delay = 0, group = nil, message = nil)
349
+ note_state_change :retry do
350
+ if group.nil?
351
+ results = @client.call(
352
+ 'job.retry', @jid, @queue_name, @worker_name, delay)
353
+ results.nil? ? false : results
354
+ else
355
+ results = @client.call(
356
+ 'job.retry', @jid, @queue_name, @worker_name, delay, group, message)
357
+ results.nil? ? false : results
358
+ end
359
+ end
360
+ end
361
+
362
+ def depend(*jids)
363
+ !!@client.call('job.addDependency', @jid, *jids)
364
+ end
365
+
366
+ def undepend(*jids)
367
+ !!@client.call('job.removeDependency', @jid, *jids)
368
+ end
369
+
370
+ def timeout
371
+ @client.call('job.timeout', @jid)
372
+ end
373
+
374
+ def log(message, data = nil)
375
+ if data
376
+ @client.call('job.log', @jid, message, JSON.dump(data))
377
+ else
378
+ @client.call('job.log', @jid, message)
379
+ end
380
+ end
381
+
382
+ [:fail, :complete, :cancel, :requeue, :retry].each do |event|
383
+ define_method :"before_#{event}" do |&block|
384
+ @before_callbacks[event] << block
385
+ end
386
+
387
+ define_method :"after_#{event}" do |&block|
388
+ @after_callbacks[event].unshift block
389
+ end
390
+ end
391
+ alias before_move before_requeue
392
+ alias after_move after_requeue
393
+
394
+ def note_state_change(event)
395
+ @before_callbacks[event].each { |blk| blk.call(self) }
396
+ result = yield
397
+ @state_changed = true
398
+ @after_callbacks[event].each { |blk| blk.call(self) }
399
+ result
400
+ end
401
+
402
+ private
403
+
404
+ def history_timestamp(name, selector)
405
+ items = queue_history.select do |q|
406
+ q['what'] == name
407
+ end
408
+ items.map do |q|
409
+ q['when']
410
+ end.public_send(selector)
411
+ end
412
+ end
413
+
414
+ # Wraps a recurring job
415
+ class RecurringJob < BaseJob
416
+ attr_reader :jid, :data, :priority, :tags, :retries, :interval, :count
417
+ attr_reader :queue_name, :klass_name, :backlog
418
+
419
+ def initialize(client, atts)
420
+ super(client, atts.fetch('jid'))
421
+ %w{jid data priority tags retries interval count backlog}.each do |att|
422
+ instance_variable_set("@#{att}".to_sym, atts.fetch(att))
423
+ end
424
+
425
+ # Parse the data string
426
+ @data = JSON.parse(@data)
427
+ @klass_name = atts.fetch('klass')
428
+ @queue_name = atts.fetch('queue')
429
+ @tags = [] if @tags == {}
430
+ end
431
+
432
+ def priority=(value)
433
+ @client.call('recurringJob.update', @jid, 'priority', value)
434
+ @priority = value
435
+ end
436
+
437
+ def retries=(value)
438
+ @client.call('recurringJob.update', @jid, 'retries', value)
439
+ @retries = value
440
+ end
441
+
442
+ def interval=(value)
443
+ @client.call('recurringJob.update', @jid, 'interval', value)
444
+ @interval = value
445
+ end
446
+
447
+ def data=(value)
448
+ @client.call('recurringJob.update', @jid, 'data', JSON.dump(value))
449
+ @data = value
450
+ end
451
+
452
+ def klass=(value)
453
+ @client.call('recurringJob.update', @jid, 'klass', value.to_s)
454
+ @klass_name = value.to_s
455
+ end
456
+
457
+ def backlog=(value)
458
+ @client.call('recurringJob.update', @jid, 'backlog', value.to_s)
459
+ @backlog = value
460
+ end
461
+
462
+ def move(queue)
463
+ @client.call('recurringJob.update', @jid, 'queue', queue)
464
+ @queue_name = queue
465
+ end
466
+ alias requeue move # for API parity with normal jobs
467
+
468
+ def cancel
469
+ @client.call('recurringJob.cancel', @jid)
470
+ end
471
+
472
+ def tag(*tags)
473
+ @client.call('recurringJob.addTag', @jid, *tags)
474
+ end
475
+
476
+ def untag(*tags)
477
+ @client.call('recurringJob.removeTag', @jid, *tags)
478
+ end
479
+
480
+ def last_spawned_jid
481
+ return nil if never_spawned?
482
+ "#{jid}-#{count}"
483
+ end
484
+
485
+ def last_spawned_job
486
+ return nil if never_spawned?
487
+ @client.jobs[last_spawned_jid]
488
+ end
489
+
490
+ private
491
+
492
+ def never_spawned?
493
+ count.zero?
494
+ end
495
+ end
496
+ end
@@ -0,0 +1,29 @@
1
+ # Encoding: utf-8
2
+
3
+ module Reqless
4
+ module JobReservers
5
+ class Ordered
6
+ attr_reader :queues
7
+
8
+ def initialize(queues)
9
+ @queues = queues
10
+ end
11
+
12
+ def reserve
13
+ @queues.each do |q|
14
+ job = q.pop
15
+ return job if job
16
+ end
17
+ nil
18
+ end
19
+
20
+ def prep_for_work!
21
+ # nothing here on purpose
22
+ end
23
+
24
+ def description
25
+ @description ||= @queues.map(&:name).join(', ') + ' (ordered)'
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ # Encoding: utf-8
2
+
3
+ module Reqless
4
+ module JobReservers
5
+ # Round-robins through all the provided queues
6
+ class RoundRobin
7
+ attr_reader :queues
8
+
9
+ def initialize(queues)
10
+ @queues = queues
11
+ @num_queues = queues.size
12
+ @last_popped_queue_index = @num_queues - 1
13
+ end
14
+
15
+ def reserve
16
+ @num_queues.times do |i|
17
+ job = next_queue.pop
18
+ return job if job
19
+ end
20
+ nil
21
+ end
22
+
23
+ def prep_for_work!
24
+ # nothing here on purpose
25
+ end
26
+
27
+ def description
28
+ @description ||=
29
+ @queues.map(&:name).join(', ') + " (#{self.class::TYPE_DESCRIPTION})"
30
+ end
31
+
32
+ def reset_description!
33
+ @description = nil
34
+ end
35
+
36
+ private
37
+
38
+ TYPE_DESCRIPTION = 'round robin'
39
+
40
+ def next_queue
41
+ @last_popped_queue_index = (@last_popped_queue_index + 1) % @num_queues
42
+ @queues[@last_popped_queue_index]
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,21 @@
1
+ # Encoding: utf-8
2
+
3
+ require 'reqless/job_reservers/round_robin'
4
+
5
+ module Reqless
6
+ module JobReservers
7
+ # Like round-robin but shuffles the order of the queues
8
+ class ShuffledRoundRobin < RoundRobin
9
+ def initialize(queues)
10
+ super(queues.shuffle)
11
+ end
12
+
13
+ def prep_for_work!
14
+ @queues = @queues.shuffle
15
+ reset_description!
16
+ end
17
+
18
+ TYPE_DESCRIPTION = 'shuffled round robin'
19
+ end
20
+ end
21
+ end