qless 0.9.3 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +9 -3
- data/README.md +70 -25
- data/Rakefile +125 -9
- data/exe/install_phantomjs +21 -0
- data/lib/qless.rb +115 -76
- data/lib/qless/config.rb +11 -9
- data/lib/qless/failure_formatter.rb +43 -0
- data/lib/qless/job.rb +201 -102
- data/lib/qless/job_reservers/ordered.rb +7 -1
- data/lib/qless/job_reservers/round_robin.rb +16 -6
- data/lib/qless/job_reservers/shuffled_round_robin.rb +9 -2
- data/lib/qless/lua/qless-lib.lua +2463 -0
- data/lib/qless/lua/qless.lua +2012 -0
- data/lib/qless/lua_script.rb +63 -12
- data/lib/qless/middleware/memory_usage_monitor.rb +62 -0
- data/lib/qless/middleware/metriks.rb +45 -0
- data/lib/qless/middleware/redis_reconnect.rb +6 -3
- data/lib/qless/middleware/requeue_exceptions.rb +94 -0
- data/lib/qless/middleware/retry_exceptions.rb +38 -9
- data/lib/qless/middleware/sentry.rb +3 -7
- data/lib/qless/middleware/timeout.rb +64 -0
- data/lib/qless/queue.rb +90 -55
- data/lib/qless/server.rb +177 -130
- data/lib/qless/server/views/_job.erb +33 -15
- data/lib/qless/server/views/completed.erb +11 -0
- data/lib/qless/server/views/layout.erb +70 -11
- data/lib/qless/server/views/overview.erb +93 -53
- data/lib/qless/server/views/queue.erb +9 -8
- data/lib/qless/server/views/queues.erb +18 -1
- data/lib/qless/subscriber.rb +37 -22
- data/lib/qless/tasks.rb +5 -10
- data/lib/qless/test_helpers/worker_helpers.rb +55 -0
- data/lib/qless/version.rb +3 -1
- data/lib/qless/worker.rb +4 -413
- data/lib/qless/worker/base.rb +247 -0
- data/lib/qless/worker/forking.rb +245 -0
- data/lib/qless/worker/serial.rb +41 -0
- metadata +135 -52
- data/lib/qless/qless-core/cancel.lua +0 -101
- data/lib/qless/qless-core/complete.lua +0 -233
- data/lib/qless/qless-core/config.lua +0 -56
- data/lib/qless/qless-core/depends.lua +0 -65
- data/lib/qless/qless-core/deregister_workers.lua +0 -12
- data/lib/qless/qless-core/fail.lua +0 -117
- data/lib/qless/qless-core/failed.lua +0 -83
- data/lib/qless/qless-core/get.lua +0 -37
- data/lib/qless/qless-core/heartbeat.lua +0 -51
- data/lib/qless/qless-core/jobs.lua +0 -41
- data/lib/qless/qless-core/pause.lua +0 -18
- data/lib/qless/qless-core/peek.lua +0 -165
- data/lib/qless/qless-core/pop.lua +0 -314
- data/lib/qless/qless-core/priority.lua +0 -32
- data/lib/qless/qless-core/put.lua +0 -169
- data/lib/qless/qless-core/qless-lib.lua +0 -2354
- data/lib/qless/qless-core/qless.lua +0 -1862
- data/lib/qless/qless-core/queues.lua +0 -58
- data/lib/qless/qless-core/recur.lua +0 -190
- data/lib/qless/qless-core/retry.lua +0 -73
- data/lib/qless/qless-core/stats.lua +0 -92
- data/lib/qless/qless-core/tag.lua +0 -100
- data/lib/qless/qless-core/track.lua +0 -79
- data/lib/qless/qless-core/unfail.lua +0 -54
- data/lib/qless/qless-core/unpause.lua +0 -12
- data/lib/qless/qless-core/workers.lua +0 -69
- data/lib/qless/wait_until.rb +0 -19
@@ -1,58 +0,0 @@
|
|
1
|
-
-- Queues(0, now, [queue])
|
2
|
-
-- -----------------------
|
3
|
-
--
|
4
|
-
-- Return all the queues we know about, with how many jobs are scheduled, waiting,
|
5
|
-
-- and running in that queue. If a queue name is provided, then only the appropriate
|
6
|
-
-- response hash should be returned. The response is JSON:
|
7
|
-
--
|
8
|
-
-- [
|
9
|
-
-- {
|
10
|
-
-- 'name': 'testing',
|
11
|
-
-- 'stalled': 2,
|
12
|
-
-- 'waiting': 5,
|
13
|
-
-- 'running': 5,
|
14
|
-
-- 'scheduled': 10,
|
15
|
-
-- 'depends': 5,
|
16
|
-
-- 'recurring': 0
|
17
|
-
-- }, {
|
18
|
-
-- ...
|
19
|
-
-- }
|
20
|
-
-- ]
|
21
|
-
|
22
|
-
if #KEYS > 0 then
|
23
|
-
error('Queues(): Got '.. #KEYS .. ' expected 0 KEYS arguments')
|
24
|
-
end
|
25
|
-
|
26
|
-
local now = assert(tonumber(ARGV[1]), 'Queues(): Arg "now" missing or not a number: ' .. (ARGV[1] or 'nil'))
|
27
|
-
local queue = ARGV[2]
|
28
|
-
|
29
|
-
local response = {}
|
30
|
-
local queuenames = redis.call('zrange', 'ql:queues', 0, -1)
|
31
|
-
|
32
|
-
if queue then
|
33
|
-
local stalled = redis.call('zcount', 'ql:q:' .. queue .. '-locks', 0, now)
|
34
|
-
response = {
|
35
|
-
name = queue,
|
36
|
-
waiting = redis.call('zcard', 'ql:q:' .. queue .. '-work'),
|
37
|
-
stalled = stalled,
|
38
|
-
running = redis.call('zcard', 'ql:q:' .. queue .. '-locks') - stalled,
|
39
|
-
scheduled = redis.call('zcard', 'ql:q:' .. queue .. '-scheduled'),
|
40
|
-
depends = redis.call('zcard', 'ql:q:' .. queue .. '-depends'),
|
41
|
-
recurring = redis.call('zcard', 'ql:q:' .. queue .. '-recur')
|
42
|
-
}
|
43
|
-
else
|
44
|
-
for index, qname in ipairs(queuenames) do
|
45
|
-
local stalled = redis.call('zcount', 'ql:q:' .. qname .. '-locks', 0, now)
|
46
|
-
table.insert(response, {
|
47
|
-
name = qname,
|
48
|
-
waiting = redis.call('zcard', 'ql:q:' .. qname .. '-work'),
|
49
|
-
stalled = stalled,
|
50
|
-
running = redis.call('zcard', 'ql:q:' .. qname .. '-locks') - stalled,
|
51
|
-
scheduled = redis.call('zcard', 'ql:q:' .. qname .. '-scheduled'),
|
52
|
-
depends = redis.call('zcard', 'ql:q:' .. qname .. '-depends'),
|
53
|
-
recurring = redis.call('zcard', 'ql:q:' .. qname .. '-recur')
|
54
|
-
})
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
return cjson.encode(response)
|
@@ -1,190 +0,0 @@
|
|
1
|
-
-- Recur(0, 'on', queue, jid, klass, data, now, 'interval', second, offset, [priority p], [tags t], [retries r])
|
2
|
-
-- Recur(0, 'off', jid)
|
3
|
-
-- Recur(0, 'get', jid)
|
4
|
-
-- Recur(0, 'update', jid, ['priority', priority], ['interval', interval], ['retries', retries], ['data', data], ['klass', klass], ['queue', queue])
|
5
|
-
-- Recur(0, 'tag', jid, tag, [tag, [...]])
|
6
|
-
-- Recur(0, 'untag', jid, tag, [tag, [...]])
|
7
|
-
-- -------------------------------------------------------------------------------------------------------
|
8
|
-
-- This script takes the name of a queue, and then the info
|
9
|
-
-- info about the work item, and makes sure that jobs matching
|
10
|
-
-- its criteria are regularly made available.
|
11
|
-
|
12
|
-
if #KEYS ~= 0 then
|
13
|
-
error('Recur(): Got ' .. #KEYS .. ', expected 0 KEYS arguments')
|
14
|
-
end
|
15
|
-
|
16
|
-
local command = assert(ARGV[1] , 'Recur(): Missing first argument')
|
17
|
-
|
18
|
-
if command == 'on' then
|
19
|
-
local queue = assert(ARGV[2] , 'Recur(): Arg "queue" missing')
|
20
|
-
local jid = assert(ARGV[3] , 'Recur(): Arg "jid" missing')
|
21
|
-
local klass = assert(ARGV[4] , 'Recur(): Arg "klass" missing')
|
22
|
-
local data = assert(cjson.decode(ARGV[5]) , 'Recur(): Arg "data" missing or not JSON: ' .. tostring(ARGV[5]))
|
23
|
-
local now = assert(tonumber(ARGV[6]) , 'Recur(): Arg "now" missing or not a number: ' .. tostring(ARGV[6]))
|
24
|
-
local spec = assert(ARGV[7] , 'Recur(): Arg "schedule type" missing')
|
25
|
-
if spec == 'interval' then
|
26
|
-
local interval = assert(tonumber(ARGV[8]) , 'Recur(): Arg "interval" must be a number: ' .. tostring(ARGV[8]))
|
27
|
-
local offset = assert(tonumber(ARGV[9]) , 'Recur(): Arg "offset" must be a number: ' .. tostring(ARGV[9]))
|
28
|
-
if interval <= 0 then
|
29
|
-
error('Recur(): Arg "interval" must be greater than or equal to 0')
|
30
|
-
end
|
31
|
-
-- Read in all the optional parameters
|
32
|
-
local options = {}
|
33
|
-
for i = 10, #ARGV, 2 do options[ARGV[i]] = ARGV[i + 1] end
|
34
|
-
options.tags = assert(cjson.decode(options.tags or {}), 'Recur(): Arg "tags" must be JSON-encoded array of string. Got: ' .. tostring(options.tags))
|
35
|
-
options.priority = assert(tonumber(options.priority or 0) , 'Recur(): Arg "priority" must be a number. Got: ' .. tostring(options.priority))
|
36
|
-
options.retries = assert(tonumber(options.retries or 0) , 'Recur(): Arg "retries" must be a number. Got: ' .. tostring(options.retries))
|
37
|
-
|
38
|
-
local count, old_queue = unpack(redis.call('hmget', 'ql:r:' .. jid, 'count', 'queue'))
|
39
|
-
count = count or 0
|
40
|
-
|
41
|
-
-- If it has previously been in another queue, then we should remove
|
42
|
-
-- some information about it
|
43
|
-
if old_queue then
|
44
|
-
redis.call('zrem', 'ql:q:' .. old_queue .. '-recur', jid)
|
45
|
-
end
|
46
|
-
|
47
|
-
-- Do some insertions
|
48
|
-
redis.call('hmset', 'ql:r:' .. jid,
|
49
|
-
'jid' , jid,
|
50
|
-
'klass' , klass,
|
51
|
-
'data' , cjson.encode(data),
|
52
|
-
'priority', options.priority,
|
53
|
-
'tags' , cjson.encode(options.tags or {}),
|
54
|
-
'state' , 'recur',
|
55
|
-
'queue' , queue,
|
56
|
-
'type' , 'interval',
|
57
|
-
-- How many jobs we've spawned from this
|
58
|
-
'count' , count,
|
59
|
-
'interval', interval,
|
60
|
-
'retries' , options.retries)
|
61
|
-
-- Now, we should schedule the next run of the job
|
62
|
-
redis.call('zadd', 'ql:q:' .. queue .. '-recur', now + offset, jid)
|
63
|
-
|
64
|
-
-- Lastly, we're going to make sure that this item is in the
|
65
|
-
-- set of known queues. We should keep this sorted by the
|
66
|
-
-- order in which we saw each of these queues
|
67
|
-
if redis.call('zscore', 'ql:queues', queue) == false then
|
68
|
-
redis.call('zadd', 'ql:queues', now, queue)
|
69
|
-
end
|
70
|
-
|
71
|
-
return jid
|
72
|
-
else
|
73
|
-
error('Recur(): schedule type "' .. tostring(spec) .. '" unknown')
|
74
|
-
end
|
75
|
-
elseif command == 'off' then
|
76
|
-
local jid = assert(ARGV[2], 'Recur(): Arg "jid" missing')
|
77
|
-
-- First, find out what queue it was attached to
|
78
|
-
local queue = redis.call('hget', 'ql:r:' .. jid, 'queue')
|
79
|
-
if queue then
|
80
|
-
-- Now, delete it from the queue it was attached to, and delete the thing itself
|
81
|
-
redis.call('zrem', 'ql:q:' .. queue .. '-recur', jid)
|
82
|
-
redis.call('del', 'ql:r:' .. jid)
|
83
|
-
return true
|
84
|
-
else
|
85
|
-
return true
|
86
|
-
end
|
87
|
-
elseif command == 'get' then
|
88
|
-
local jid = assert(ARGV[2], 'Recur(): Arg "jid" missing')
|
89
|
-
local job = redis.call(
|
90
|
-
'hmget', 'ql:r:' .. jid, 'jid', 'klass', 'state', 'queue',
|
91
|
-
'priority', 'interval', 'retries', 'count', 'data', 'tags')
|
92
|
-
|
93
|
-
if not job[1] then
|
94
|
-
return false
|
95
|
-
end
|
96
|
-
|
97
|
-
return cjson.encode({
|
98
|
-
jid = job[1],
|
99
|
-
klass = job[2],
|
100
|
-
state = job[3],
|
101
|
-
queue = job[4],
|
102
|
-
priority = tonumber(job[5]),
|
103
|
-
interval = tonumber(job[6]),
|
104
|
-
retries = tonumber(job[7]),
|
105
|
-
count = tonumber(job[8]),
|
106
|
-
data = cjson.decode(job[9]),
|
107
|
-
tags = cjson.decode(job[10])
|
108
|
-
})
|
109
|
-
elseif command == 'update' then
|
110
|
-
local jid = assert(ARGV[2], 'Recur(): Arg "jid" missing')
|
111
|
-
local options = {}
|
112
|
-
|
113
|
-
-- Make sure that the job exists
|
114
|
-
if redis.call('exists', 'ql:r:' .. jid) ~= 0 then
|
115
|
-
for i = 3, #ARGV, 2 do
|
116
|
-
local key = ARGV[i]
|
117
|
-
local value = ARGV[i+1]
|
118
|
-
if key == 'priority' or key == 'interval' or key == 'retries' then
|
119
|
-
value = assert(tonumber(value), 'Recur(): Arg "' .. key .. '" must be a number: ' .. tostring(value))
|
120
|
-
-- If the command is 'interval', then we need to update the time
|
121
|
-
-- when it should next be scheduled
|
122
|
-
if key == 'interval' then
|
123
|
-
local queue, interval = unpack(redis.call('hmget', 'ql:r:' .. jid, 'queue', 'interval'))
|
124
|
-
redis.call('zincrby', 'ql:q:' .. queue .. '-recur', value - tonumber(interval), jid)
|
125
|
-
end
|
126
|
-
redis.call('hset', 'ql:r:' .. jid, key, value)
|
127
|
-
elseif key == 'data' then
|
128
|
-
value = assert(cjson.decode(value), 'Recur(): Arg "data" is not JSON-encoded: ' .. tostring(value))
|
129
|
-
redis.call('hset', 'ql:r:' .. jid, 'data', cjson.encode(value))
|
130
|
-
elseif key == 'klass' then
|
131
|
-
redis.call('hset', 'ql:r:' .. jid, 'klass', value)
|
132
|
-
elseif key == 'queue' then
|
133
|
-
local queue = redis.call('hget', 'ql:r:' .. jid, 'queue')
|
134
|
-
local score = redis.call('zscore', 'ql:q:' .. queue .. '-recur', jid)
|
135
|
-
redis.call('zrem', 'ql:q:' .. queue .. '-recur', jid)
|
136
|
-
redis.call('zadd', 'ql:q:' .. value .. '-recur', score, jid)
|
137
|
-
redis.call('hset', 'ql:r:' .. jid, 'queue', value)
|
138
|
-
else
|
139
|
-
error('Recur(): Unrecognized option "' .. key .. '"')
|
140
|
-
end
|
141
|
-
end
|
142
|
-
return true
|
143
|
-
else
|
144
|
-
return false
|
145
|
-
end
|
146
|
-
elseif command == 'tag' then
|
147
|
-
local jid = assert(ARGV[2], 'Recur(): Arg "jid" missing')
|
148
|
-
local tags = redis.call('hget', 'ql:r:' .. jid, 'tags')
|
149
|
-
-- If the job has been canceled / deleted, then return false
|
150
|
-
if tags then
|
151
|
-
-- Decode the json blob, convert to dictionary
|
152
|
-
tags = cjson.decode(tags)
|
153
|
-
local _tags = {}
|
154
|
-
for i,v in ipairs(tags) do _tags[v] = true end
|
155
|
-
|
156
|
-
-- Otherwise, add the job to the sorted set with that tags
|
157
|
-
for i=3,#ARGV do if _tags[ARGV[i]] == nil then table.insert(tags, ARGV[i]) end end
|
158
|
-
|
159
|
-
tags = cjson.encode(tags)
|
160
|
-
redis.call('hset', 'ql:r:' .. jid, 'tags', tags)
|
161
|
-
return tags
|
162
|
-
else
|
163
|
-
return false
|
164
|
-
end
|
165
|
-
elseif command == 'untag' then
|
166
|
-
local jid = assert(ARGV[2], 'Recur(): Arg "jid" missing')
|
167
|
-
-- Get the existing tags
|
168
|
-
local tags = redis.call('hget', 'ql:r:' .. jid, 'tags')
|
169
|
-
-- If the job has been canceled / deleted, then return false
|
170
|
-
if tags then
|
171
|
-
-- Decode the json blob, convert to dictionary
|
172
|
-
tags = cjson.decode(tags)
|
173
|
-
local _tags = {}
|
174
|
-
-- Make a hash
|
175
|
-
for i,v in ipairs(tags) do _tags[v] = true end
|
176
|
-
-- Delete these from the hash
|
177
|
-
for i = 3,#ARGV do _tags[ARGV[i]] = nil end
|
178
|
-
-- Back into a list
|
179
|
-
local results = {}
|
180
|
-
for i, tag in ipairs(tags) do if _tags[tag] then table.insert(results, tag) end end
|
181
|
-
-- json encode them, set, and return
|
182
|
-
tags = cjson.encode(results)
|
183
|
-
redis.call('hset', 'ql:r:' .. jid, 'tags', tags)
|
184
|
-
return tags
|
185
|
-
else
|
186
|
-
return false
|
187
|
-
end
|
188
|
-
else
|
189
|
-
error('Recur(): First argument must be one of [on, off, get, update, tag, untag]. Got ' .. tostring(ARGV[1]))
|
190
|
-
end
|
@@ -1,73 +0,0 @@
|
|
1
|
-
-- retry(0, jid, queue, worker, now, [delay])
|
2
|
-
-- ------------------------------------------
|
3
|
-
-- This script accepts jid, queue, worker and delay for
|
4
|
-
-- retrying a job. This is similar in functionality to
|
5
|
-
-- `put`, except that this counts against the retries
|
6
|
-
-- a job has for a stage.
|
7
|
-
--
|
8
|
-
-- If the worker is not the worker with a lock on the job,
|
9
|
-
-- then it returns false. If the job is not actually running,
|
10
|
-
-- then it returns false. Otherwise, it returns the number
|
11
|
-
-- of retries remaining. If the allowed retries have been
|
12
|
-
-- exhausted, then it is automatically failed, and a negative
|
13
|
-
-- number is returned.
|
14
|
-
|
15
|
-
if #KEYS ~= 0 then
|
16
|
-
error('Retry(): Got ' .. #KEYS .. ', expected 0')
|
17
|
-
end
|
18
|
-
|
19
|
-
local jid = assert(ARGV[1] , 'Retry(): Arg "jid" missing')
|
20
|
-
local queue = assert(ARGV[2] , 'Retry(): Arg "queue" missing')
|
21
|
-
local worker = assert(ARGV[3] , 'Retry(): Arg "worker" missing')
|
22
|
-
local now = assert(tonumber(ARGV[4]) , 'Retry(): Arg "now" missing')
|
23
|
-
local delay = assert(tonumber(ARGV[5] or 0), 'Retry(): Arg "delay" not a number: ' .. tostring(ARGV[5]))
|
24
|
-
|
25
|
-
-- Let's see what the old priority, history and tags were
|
26
|
-
local oldqueue, state, retries, oldworker, priority = unpack(redis.call('hmget', 'ql:j:' .. jid, 'queue', 'state', 'retries', 'worker', 'priority'))
|
27
|
-
|
28
|
-
-- If this isn't the worker that owns
|
29
|
-
if oldworker ~= worker or (state ~= 'running') then
|
30
|
-
return false
|
31
|
-
end
|
32
|
-
|
33
|
-
-- Remove it from the locks key of the old queue
|
34
|
-
redis.call('zrem', 'ql:q:' .. oldqueue .. '-locks', jid)
|
35
|
-
|
36
|
-
local remaining = redis.call('hincrby', 'ql:j:' .. jid, 'remaining', -1)
|
37
|
-
|
38
|
-
-- Remove this job from the worker that was previously working it
|
39
|
-
redis.call('zrem', 'ql:w:' .. worker .. ':jobs', jid)
|
40
|
-
|
41
|
-
if remaining < 0 then
|
42
|
-
-- Now remove the instance from the schedule, and work queues for the queue it's in
|
43
|
-
local group = 'failed-retries-' .. queue
|
44
|
-
-- First things first, we should get the history
|
45
|
-
local history = redis.call('hget', 'ql:j:' .. jid, 'history')
|
46
|
-
-- Now, take the element of the history for which our provided worker is the worker, and update 'failed'
|
47
|
-
history = cjson.decode(history or '[]')
|
48
|
-
history[#history]['failed'] = now
|
49
|
-
|
50
|
-
redis.call('hmset', 'ql:j:' .. jid, 'state', 'failed', 'worker', '',
|
51
|
-
'expires', '', 'history', cjson.encode(history), 'failure', cjson.encode({
|
52
|
-
['group'] = group,
|
53
|
-
['message'] = 'Job exhausted retries in queue "' .. queue .. '"',
|
54
|
-
['when'] = now,
|
55
|
-
['worker'] = worker
|
56
|
-
}))
|
57
|
-
|
58
|
-
-- Add this type of failure to the list of failures
|
59
|
-
redis.call('sadd', 'ql:failures', group)
|
60
|
-
-- And add this particular instance to the failed types
|
61
|
-
redis.call('lpush', 'ql:f:' .. group, jid)
|
62
|
-
else
|
63
|
-
-- Put it in the queue again with a delay. Like put()
|
64
|
-
if delay > 0 then
|
65
|
-
redis.call('zadd', 'ql:q:' .. queue .. '-scheduled', now + delay, jid)
|
66
|
-
redis.call('hset', 'ql:j:' .. jid, 'state', 'scheduled')
|
67
|
-
else
|
68
|
-
redis.call('zadd', 'ql:q:' .. queue .. '-work', priority - (now / 10000000000), jid)
|
69
|
-
redis.call('hset', 'ql:j:' .. jid, 'state', 'waiting')
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
return remaining
|
@@ -1,92 +0,0 @@
|
|
1
|
-
-- Stats(0, queue, date)
|
2
|
-
-- ---------------------
|
3
|
-
-- Return the current statistics for a given queue on a given date. The results
|
4
|
-
-- are returned are a JSON blob:
|
5
|
-
--
|
6
|
-
--
|
7
|
-
-- {
|
8
|
-
-- # These are unimplemented as of yet
|
9
|
-
-- 'failed': 3,
|
10
|
-
-- 'retries': 5,
|
11
|
-
-- 'wait' : {
|
12
|
-
-- 'total' : ...,
|
13
|
-
-- 'mean' : ...,
|
14
|
-
-- 'variance' : ...,
|
15
|
-
-- 'histogram': [
|
16
|
-
-- ...
|
17
|
-
-- ]
|
18
|
-
-- }, 'run': {
|
19
|
-
-- 'total' : ...,
|
20
|
-
-- 'mean' : ...,
|
21
|
-
-- 'variance' : ...,
|
22
|
-
-- 'histogram': [
|
23
|
-
-- ...
|
24
|
-
-- ]
|
25
|
-
-- }
|
26
|
-
-- }
|
27
|
-
--
|
28
|
-
-- The histogram's data points are at the second resolution for the first minute,
|
29
|
-
-- the minute resolution for the first hour, the 15-minute resolution for the first
|
30
|
-
-- day, the hour resolution for the first 3 days, and then at the day resolution
|
31
|
-
-- from there on out. The `histogram` key is a list of those values.
|
32
|
-
--
|
33
|
-
-- Args:
|
34
|
-
-- 1) queue
|
35
|
-
-- 2) time
|
36
|
-
|
37
|
-
if #KEYS > 0 then error('Stats(): No Keys should be provided') end
|
38
|
-
|
39
|
-
local queue = assert(ARGV[1] , 'Stats(): Arg "queue" missing')
|
40
|
-
local time = assert(tonumber(ARGV[2]), 'Stats(): Arg "time" missing or not a number: ' .. (ARGV[2] or 'nil'))
|
41
|
-
|
42
|
-
-- The bin is midnight of the provided day
|
43
|
-
-- 24 * 60 * 60 = 86400
|
44
|
-
local bin = time - (time % 86400)
|
45
|
-
|
46
|
-
-- This a table of all the keys we want to use in order to produce a histogram
|
47
|
-
local histokeys = {
|
48
|
-
's0','s1','s2','s3','s4','s5','s6','s7','s8','s9','s10','s11','s12','s13','s14','s15','s16','s17','s18','s19','s20','s21','s22','s23','s24','s25','s26','s27','s28','s29','s30','s31','s32','s33','s34','s35','s36','s37','s38','s39','s40','s41','s42','s43','s44','s45','s46','s47','s48','s49','s50','s51','s52','s53','s54','s55','s56','s57','s58','s59',
|
49
|
-
'm1','m2','m3','m4','m5','m6','m7','m8','m9','m10','m11','m12','m13','m14','m15','m16','m17','m18','m19','m20','m21','m22','m23','m24','m25','m26','m27','m28','m29','m30','m31','m32','m33','m34','m35','m36','m37','m38','m39','m40','m41','m42','m43','m44','m45','m46','m47','m48','m49','m50','m51','m52','m53','m54','m55','m56','m57','m58','m59',
|
50
|
-
'h1','h2','h3','h4','h5','h6','h7','h8','h9','h10','h11','h12','h13','h14','h15','h16','h17','h18','h19','h20','h21','h22','h23',
|
51
|
-
'd1','d2','d3','d4','d5','d6'
|
52
|
-
}
|
53
|
-
|
54
|
-
local mkstats = function(name, bin, queue)
|
55
|
-
-- The results we'll be sending back
|
56
|
-
local results = {}
|
57
|
-
|
58
|
-
local count, mean, vk = unpack(redis.call('hmget', 'ql:s:' .. name .. ':' .. bin .. ':' .. queue, 'total', 'mean', 'vk'))
|
59
|
-
|
60
|
-
count = tonumber(count) or 0
|
61
|
-
mean = tonumber(mean) or 0
|
62
|
-
vk = tonumber(vk)
|
63
|
-
|
64
|
-
results.count = count or 0
|
65
|
-
results.mean = mean or 0
|
66
|
-
results.histogram = {}
|
67
|
-
|
68
|
-
if not count then
|
69
|
-
results.std = 0
|
70
|
-
else
|
71
|
-
if count > 1 then
|
72
|
-
results.std = math.sqrt(vk / (count - 1))
|
73
|
-
else
|
74
|
-
results.std = 0
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
local histogram = redis.call('hmget', 'ql:s:' .. name .. ':' .. bin .. ':' .. queue, unpack(histokeys))
|
79
|
-
for i=1,#histokeys do
|
80
|
-
table.insert(results.histogram, tonumber(histogram[i]) or 0)
|
81
|
-
end
|
82
|
-
return results
|
83
|
-
end
|
84
|
-
|
85
|
-
local retries, failed, failures = unpack(redis.call('hmget', 'ql:s:stats:' .. bin .. ':' .. queue, 'retries', 'failed', 'failures'))
|
86
|
-
return cjson.encode({
|
87
|
-
retries = tonumber(retries or 0),
|
88
|
-
failed = tonumber(failed or 0),
|
89
|
-
failures = tonumber(failures or 0),
|
90
|
-
wait = mkstats('wait', bin, queue),
|
91
|
-
run = mkstats('run' , bin, queue)
|
92
|
-
})
|
@@ -1,100 +0,0 @@
|
|
1
|
-
-- tag(0, ('add' | 'remove'), jid, now, tag, [tag, ...])
|
2
|
-
-- tag(0, 'get', tag, [offset, [count]])
|
3
|
-
-- tag(0, 'top', [offset, [count]])
|
4
|
-
-- ------------------------------------------------------------------------------------------------------------------
|
5
|
-
-- Accepts a jid, 'add' or 'remove', and then a list of tags
|
6
|
-
-- to either add or remove from the job. Alternatively, 'get',
|
7
|
-
-- a tag to get jobs associated with that tag, and offset and
|
8
|
-
-- count
|
9
|
-
--
|
10
|
-
-- If 'add' or 'remove', the response is a list of the jobs
|
11
|
-
-- current tags, or False if the job doesn't exist. If 'get',
|
12
|
-
-- the response is of the form:
|
13
|
-
--
|
14
|
-
-- {
|
15
|
-
-- total: ...,
|
16
|
-
-- jobs: [
|
17
|
-
-- jid,
|
18
|
-
-- ...
|
19
|
-
-- ]
|
20
|
-
-- }
|
21
|
-
--
|
22
|
-
-- If 'top' is supplied, it returns the most commonly-used tags
|
23
|
-
-- in a paginated fashion.
|
24
|
-
|
25
|
-
if #KEYS ~= 0 then
|
26
|
-
error('Tag(): Got ' .. #KEYS .. ', expected 0')
|
27
|
-
end
|
28
|
-
|
29
|
-
local command = assert(ARGV[1], 'Tag(): Missing first arg "add", "remove" or "get"')
|
30
|
-
|
31
|
-
if command == 'add' then
|
32
|
-
local jid = assert(ARGV[2] , 'Tag(): Arg "jid" missing')
|
33
|
-
local now = assert(tonumber(ARGV[3]), 'Tag(): Arg "now" is not a number')
|
34
|
-
local tags = redis.call('hget', 'ql:j:' .. jid, 'tags')
|
35
|
-
-- If the job has been canceled / deleted, then return false
|
36
|
-
if tags then
|
37
|
-
-- Decode the json blob, convert to dictionary
|
38
|
-
tags = cjson.decode(tags)
|
39
|
-
local _tags = {}
|
40
|
-
for i,v in ipairs(tags) do _tags[v] = true end
|
41
|
-
|
42
|
-
-- Otherwise, add the job to the sorted set with that tags
|
43
|
-
for i=4,#ARGV do
|
44
|
-
local tag = ARGV[i]
|
45
|
-
if _tags[tag] == nil then
|
46
|
-
table.insert(tags, tag)
|
47
|
-
end
|
48
|
-
redis.call('zadd', 'ql:t:' .. tag, now, jid)
|
49
|
-
redis.call('zincrby', 'ql:tags', 1, tag)
|
50
|
-
end
|
51
|
-
|
52
|
-
tags = cjson.encode(tags)
|
53
|
-
redis.call('hset', 'ql:j:' .. jid, 'tags', tags)
|
54
|
-
return tags
|
55
|
-
else
|
56
|
-
return false
|
57
|
-
end
|
58
|
-
elseif command == 'remove' then
|
59
|
-
local jid = assert(ARGV[2] , 'Tag(): Arg "jid" missing')
|
60
|
-
local now = assert(tonumber(ARGV[3]), 'Tag(): Arg "now" is not a number')
|
61
|
-
local tags = redis.call('hget', 'ql:j:' .. jid, 'tags')
|
62
|
-
-- If the job has been canceled / deleted, then return false
|
63
|
-
if tags then
|
64
|
-
-- Decode the json blob, convert to dictionary
|
65
|
-
tags = cjson.decode(tags)
|
66
|
-
local _tags = {}
|
67
|
-
for i,v in ipairs(tags) do _tags[v] = true end
|
68
|
-
|
69
|
-
-- Otherwise, add the job to the sorted set with that tags
|
70
|
-
for i=4,#ARGV do
|
71
|
-
local tag = ARGV[i]
|
72
|
-
_tags[tag] = nil
|
73
|
-
redis.call('zrem', 'ql:t:' .. tag, jid)
|
74
|
-
redis.call('zincrby', 'ql:tags', -1, tag)
|
75
|
-
end
|
76
|
-
|
77
|
-
local results = {}
|
78
|
-
for i,tag in ipairs(tags) do if _tags[tag] then table.insert(results, tag) end end
|
79
|
-
|
80
|
-
tags = cjson.encode(results)
|
81
|
-
redis.call('hset', 'ql:j:' .. jid, 'tags', tags)
|
82
|
-
return tags
|
83
|
-
else
|
84
|
-
return false
|
85
|
-
end
|
86
|
-
elseif command == 'get' then
|
87
|
-
local tag = assert(ARGV[2] , 'Tag(): Arg "tag" missing')
|
88
|
-
local offset = assert(tonumber(ARGV[3] or 0) , 'Tag(): Arg "offset" not a number: ' .. tostring(ARGV[3]))
|
89
|
-
local count = assert(tonumber(ARGV[4] or 25), 'Tag(): Arg "count" not a number: ' .. tostring(ARGV[4]))
|
90
|
-
return cjson.encode({
|
91
|
-
total = redis.call('zcard', 'ql:t:' .. tag),
|
92
|
-
jobs = redis.call('zrange', 'ql:t:' .. tag, offset, count)
|
93
|
-
})
|
94
|
-
elseif command == 'top' then
|
95
|
-
local offset = assert(tonumber(ARGV[2] or 0) , 'Tag(): Arg "offset" not a number: ' .. tostring(ARGV[2]))
|
96
|
-
local count = assert(tonumber(ARGV[3] or 25), 'Tag(): Arg "count" not a number: ' .. tostring(ARGV[3]))
|
97
|
-
return cjson.encode(redis.call('zrevrangebyscore', 'ql:tags', '+inf', 2, 'limit', offset, count))
|
98
|
-
else
|
99
|
-
error('Tag(): First argument must be "add", "remove" or "get"')
|
100
|
-
end
|