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.
- data/Gemfile +2 -0
- data/README.md +42 -3
- data/Rakefile +26 -2
- data/{bin → exe}/qless-web +3 -2
- data/lib/qless.rb +55 -28
- data/lib/qless/config.rb +1 -3
- data/lib/qless/job.rb +127 -22
- data/lib/qless/job_reservers/round_robin.rb +3 -1
- data/lib/qless/job_reservers/shuffled_round_robin.rb +14 -0
- data/lib/qless/lua_script.rb +42 -0
- data/lib/qless/middleware/redis_reconnect.rb +24 -0
- data/lib/qless/middleware/retry_exceptions.rb +43 -0
- data/lib/qless/middleware/sentry.rb +70 -0
- data/lib/qless/qless-core/cancel.lua +89 -59
- data/lib/qless/qless-core/complete.lua +16 -1
- data/lib/qless/qless-core/config.lua +12 -0
- data/lib/qless/qless-core/deregister_workers.lua +12 -0
- data/lib/qless/qless-core/fail.lua +24 -14
- data/lib/qless/qless-core/heartbeat.lua +2 -1
- data/lib/qless/qless-core/pause.lua +18 -0
- data/lib/qless/qless-core/pop.lua +24 -3
- data/lib/qless/qless-core/put.lua +14 -1
- data/lib/qless/qless-core/qless-lib.lua +2354 -0
- data/lib/qless/qless-core/qless.lua +1862 -0
- data/lib/qless/qless-core/retry.lua +1 -1
- data/lib/qless/qless-core/unfail.lua +54 -0
- data/lib/qless/qless-core/unpause.lua +12 -0
- data/lib/qless/queue.rb +45 -21
- data/lib/qless/server.rb +38 -39
- data/lib/qless/server/static/css/docs.css +21 -1
- data/lib/qless/server/views/_job.erb +5 -5
- data/lib/qless/server/views/overview.erb +14 -9
- data/lib/qless/subscriber.rb +48 -0
- data/lib/qless/version.rb +1 -1
- data/lib/qless/wait_until.rb +19 -0
- data/lib/qless/worker.rb +243 -33
- metadata +49 -30
- data/bin/install_phantomjs +0 -7
- data/bin/qless-campfire +0 -106
- data/bin/qless-growl +0 -99
- data/lib/qless/lua.rb +0 -25
@@ -19,11 +19,13 @@ module Qless
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def description
|
22
|
-
@description ||= @queues.map(&:name).join(', ') + " (
|
22
|
+
@description ||= @queues.map(&:name).join(', ') + " (#{self.class::TYPE_DESCRIPTION})"
|
23
23
|
end
|
24
24
|
|
25
25
|
private
|
26
26
|
|
27
|
+
TYPE_DESCRIPTION = "round robin"
|
28
|
+
|
27
29
|
def next_queue
|
28
30
|
@last_popped_queue_index = (@last_popped_queue_index + 1) % @num_queues
|
29
31
|
@queues[@last_popped_queue_index]
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
|
3
|
+
module Qless
|
4
|
+
class LuaScript
|
5
|
+
LUA_SCRIPT_DIR = File.expand_path("../qless-core/", __FILE__)
|
6
|
+
|
7
|
+
def initialize(name, redis)
|
8
|
+
@name = name
|
9
|
+
@redis = redis
|
10
|
+
@sha = Digest::SHA1.hexdigest(script_contents)
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :name, :redis, :sha
|
14
|
+
|
15
|
+
def reload()
|
16
|
+
@sha = @redis.script(:load, script_contents)
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(keys, argv)
|
20
|
+
_call(keys, argv)
|
21
|
+
rescue
|
22
|
+
reload
|
23
|
+
_call(keys, argv)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
if USING_LEGACY_REDIS_VERSION
|
29
|
+
def _call(keys, argv)
|
30
|
+
@redis.evalsha(@sha, keys.length, *(keys + argv))
|
31
|
+
end
|
32
|
+
else
|
33
|
+
def _call(keys, argv)
|
34
|
+
@redis.evalsha(@sha, keys: keys, argv: argv)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def script_contents
|
39
|
+
@script_contents ||= File.read(File.join(LUA_SCRIPT_DIR, "#{@name}.lua"))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Qless
|
2
|
+
module Middleware
|
3
|
+
module RedisReconnect
|
4
|
+
def self.new(*redis_connections, &block)
|
5
|
+
Module.new do
|
6
|
+
define_singleton_method :to_s do
|
7
|
+
"Qless::Middleware::RedisReconnect"
|
8
|
+
end
|
9
|
+
|
10
|
+
block ||= lambda { |job| redis_connections }
|
11
|
+
|
12
|
+
define_method :around_perform do |job|
|
13
|
+
Array(block.call(job)).each do |redis|
|
14
|
+
redis.client.reconnect
|
15
|
+
end
|
16
|
+
|
17
|
+
super(job)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Qless
|
2
|
+
module Middleware
|
3
|
+
module RetryExceptions
|
4
|
+
def around_perform(job)
|
5
|
+
super
|
6
|
+
rescue *retryable_exception_classes => e
|
7
|
+
raise if job.retries_left <= 0
|
8
|
+
|
9
|
+
attempt_num = (job.original_retries - job.retries_left) + 1
|
10
|
+
job.retry(backoff_strategy.call(attempt_num))
|
11
|
+
end
|
12
|
+
|
13
|
+
def retryable_exception_classes
|
14
|
+
@retryable_exception_classes ||= []
|
15
|
+
end
|
16
|
+
|
17
|
+
def retry_on(*exception_classes)
|
18
|
+
retryable_exception_classes.push(*exception_classes)
|
19
|
+
end
|
20
|
+
|
21
|
+
NO_BACKOFF_STRATEGY = lambda { |num| 0 }
|
22
|
+
|
23
|
+
def use_backoff_strategy(strategy = nil, &block)
|
24
|
+
@backoff_strategy = strategy || block
|
25
|
+
end
|
26
|
+
|
27
|
+
def backoff_strategy
|
28
|
+
@backoff_strategy ||= NO_BACKOFF_STRATEGY
|
29
|
+
end
|
30
|
+
|
31
|
+
def exponential(base, options = {})
|
32
|
+
rand_fuzz = options.fetch(:rand_fuzz, 1)
|
33
|
+
lambda do |num|
|
34
|
+
base ** num + rand(rand_fuzz)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# For backwards compatibility
|
41
|
+
RetryExceptions = Middleware::RetryExceptions
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'raven'
|
2
|
+
|
3
|
+
module Qless
|
4
|
+
module Middleware
|
5
|
+
# This middleware logs errors to the sentry exception notification service:
|
6
|
+
# http://getsentry.com/
|
7
|
+
module Sentry
|
8
|
+
def around_perform(job)
|
9
|
+
super
|
10
|
+
rescue Exception => e
|
11
|
+
SentryLogger.new(e, job).log
|
12
|
+
raise
|
13
|
+
end
|
14
|
+
|
15
|
+
# Logs a single exception to Sentry, adding pertinent job info.
|
16
|
+
class SentryLogger
|
17
|
+
def initialize(exception, job)
|
18
|
+
@exception, @job = exception, job
|
19
|
+
end
|
20
|
+
|
21
|
+
def log
|
22
|
+
event = ::Raven::Event.capture_exception(@exception) do |evt|
|
23
|
+
evt.extra = { job: job_metadata }
|
24
|
+
end
|
25
|
+
|
26
|
+
safely_send event
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def safely_send(event)
|
32
|
+
return unless event
|
33
|
+
::Raven.send(event)
|
34
|
+
rescue
|
35
|
+
# We don't want to silence our errors when the Sentry server
|
36
|
+
# responds with an error. We'll still see the errors on the
|
37
|
+
# Qless Web UI.
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def job_metadata
|
42
|
+
{
|
43
|
+
jid: @job.jid,
|
44
|
+
klass: @job.klass_name,
|
45
|
+
history: job_history,
|
46
|
+
data: @job.data,
|
47
|
+
queue: @job.queue_name,
|
48
|
+
worker: @job.worker_name,
|
49
|
+
tags: @job.tags,
|
50
|
+
priority: @job.priority
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
# We want to log formatted timestamps rather than integer timestamps
|
55
|
+
def job_history
|
56
|
+
@job.queue_history.map do |history_event|
|
57
|
+
history_event.each_with_object({}) do |(key, value), hash|
|
58
|
+
hash[key] = if value.is_a?(Time)
|
59
|
+
value.iso8601
|
60
|
+
else
|
61
|
+
value
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
@@ -9,63 +9,93 @@
|
|
9
9
|
|
10
10
|
if #KEYS > 0 then error('Cancel(): No Keys should be provided') end
|
11
11
|
|
12
|
-
local
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
12
|
+
local function cancel(jid, jid_set)
|
13
|
+
if not jid_set[jid] then
|
14
|
+
error('Cancel(): ' .. jid .. ' is a dependency of one of the jobs but is not in the provided jid set')
|
15
|
+
end
|
16
|
+
|
17
|
+
-- Find any stage it's associated with and remove its from that stage
|
18
|
+
local state, queue, failure, worker = unpack(redis.call('hmget', 'ql:j:' .. jid, 'state', 'queue', 'failure', 'worker'))
|
19
|
+
|
20
|
+
if state == 'complete' then
|
21
|
+
return false
|
22
|
+
else
|
23
|
+
-- If this job has dependents, then we should probably fail
|
24
|
+
local dependents = redis.call('smembers', 'ql:j:' .. jid .. '-dependents')
|
25
|
+
for _, dependent_jid in ipairs(dependents) do
|
26
|
+
cancel(dependent_jid, jid_set)
|
27
|
+
end
|
28
|
+
|
29
|
+
-- Send a message out on the appropriate channels
|
30
|
+
local encoded = cjson.encode({
|
31
|
+
jid = jid,
|
32
|
+
worker = worker,
|
33
|
+
event = 'canceled',
|
34
|
+
queue = queue
|
35
|
+
})
|
36
|
+
redis.call('publish', 'ql:log', encoded)
|
37
|
+
|
38
|
+
-- Remove this job from whatever worker has it, if any
|
39
|
+
if worker then
|
40
|
+
redis.call('zrem', 'ql:w:' .. worker .. ':jobs', jid)
|
41
|
+
-- If necessary, send a message to the appropriate worker, too
|
42
|
+
redis.call('publish', 'ql:w:' .. worker, encoded)
|
43
|
+
end
|
44
|
+
|
45
|
+
-- Remove it from that queue
|
46
|
+
if queue then
|
47
|
+
redis.call('zrem', 'ql:q:' .. queue .. '-work', jid)
|
48
|
+
redis.call('zrem', 'ql:q:' .. queue .. '-locks', jid)
|
49
|
+
redis.call('zrem', 'ql:q:' .. queue .. '-scheduled', jid)
|
50
|
+
redis.call('zrem', 'ql:q:' .. queue .. '-depends', jid)
|
51
|
+
end
|
52
|
+
|
53
|
+
-- We should probably go through all our dependencies and remove ourselves
|
54
|
+
-- from the list of dependents
|
55
|
+
for i, j in ipairs(redis.call('smembers', 'ql:j:' .. jid .. '-dependencies')) do
|
56
|
+
redis.call('srem', 'ql:j:' .. j .. '-dependents', jid)
|
57
|
+
end
|
58
|
+
|
59
|
+
-- Delete any notion of dependencies it has
|
60
|
+
redis.call('del', 'ql:j:' .. jid .. '-dependencies')
|
61
|
+
|
62
|
+
-- If we're in the failed state, remove all of our data
|
63
|
+
if state == 'failed' then
|
64
|
+
failure = cjson.decode(failure)
|
65
|
+
-- We need to make this remove it from the failed queues
|
66
|
+
redis.call('lrem', 'ql:f:' .. failure.group, 0, jid)
|
67
|
+
if redis.call('llen', 'ql:f:' .. failure.group) == 0 then
|
68
|
+
redis.call('srem', 'ql:failures', failure.group)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
-- Remove it as a job that's tagged with this particular tag
|
73
|
+
local tags = cjson.decode(redis.call('hget', 'ql:j:' .. jid, 'tags') or '{}')
|
74
|
+
for i, tag in ipairs(tags) do
|
75
|
+
redis.call('zrem', 'ql:t:' .. tag, jid)
|
76
|
+
redis.call('zincrby', 'ql:tags', -1, tag)
|
77
|
+
end
|
78
|
+
|
79
|
+
-- If the job was being tracked, we should notify
|
80
|
+
if redis.call('zscore', 'ql:tracked', jid) ~= false then
|
81
|
+
redis.call('publish', 'canceled', jid)
|
82
|
+
end
|
83
|
+
|
84
|
+
-- Just go ahead and delete our data
|
85
|
+
redis.call('del', 'ql:j:' .. jid)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
-- Taken from: http://www.lua.org/pil/11.5.html
|
90
|
+
local function to_set(list)
|
91
|
+
local set = {}
|
92
|
+
for _, l in ipairs(list) do set[l] = true end
|
93
|
+
return set
|
94
|
+
end
|
95
|
+
|
96
|
+
local jids = assert(ARGV, 'Cancel(): Arg "jid" missing.')
|
97
|
+
local jid_set = to_set(jids)
|
98
|
+
|
99
|
+
for _, jid in ipairs(jids) do
|
100
|
+
cancel(jid, jid_set)
|
71
101
|
end
|
@@ -118,12 +118,20 @@ if redis.call('zscore', 'ql:tracked', jid) ~= false then
|
|
118
118
|
end
|
119
119
|
|
120
120
|
if nextq then
|
121
|
+
-- Send a message out to log
|
122
|
+
redis.call('publish', 'ql:log', cjson.encode({
|
123
|
+
jid = jid,
|
124
|
+
event = 'advanced',
|
125
|
+
queue = queue,
|
126
|
+
to = nextq
|
127
|
+
}))
|
128
|
+
|
121
129
|
-- Enqueue the job
|
122
130
|
table.insert(history, {
|
123
131
|
q = nextq,
|
124
132
|
put = math.floor(now)
|
125
133
|
})
|
126
|
-
|
134
|
+
|
127
135
|
-- We're going to make sure that this queue is in the
|
128
136
|
-- set of known queues
|
129
137
|
if redis.call('zscore', 'ql:queues', nextq) == false then
|
@@ -158,6 +166,13 @@ if nextq then
|
|
158
166
|
end
|
159
167
|
end
|
160
168
|
else
|
169
|
+
-- Send a message out to log
|
170
|
+
redis.call('publish', 'ql:log', cjson.encode({
|
171
|
+
jid = jid,
|
172
|
+
event = 'completed',
|
173
|
+
queue = queue
|
174
|
+
}))
|
175
|
+
|
161
176
|
redis.call('hmset', 'ql:j:' .. jid, 'state', 'complete', 'worker', '', 'failure', '{}',
|
162
177
|
'queue', '', 'expires', 0, 'history', cjson.encode(history), 'remaining', tonumber(retries))
|
163
178
|
|
@@ -33,9 +33,21 @@ if command == 'get' then
|
|
33
33
|
elseif command == 'set' then
|
34
34
|
local option = assert(ARGV[2], 'Config(): Arg "option" missing')
|
35
35
|
local value = assert(ARGV[3], 'Config(): Arg "value" missing')
|
36
|
+
-- Send out a log message
|
37
|
+
redis.call('publish', 'ql:log', cjson.encode({
|
38
|
+
event = 'config_set',
|
39
|
+
option = option
|
40
|
+
}))
|
41
|
+
|
36
42
|
redis.call('hset', 'ql:config', option, value)
|
37
43
|
elseif command == 'unset' then
|
38
44
|
local option = assert(ARGV[2], 'Config(): Arg "option" missing')
|
45
|
+
-- Send out a log message
|
46
|
+
redis.call('publish', 'ql:log', cjson.encode({
|
47
|
+
event = 'config_unset',
|
48
|
+
option = option
|
49
|
+
}))
|
50
|
+
|
39
51
|
redis.call('hdel', 'ql:config', option)
|
40
52
|
else
|
41
53
|
error('Config(): Unrecognized command ' .. command)
|
@@ -0,0 +1,12 @@
|
|
1
|
+
-- DeregisterWorkers(0, worker)
|
2
|
+
-- This script takes the name of a worker(s) on removes it/them
|
3
|
+
-- from the ql:workers set.
|
4
|
+
--
|
5
|
+
-- Args: The list of workers to deregister.
|
6
|
+
|
7
|
+
if #KEYS > 0 then error('DeregisterWorkers(): No Keys should be provided') end
|
8
|
+
if #ARGV < 1 then error('DeregisterWorkers(): Must provide at least one worker to deregister') end
|
9
|
+
|
10
|
+
local key = 'ql:workers'
|
11
|
+
|
12
|
+
redis.call('zrem', key, unpack(ARGV))
|
@@ -1,20 +1,21 @@
|
|
1
1
|
-- Fail(0, jid, worker, group, message, now, [data])
|
2
2
|
-- -------------------------------------------------
|
3
|
-
-- Mark the particular job as failed, with the provided group, and a more
|
4
|
-
-- message. By `group`, we mean some phrase that might be one of
|
5
|
-
-- modes of failure. The `message` is something more
|
6
|
-
-- a traceback.
|
3
|
+
-- Mark the particular job as failed, with the provided group, and a more
|
4
|
+
-- specific message. By `group`, we mean some phrase that might be one of
|
5
|
+
-- several categorical modes of failure. The `message` is something more
|
6
|
+
-- job-specific, like perhaps a traceback.
|
7
7
|
--
|
8
|
-
-- This method should __not__ be used to note that a job has been dropped or
|
9
|
-
-- failed in a transient way. This method __should__ be used to note that
|
10
|
-
-- something really wrong with it that must be remedied.
|
8
|
+
-- This method should __not__ be used to note that a job has been dropped or
|
9
|
+
-- has failed in a transient way. This method __should__ be used to note that
|
10
|
+
-- a job has something really wrong with it that must be remedied.
|
11
11
|
--
|
12
|
-
-- The motivation behind the `group` is so that similar errors can be grouped
|
13
|
-
-- Optionally, updated data can be provided for the job. A job in
|
14
|
-
-- marked as failed. If it has been given to a worker as a
|
15
|
-
-- requests to heartbeat or complete that job will
|
16
|
-
-- they are canceled or completed.
|
17
|
-
--
|
12
|
+
-- The motivation behind the `group` is so that similar errors can be grouped
|
13
|
+
-- together. Optionally, updated data can be provided for the job. A job in
|
14
|
+
-- any state can be marked as failed. If it has been given to a worker as a
|
15
|
+
-- job, then its subsequent requests to heartbeat or complete that job will
|
16
|
+
-- fail. Failed jobs are kept until they are canceled or completed.
|
17
|
+
--
|
18
|
+
-- __Returns__ the id of the failed job if successful, or `False` on failure.
|
18
19
|
--
|
19
20
|
-- Args:
|
20
21
|
-- 1) jid
|
@@ -49,6 +50,15 @@ if state ~= 'running' then
|
|
49
50
|
return false
|
50
51
|
end
|
51
52
|
|
53
|
+
-- Send out a log message
|
54
|
+
redis.call('publish', 'ql:log', cjson.encode({
|
55
|
+
jid = jid,
|
56
|
+
event = 'failed',
|
57
|
+
worker = worker,
|
58
|
+
group = group,
|
59
|
+
message = message
|
60
|
+
}))
|
61
|
+
|
52
62
|
if redis.call('zscore', 'ql:tracked', jid) ~= false then
|
53
63
|
redis.call('publish', 'failed', jid)
|
54
64
|
end
|
@@ -104,4 +114,4 @@ redis.call('lpush', 'ql:f:' .. group, jid)
|
|
104
114
|
-- Here is where we'd intcrement stats about the particular stage
|
105
115
|
-- and possibly the workers
|
106
116
|
|
107
|
-
return jid
|
117
|
+
return jid
|