reqless 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,90 @@
1
+ # Encoding: utf-8
2
+
3
+ require 'digest/sha1'
4
+
5
+ module Reqless
6
+ LuaScriptError = Class.new(Reqless::Error)
7
+
8
+ # Wraps a lua script. Knows how to reload it if necessary
9
+ class LuaScript
10
+ DEFAULT_ON_RELOAD_CALLBACK = proc {}
11
+ SCRIPT_ROOT = File.expand_path('../lua', __FILE__)
12
+
13
+ def initialize(name, redis, options = {})
14
+ @name = name
15
+ @on_reload_callback = options[:on_reload_callback] || DEFAULT_ON_RELOAD_CALLBACK
16
+ @redis = redis
17
+ @sha = Digest::SHA1.hexdigest(script_contents)
18
+ end
19
+
20
+ attr_reader :name, :redis, :sha
21
+
22
+ def reload
23
+ @sha = @redis.script(:load, script_contents)
24
+ @on_reload_callback.call(@redis, @sha)
25
+ @sha
26
+ end
27
+
28
+ def call(*argv)
29
+ handle_no_script_error do
30
+ _call(*argv)
31
+ end
32
+ rescue Redis::CommandError => err
33
+ if match = err.message.match('user_script:\d+:\s*(\w+.+$)')
34
+ raise LuaScriptError.new(match[1])
35
+ else
36
+ raise err
37
+ end
38
+ end
39
+
40
+ private
41
+ def _call(*argv)
42
+ @redis.evalsha(@sha, keys: [], argv: argv)
43
+ end
44
+
45
+ def handle_no_script_error
46
+ yield
47
+ rescue ScriptNotLoadedRedisCommandError
48
+ reload
49
+ yield
50
+ end
51
+
52
+ # Module for notifying when a script hasn't yet been loaded
53
+ module ScriptNotLoadedRedisCommandError
54
+ MESSAGE = 'NOSCRIPT No matching script. Please use EVAL.'
55
+
56
+ def self.===(error)
57
+ error.is_a?(Redis::CommandError) && error.message.include?(MESSAGE)
58
+ end
59
+ end
60
+
61
+ def script_contents
62
+ @script_contents ||= File.read(File.join(SCRIPT_ROOT, "#{@name}.lua"))
63
+ end
64
+ end
65
+
66
+ # Provides a simple way to load and use lua-based Reqless plugins.
67
+ # This combines the reqless-lib.lua script plus your custom script
68
+ # contents all into one script, so that your script can use
69
+ # Reqless's lua API.
70
+ class LuaPlugin < LuaScript
71
+ def initialize(name, redis, plugin_contents)
72
+ @name = name
73
+ @redis = redis
74
+ @plugin_contents = plugin_contents.gsub(COMMENT_LINES_RE, '')
75
+ super(name, redis)
76
+ end
77
+
78
+ private
79
+
80
+ def script_contents
81
+ @script_contents ||= [REQLESS_LIB_CONTENTS, @plugin_contents].join("\n\n")
82
+ end
83
+
84
+ COMMENT_LINES_RE = /^\s*--.*$\n?/
85
+
86
+ REQLESS_LIB_CONTENTS = File.read(
87
+ File.join(SCRIPT_ROOT, 'reqless-lib.lua')
88
+ ).gsub(COMMENT_LINES_RE, '')
89
+ end
90
+ end
@@ -0,0 +1,94 @@
1
+ # Encoding: utf-8
2
+
3
+ module Reqless
4
+ module Middleware
5
+ # This middleware is like RetryExceptions, but it doesn't use reqless-core's
6
+ # internal retry/retry-tracking mechanism. Instead, it re-queues the job
7
+ # when it fails with a matched error, and increments a counter in the job's
8
+ # data.
9
+ #
10
+ # This is useful for exceptions for which you want a different
11
+ # backoff/retry strategy. The internal retry mechanism doesn't allow for
12
+ # separate tracking by exception type, and thus doesn't allow you to retry
13
+ # different exceptions a different number of times.
14
+ #
15
+ # This is particularly useful for handling resource throttling errors,
16
+ # where you may not want exponential backoff, and you may want the error
17
+ # to be retried many times, w/o having other transient errors retried so
18
+ # many times.
19
+ module RequeueExceptions
20
+ RequeueableException = Struct.new(:klass, :delay_min, :delay_span, :max_attempts) do
21
+ def self.from_splat_and_options(*klasses, options)
22
+ delay_range = options.fetch(:delay_range)
23
+ delay_min = Float(delay_range.min)
24
+ delay_span = Float(delay_range.max) - Float(delay_range.min)
25
+ max_attempts = options.fetch(:max_attempts)
26
+ klasses.map do |klass|
27
+ new(klass, delay_min, delay_span, max_attempts)
28
+ end
29
+ end
30
+
31
+ def delay
32
+ delay_min + Random.rand(delay_span)
33
+ end
34
+
35
+ def raise_if_exhausted_requeues(error, requeues)
36
+ raise error if requeues >= max_attempts
37
+ end
38
+ end
39
+
40
+ def requeue_on(*exceptions, options)
41
+ RequeueableException.from_splat_and_options(
42
+ *exceptions, options).each do |exc|
43
+ requeueable_exceptions[exc.klass] = exc
44
+ end
45
+ end
46
+
47
+ DEFAULT_ON_REQUEUE_CALLBACK = lambda { |error, job| }
48
+ def use_on_requeue_callback(&block)
49
+ @on_requeue_callback = block if block
50
+ end
51
+
52
+ def on_requeue_callback
53
+ @on_requeue_callback ||= DEFAULT_ON_REQUEUE_CALLBACK
54
+ end
55
+
56
+ def handle_exception(job, error)
57
+ config = requeuable_exception_for(error)
58
+
59
+ requeues_by_exception = (job.data['requeues_by_exception'] ||= {})
60
+ requeues_by_exception[config.klass.name] ||= 0
61
+
62
+ config.raise_if_exhausted_requeues(
63
+ error, requeues_by_exception[config.klass.name])
64
+
65
+ requeues_by_exception[config.klass.name] += 1
66
+ job.requeue(job.queue_name, delay: config.delay, data: job.data)
67
+
68
+ on_requeue_callback.call(error, job)
69
+ end
70
+
71
+ def around_perform(job)
72
+ super
73
+ rescue *requeueable_exceptions.keys => e
74
+ handle_exception(job, e)
75
+ end
76
+
77
+ def requeueable?(exception)
78
+ requeueable_exceptions.member?(exception)
79
+ end
80
+
81
+ def requeueable_exceptions
82
+ @requeueable_exceptions ||= {}
83
+ end
84
+
85
+ def requeuable_exception_for(e)
86
+ requeueable_exceptions.fetch(e.class) do
87
+ requeueable_exceptions.each do |klass, exc|
88
+ break exc if klass === e
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,72 @@
1
+ # Encoding: utf-8
2
+
3
+ module Reqless
4
+ module Middleware
5
+ # Auto-retries particular errors using reqless-core's internal retry tracking
6
+ # mechanism. Supports a backoff strategy (typically exponential).
7
+ #
8
+ # Note: this does not support varying the number of allowed retries by
9
+ # exception type. If you want that kind of flexibility, use the
10
+ # RequeueExceptions middleware instead.
11
+ module RetryExceptions
12
+ def around_perform(job)
13
+ super
14
+ rescue *retryable_exception_classes => error
15
+ raise if job.retries_left <= 0
16
+
17
+ attempt_num = (job.original_retries - job.retries_left) + 1
18
+ failure = Reqless.failure_formatter.format(job, error)
19
+ job.retry(backoff_strategy.call(attempt_num, error), *failure)
20
+
21
+ on_retry_callback.call(error, job)
22
+ end
23
+
24
+ def retryable_exception_classes
25
+ @retryable_exception_classes ||= []
26
+ end
27
+
28
+ def retry_on(*exception_classes)
29
+ retryable_exception_classes.push(*exception_classes)
30
+ end
31
+
32
+ NO_BACKOFF_STRATEGY = ->(_num, _error) { 0 }
33
+
34
+ def use_backoff_strategy(strategy = nil, &block)
35
+ @backoff_strategy = strategy || block
36
+ end
37
+
38
+ def backoff_strategy
39
+ @backoff_strategy ||= NO_BACKOFF_STRATEGY
40
+ end
41
+
42
+ DEFAULT_ON_RETRY_CALLBACK = lambda { |error, job| }
43
+ def use_on_retry_callback(&block)
44
+ @on_retry_callback = block if block
45
+ end
46
+
47
+ def on_retry_callback
48
+ @on_retry_callback ||= DEFAULT_ON_RETRY_CALLBACK
49
+ end
50
+
51
+ # If `factor` is omitted it is set to `delay_seconds` to reproduce legacy
52
+ # behavior.
53
+ def exponential(delay_seconds, options={})
54
+ factor = options.fetch(:factor, delay_seconds)
55
+ fuzz_factor = options.fetch(:fuzz_factor, 0)
56
+
57
+ lambda do |retry_no, error|
58
+ unfuzzed = delay_seconds * factor**(retry_no - 1)
59
+ return unfuzzed if fuzz_factor.zero?
60
+ r = 2 * rand - 1
61
+ # r is uniformly distributed in range [-1, 1]
62
+ unfuzzed * (1 + fuzz_factor * r)
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ # For backwards compatibility
69
+ module RetryExceptions
70
+ include Middleware::RetryExceptions
71
+ end
72
+ end
@@ -0,0 +1,66 @@
1
+ # Encoding: utf-8
2
+
3
+ require 'raven'
4
+
5
+ module Reqless
6
+ module Middleware
7
+ # This middleware logs errors to the sentry exception notification service:
8
+ # http://getsentry.com/
9
+ module Sentry
10
+ def around_perform(job)
11
+ super
12
+ rescue Exception => e
13
+ SentryLogger.new(e, job).log
14
+ raise
15
+ end
16
+
17
+ # Logs a single exception to Sentry, adding pertinent job info.
18
+ class SentryLogger
19
+ def initialize(exception, job)
20
+ @exception, @job = exception, job
21
+ end
22
+
23
+ def log
24
+ event = ::Raven::Event.capture_exception(@exception) do |evt|
25
+ evt.extra = { job: job_metadata }
26
+ end
27
+
28
+ safely_send event
29
+ end
30
+
31
+ private
32
+
33
+ def safely_send(event)
34
+ return unless event
35
+ ::Raven.send(event)
36
+ rescue
37
+ # We don't want to silence our errors when the Sentry server
38
+ # responds with an error. We'll still see the errors on the
39
+ # Reqless Web UI.
40
+ end
41
+
42
+ def job_metadata
43
+ {
44
+ jid: @job.jid,
45
+ klass: @job.klass_name,
46
+ history: job_history,
47
+ data: @job.data,
48
+ queue: @job.queue_name,
49
+ worker: @job.worker_name,
50
+ tags: @job.tags,
51
+ priority: @job.priority
52
+ }
53
+ end
54
+
55
+ # We want to log formatted timestamps rather than integer timestamps
56
+ def job_history
57
+ @job.queue_history.map do |history_event|
58
+ history_event.each_with_object({}) do |(key, value), hash|
59
+ hash[key] = value.is_a?(Time) ? value.iso8601 : value
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,63 @@
1
+ require 'timeout'
2
+ require 'reqless/middleware/requeue_exceptions'
3
+
4
+ module Reqless
5
+ # Unique error class used when a job is timed out by this middleware.
6
+ # Allows us to differentiate this timeout from others caused by `::Timeout::Erorr`
7
+ JobTimedoutError = Class.new(StandardError)
8
+ InvalidTimeoutError = Class.new(ArgumentError)
9
+
10
+ module Middleware
11
+ # Applies a hard time out. To use this middleware, instantiate it and pass a block; the block
12
+ # will be passed the job object (which has a `ttl` method for getting the job's remaining TTL),
13
+ # and the block should return the desired timeout in seconds.
14
+ # This allows you to set a hard constant time out to a particular job class
15
+ # (using something like `extend Reqless::Middleware::Timeout.new { 60 * 60 }`),
16
+ # or a variable timeout based on the individual TTLs of each job
17
+ # (using something like `extend Reqless::Middleware::Timeout.new { |job| job.ttl * 1.1 }`).
18
+ class Timeout < Module
19
+ def initialize(opts = {})
20
+ timeout_class = opts.fetch(:timeout_class, ::Timeout)
21
+ kernel_class = opts.fetch(:kernel_class, Kernel)
22
+ module_eval do
23
+ define_method :around_perform do |job|
24
+ timeout_seconds = yield job
25
+
26
+ return super(job) if timeout_seconds.nil?
27
+
28
+ if !timeout_seconds.is_a?(Numeric) || timeout_seconds <= 0
29
+ raise InvalidTimeoutError, "Timeout must be a positive number or nil, " \
30
+ "but was #{timeout_seconds}"
31
+ end
32
+
33
+ begin
34
+ timeout_class.timeout(timeout_seconds) { super(job) }
35
+ rescue ::Timeout::Error => e
36
+ error = JobTimedoutError.new(e.message)
37
+ error.set_backtrace(e.backtrace)
38
+ # The stalled connection to redis might be the cause of the timeout. We cannot rely
39
+ # on state of connection either (e.g., we might be in the middle of Redis call when
40
+ # timeout happend). To play it safe, we reconnect.
41
+ job.fail(*Reqless.failure_formatter.format(job, error, []))
42
+ # Since we are leaving with bang (exit!), normal requeue logic does not work.
43
+ # Do it manually right here.
44
+ if self.is_a?(::Reqless::Middleware::RequeueExceptions) &&
45
+ self.requeueable?(JobTimedoutError)
46
+ self.handle_exception(job, error)
47
+ end
48
+
49
+ # ::Timeout.timeout is dangerous to use as it can leave things in an inconsistent
50
+ # state. With Redis, for example, we've seen the socket buffer left with unread bytes
51
+ # on it, which can affect later redis calls. Thus, it's much safer just to exit, and
52
+ # allow the parent process to restart the worker in a known, clean state.
53
+ #
54
+ # We use 73 as a unique exit status for this case. 73 looks
55
+ # a bit like TE (Timeout::Error)
56
+ kernel_class.exit!(73)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,189 @@
1
+ # Encoding: utf-8
2
+
3
+ require 'reqless/job'
4
+ require 'redis'
5
+ require 'json'
6
+
7
+ module Reqless
8
+ # A class for interacting with jobs in different states in a queue. Not meant
9
+ # to be instantiated directly, it's accessed with Queue#jobs
10
+ class QueueJobs
11
+ def initialize(name, client)
12
+ @name = name
13
+ @client = client
14
+ end
15
+
16
+ def running(start = 0, count = 25)
17
+ JSON.parse(@client.call('queue.jobsByState', 'running', @name, start, count))
18
+ end
19
+
20
+ def throttled(start = 0, count = 25)
21
+ JSON.parse(@client.call('queue.jobsByState', 'throttled', @name, start, count))
22
+ end
23
+
24
+ def stalled(start = 0, count = 25)
25
+ JSON.parse(@client.call('queue.jobsByState', 'stalled', @name, start, count))
26
+ end
27
+
28
+ def scheduled(start = 0, count = 25)
29
+ JSON.parse(@client.call('queue.jobsByState', 'scheduled', @name, start, count))
30
+ end
31
+
32
+ def depends(start = 0, count = 25)
33
+ JSON.parse(@client.call('queue.jobsByState', 'depends', @name, start, count))
34
+ end
35
+
36
+ def recurring(start = 0, count = 25)
37
+ JSON.parse(@client.call('queue.jobsByState', 'recurring', @name, start, count))
38
+ end
39
+ end
40
+
41
+ # A class for interacting with a specific queue. Not meant to be instantiated
42
+ # directly, it's accessed with Client#queues[...]
43
+ class Queue
44
+ attr_reader :name, :client
45
+
46
+ def initialize(name, client)
47
+ @client = client
48
+ @name = name
49
+ end
50
+
51
+ # Our worker name is the same as our client's
52
+ def worker_name
53
+ @client.worker_name
54
+ end
55
+
56
+ def jobs
57
+ @jobs ||= QueueJobs.new(@name, @client)
58
+ end
59
+
60
+ def counts
61
+ JSON.parse(@client.call('queue.counts', @name))
62
+ end
63
+
64
+ def heartbeat
65
+ get_config :heartbeat
66
+ end
67
+
68
+ def heartbeat=(value)
69
+ set_config :heartbeat, value
70
+ end
71
+
72
+ def throttle
73
+ @throttle ||= Reqless::Throttle.new("ql:q:#{name}", client)
74
+ end
75
+
76
+ def paused?
77
+ counts['paused']
78
+ end
79
+
80
+ def pause(opts = {})
81
+ @client.call('queue.pause', name)
82
+ @client.call('job.timeout', jobs.running(0, -1)) unless opts[:stopjobs].nil?
83
+ end
84
+
85
+ def unpause
86
+ @client.call('queue.unpause', name)
87
+ end
88
+
89
+ # Put the described job in this queue
90
+ # Options include:
91
+ # => priority (int)
92
+ # => tags (array of strings)
93
+ # => delay (int)
94
+ # => throttles (array of strings)
95
+ def put(klass, data, opts = {})
96
+ opts = job_options(klass, data, opts)
97
+ @client.call(
98
+ 'queue.put',
99
+ worker_name, @name,
100
+ (opts[:jid] || Reqless.generate_jid),
101
+ klass.is_a?(String) ? klass : klass.name,
102
+ *Job.build_opts_array(opts.merge(:data => data)),
103
+ )
104
+ end
105
+
106
+ # Make a recurring job in this queue
107
+ # Options include:
108
+ # => priority (int)
109
+ # => tags (array of strings)
110
+ # => retries (int)
111
+ # => offset (int)
112
+ def recur(klass, data, interval, opts = {})
113
+ opts = job_options(klass, data, opts)
114
+ @client.call(
115
+ 'queue.recurAtInterval',
116
+ @name,
117
+ (opts[:jid] || Reqless.generate_jid),
118
+ klass.is_a?(String) ? klass : klass.name,
119
+ JSON.generate(data),
120
+ interval, opts.fetch(:offset, 0),
121
+ 'priority', opts.fetch(:priority, 0),
122
+ 'tags', JSON.generate(opts.fetch(:tags, [])),
123
+ 'retries', opts.fetch(:retries, 5),
124
+ 'backlog', opts.fetch(:backlog, 0)
125
+ )
126
+ end
127
+
128
+ # Pop a work item off the queue
129
+ def pop(count = nil)
130
+ jids = JSON.parse(@client.call('queue.pop', @name, worker_name, (count || 1)))
131
+ jobs = jids.map { |j| Job.new(@client, j) }
132
+ count.nil? ? jobs[0] : jobs
133
+ end
134
+
135
+ # Peek at a work item
136
+ def peek(offset_or_count = nil, count = nil)
137
+ actual_offset = offset_or_count && count ? offset_or_count : 0
138
+ actual_count = offset_or_count && count ? count : (offset_or_count || 1)
139
+ return_single_job = offset_or_count.nil? && count.nil?
140
+ jids = JSON.parse(@client.call('queue.peek', @name, actual_offset, actual_count))
141
+ jobs = jids.map { |j| Job.new(@client, j) }
142
+ return_single_job ? jobs[0] : jobs
143
+ end
144
+
145
+ def stats(date = nil)
146
+ JSON.parse(@client.call('queue.stats', @name, (date || Time.now.to_f)))
147
+ end
148
+
149
+ # How many items in the queue?
150
+ def length
151
+ (@client.redis.multi do |pipeline|
152
+ pipeline.zcard("ql:q:#{@name}-locks")
153
+ pipeline.zcard("ql:q:#{@name}-work")
154
+ pipeline.zcard("ql:q:#{@name}-scheduled")
155
+ end).inject(0, :+)
156
+ end
157
+
158
+ def to_s
159
+ "#<Reqless::Queue #{@name}>"
160
+ end
161
+ alias_method :inspect, :to_s
162
+
163
+ def ==(other)
164
+ self.class == other.class &&
165
+ client == other.client &&
166
+ name.to_s == other.name.to_s
167
+ end
168
+ alias eql? ==
169
+
170
+ def hash
171
+ self.class.hash ^ client.hash ^ name.to_s.hash
172
+ end
173
+
174
+ private
175
+
176
+ def job_options(klass, data, opts)
177
+ return opts unless klass.respond_to?(:default_job_options)
178
+ klass.default_job_options(data).merge(opts)
179
+ end
180
+
181
+ def set_config(config, value)
182
+ @client.config["#{@name}-#{config}"] = value
183
+ end
184
+
185
+ def get_config(config)
186
+ @client.config["#{@name}-#{config}"]
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,16 @@
1
+ module Reqless
2
+ class QueuePriorityPattern
3
+ attr_reader :pattern, :should_distribute_fairly
4
+
5
+ def initialize(pattern, should_distribute_fairly = false)
6
+ @pattern = pattern
7
+ @should_distribute_fairly = should_distribute_fairly
8
+ end
9
+
10
+ def ==(other)
11
+ return self.class == other.class &&
12
+ self.pattern.join == other.pattern.join &&
13
+ self.should_distribute_fairly == other.should_distribute_fairly
14
+ end
15
+ end
16
+ end