qless 0.9.2 → 0.9.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|