qless_lua 1.0.0
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.
- checksums.yaml +7 -0
- data/lib/qless_lua.rb +2040 -0
- data/qless_lua.gemspec +22 -0
- metadata +46 -0
data/lib/qless_lua.rb
ADDED
@@ -0,0 +1,2040 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This file is generated. See https://github.com/libqless/qless-core/ruby
|
4
|
+
|
5
|
+
require 'digest/sha1'
|
6
|
+
|
7
|
+
# A wrapper that will give you `qless.lua` source to be used with your redis
|
8
|
+
class QlessLua
|
9
|
+
SOURCE = <<-LUA_SOURCE.strip.freeze
|
10
|
+
local Qless = {
|
11
|
+
ns = 'ql:'
|
12
|
+
}
|
13
|
+
|
14
|
+
local QlessQueue = {
|
15
|
+
ns = Qless.ns .. 'q:'
|
16
|
+
}
|
17
|
+
QlessQueue.__index = QlessQueue
|
18
|
+
|
19
|
+
local QlessWorker = {
|
20
|
+
ns = Qless.ns .. 'w:'
|
21
|
+
}
|
22
|
+
QlessWorker.__index = QlessWorker
|
23
|
+
|
24
|
+
local QlessJob = {
|
25
|
+
ns = Qless.ns .. 'j:'
|
26
|
+
}
|
27
|
+
QlessJob.__index = QlessJob
|
28
|
+
|
29
|
+
local QlessRecurringJob = {}
|
30
|
+
QlessRecurringJob.__index = QlessRecurringJob
|
31
|
+
|
32
|
+
Qless.config = {}
|
33
|
+
|
34
|
+
local function tbl_extend(self, other)
|
35
|
+
for i, v in ipairs(other) do
|
36
|
+
table.insert(self, v)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
function Qless.publish(channel, message)
|
41
|
+
redis.call('publish', Qless.ns .. channel, message)
|
42
|
+
end
|
43
|
+
|
44
|
+
function Qless.job(jid)
|
45
|
+
assert(jid, 'Job(): no jid provided')
|
46
|
+
local job = {}
|
47
|
+
setmetatable(job, QlessJob)
|
48
|
+
job.jid = jid
|
49
|
+
return job
|
50
|
+
end
|
51
|
+
|
52
|
+
function Qless.recurring(jid)
|
53
|
+
assert(jid, 'Recurring(): no jid provided')
|
54
|
+
local job = {}
|
55
|
+
setmetatable(job, QlessRecurringJob)
|
56
|
+
job.jid = jid
|
57
|
+
return job
|
58
|
+
end
|
59
|
+
|
60
|
+
function Qless.failed(group, start, limit)
|
61
|
+
start = assert(tonumber(start or 0),
|
62
|
+
'Failed(): Arg "start" is not a number: ' .. (start or 'nil'))
|
63
|
+
limit = assert(tonumber(limit or 25),
|
64
|
+
'Failed(): Arg "limit" is not a number: ' .. (limit or 'nil'))
|
65
|
+
|
66
|
+
if group then
|
67
|
+
return {
|
68
|
+
total = redis.call('llen', 'ql:f:' .. group),
|
69
|
+
jobs = redis.call('lrange', 'ql:f:' .. group, start, start + limit - 1)
|
70
|
+
}
|
71
|
+
else
|
72
|
+
local response = {}
|
73
|
+
local groups = redis.call('smembers', 'ql:failures')
|
74
|
+
for index, group in ipairs(groups) do
|
75
|
+
response[group] = redis.call('llen', 'ql:f:' .. group)
|
76
|
+
end
|
77
|
+
return response
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
function Qless.jobs(now, state, ...)
|
82
|
+
assert(state, 'Jobs(): Arg "state" missing')
|
83
|
+
if state == 'complete' then
|
84
|
+
local offset = assert(tonumber(arg[1] or 0),
|
85
|
+
'Jobs(): Arg "offset" not a number: ' .. tostring(arg[1]))
|
86
|
+
local count = assert(tonumber(arg[2] or 25),
|
87
|
+
'Jobs(): Arg "count" not a number: ' .. tostring(arg[2]))
|
88
|
+
return redis.call('zrevrange', 'ql:completed', offset,
|
89
|
+
offset + count - 1)
|
90
|
+
else
|
91
|
+
local name = assert(arg[1], 'Jobs(): Arg "queue" missing')
|
92
|
+
local offset = assert(tonumber(arg[2] or 0),
|
93
|
+
'Jobs(): Arg "offset" not a number: ' .. tostring(arg[2]))
|
94
|
+
local count = assert(tonumber(arg[3] or 25),
|
95
|
+
'Jobs(): Arg "count" not a number: ' .. tostring(arg[3]))
|
96
|
+
|
97
|
+
local queue = Qless.queue(name)
|
98
|
+
if state == 'running' then
|
99
|
+
return queue.locks.peek(now, offset, count)
|
100
|
+
elseif state == 'stalled' then
|
101
|
+
return queue.locks.expired(now, offset, count)
|
102
|
+
elseif state == 'scheduled' then
|
103
|
+
queue:check_scheduled(now, queue.scheduled.length())
|
104
|
+
return queue.scheduled.peek(now, offset, count)
|
105
|
+
elseif state == 'depends' then
|
106
|
+
return queue.depends.peek(now, offset, count)
|
107
|
+
elseif state == 'recurring' then
|
108
|
+
return queue.recurring.peek('+inf', offset, count)
|
109
|
+
else
|
110
|
+
error('Jobs(): Unknown type "' .. state .. '"')
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
function Qless.track(now, command, jid)
|
116
|
+
if command ~= nil then
|
117
|
+
assert(jid, 'Track(): Arg "jid" missing')
|
118
|
+
assert(Qless.job(jid):exists(), 'Track(): Job does not exist')
|
119
|
+
if string.lower(command) == 'track' then
|
120
|
+
Qless.publish('track', jid)
|
121
|
+
return redis.call('zadd', 'ql:tracked', now, jid)
|
122
|
+
elseif string.lower(command) == 'untrack' then
|
123
|
+
Qless.publish('untrack', jid)
|
124
|
+
return redis.call('zrem', 'ql:tracked', jid)
|
125
|
+
else
|
126
|
+
error('Track(): Unknown action "' .. command .. '"')
|
127
|
+
end
|
128
|
+
else
|
129
|
+
local response = {
|
130
|
+
jobs = {},
|
131
|
+
expired = {}
|
132
|
+
}
|
133
|
+
local jids = redis.call('zrange', 'ql:tracked', 0, -1)
|
134
|
+
for index, jid in ipairs(jids) do
|
135
|
+
local data = Qless.job(jid):data()
|
136
|
+
if data then
|
137
|
+
table.insert(response.jobs, data)
|
138
|
+
else
|
139
|
+
table.insert(response.expired, jid)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
return response
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
function Qless.tag(now, command, ...)
|
147
|
+
assert(command,
|
148
|
+
'Tag(): Arg "command" must be "add", "remove", "get" or "top"')
|
149
|
+
|
150
|
+
if command == 'add' then
|
151
|
+
local jid = assert(arg[1], 'Tag(): Arg "jid" missing')
|
152
|
+
local tags = redis.call('hget', QlessJob.ns .. jid, 'tags')
|
153
|
+
if tags then
|
154
|
+
tags = cjson.decode(tags)
|
155
|
+
local _tags = {}
|
156
|
+
for i,v in ipairs(tags) do _tags[v] = true end
|
157
|
+
|
158
|
+
for i=2,#arg do
|
159
|
+
local tag = arg[i]
|
160
|
+
if _tags[tag] == nil or _tags[tag] == false then
|
161
|
+
_tags[tag] = true
|
162
|
+
table.insert(tags, tag)
|
163
|
+
end
|
164
|
+
redis.call('zadd', 'ql:t:' .. tag, now, jid)
|
165
|
+
redis.call('zincrby', 'ql:tags', 1, tag)
|
166
|
+
end
|
167
|
+
|
168
|
+
redis.call('hset', QlessJob.ns .. jid, 'tags', cjson.encode(tags))
|
169
|
+
return tags
|
170
|
+
else
|
171
|
+
error('Tag(): Job ' .. jid .. ' does not exist')
|
172
|
+
end
|
173
|
+
elseif command == 'remove' then
|
174
|
+
local jid = assert(arg[1], 'Tag(): Arg "jid" missing')
|
175
|
+
local tags = redis.call('hget', QlessJob.ns .. jid, 'tags')
|
176
|
+
if tags then
|
177
|
+
tags = cjson.decode(tags)
|
178
|
+
local _tags = {}
|
179
|
+
for i,v in ipairs(tags) do _tags[v] = true end
|
180
|
+
|
181
|
+
for i=2,#arg do
|
182
|
+
local tag = arg[i]
|
183
|
+
_tags[tag] = nil
|
184
|
+
redis.call('zrem', 'ql:t:' .. tag, jid)
|
185
|
+
redis.call('zincrby', 'ql:tags', -1, tag)
|
186
|
+
end
|
187
|
+
|
188
|
+
local results = {}
|
189
|
+
for i,tag in ipairs(tags) do if _tags[tag] then table.insert(results, tag) end end
|
190
|
+
|
191
|
+
redis.call('hset', QlessJob.ns .. jid, 'tags', cjson.encode(results))
|
192
|
+
return results
|
193
|
+
else
|
194
|
+
error('Tag(): Job ' .. jid .. ' does not exist')
|
195
|
+
end
|
196
|
+
elseif command == 'get' then
|
197
|
+
local tag = assert(arg[1], 'Tag(): Arg "tag" missing')
|
198
|
+
local offset = assert(tonumber(arg[2] or 0),
|
199
|
+
'Tag(): Arg "offset" not a number: ' .. tostring(arg[2]))
|
200
|
+
local count = assert(tonumber(arg[3] or 25),
|
201
|
+
'Tag(): Arg "count" not a number: ' .. tostring(arg[3]))
|
202
|
+
return {
|
203
|
+
total = redis.call('zcard', 'ql:t:' .. tag),
|
204
|
+
jobs = redis.call('zrange', 'ql:t:' .. tag, offset, offset + count - 1)
|
205
|
+
}
|
206
|
+
elseif command == 'top' then
|
207
|
+
local offset = assert(tonumber(arg[1] or 0) , 'Tag(): Arg "offset" not a number: ' .. tostring(arg[1]))
|
208
|
+
local count = assert(tonumber(arg[2] or 25), 'Tag(): Arg "count" not a number: ' .. tostring(arg[2]))
|
209
|
+
return redis.call('zrevrangebyscore', 'ql:tags', '+inf', 2, 'limit', offset, count)
|
210
|
+
else
|
211
|
+
error('Tag(): First argument must be "add", "remove" or "get"')
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
function Qless.cancel(...)
|
216
|
+
local dependents = {}
|
217
|
+
for _, jid in ipairs(arg) do
|
218
|
+
dependents[jid] = redis.call(
|
219
|
+
'smembers', QlessJob.ns .. jid .. '-dependents') or {}
|
220
|
+
end
|
221
|
+
|
222
|
+
for i, jid in ipairs(arg) do
|
223
|
+
for j, dep in ipairs(dependents[jid]) do
|
224
|
+
if dependents[dep] == nil or dependents[dep] == false then
|
225
|
+
error('Cancel(): ' .. jid .. ' is a dependency of ' .. dep ..
|
226
|
+
' but is not mentioned to be canceled')
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
for _, jid in ipairs(arg) do
|
232
|
+
local state, queue, failure, worker = unpack(redis.call(
|
233
|
+
'hmget', QlessJob.ns .. jid, 'state', 'queue', 'failure', 'worker'))
|
234
|
+
|
235
|
+
if state ~= 'complete' then
|
236
|
+
local encoded = cjson.encode({
|
237
|
+
jid = jid,
|
238
|
+
worker = worker,
|
239
|
+
event = 'canceled',
|
240
|
+
queue = queue
|
241
|
+
})
|
242
|
+
Qless.publish('log', encoded)
|
243
|
+
|
244
|
+
if worker and (worker ~= '') then
|
245
|
+
redis.call('zrem', 'ql:w:' .. worker .. ':jobs', jid)
|
246
|
+
Qless.publish('w:' .. worker, encoded)
|
247
|
+
end
|
248
|
+
|
249
|
+
if queue then
|
250
|
+
local queue = Qless.queue(queue)
|
251
|
+
queue.work.remove(jid)
|
252
|
+
queue.locks.remove(jid)
|
253
|
+
queue.scheduled.remove(jid)
|
254
|
+
queue.depends.remove(jid)
|
255
|
+
end
|
256
|
+
|
257
|
+
for i, j in ipairs(redis.call(
|
258
|
+
'smembers', QlessJob.ns .. jid .. '-dependencies')) do
|
259
|
+
redis.call('srem', QlessJob.ns .. j .. '-dependents', jid)
|
260
|
+
end
|
261
|
+
|
262
|
+
redis.call('del', QlessJob.ns .. jid .. '-dependencies')
|
263
|
+
|
264
|
+
if state == 'failed' then
|
265
|
+
failure = cjson.decode(failure)
|
266
|
+
redis.call('lrem', 'ql:f:' .. failure.group, 0, jid)
|
267
|
+
if redis.call('llen', 'ql:f:' .. failure.group) == 0 then
|
268
|
+
redis.call('srem', 'ql:failures', failure.group)
|
269
|
+
end
|
270
|
+
local bin = failure.when - (failure.when % 86400)
|
271
|
+
local failed = redis.call(
|
272
|
+
'hget', 'ql:s:stats:' .. bin .. ':' .. queue, 'failed')
|
273
|
+
redis.call('hset',
|
274
|
+
'ql:s:stats:' .. bin .. ':' .. queue, 'failed', failed - 1)
|
275
|
+
end
|
276
|
+
|
277
|
+
local tags = cjson.decode(
|
278
|
+
redis.call('hget', QlessJob.ns .. jid, 'tags') or '{}')
|
279
|
+
for i, tag in ipairs(tags) do
|
280
|
+
redis.call('zrem', 'ql:t:' .. tag, jid)
|
281
|
+
redis.call('zincrby', 'ql:tags', -1, tag)
|
282
|
+
end
|
283
|
+
|
284
|
+
if redis.call('zscore', 'ql:tracked', jid) ~= false then
|
285
|
+
Qless.publish('canceled', jid)
|
286
|
+
end
|
287
|
+
|
288
|
+
redis.call('del', QlessJob.ns .. jid)
|
289
|
+
redis.call('del', QlessJob.ns .. jid .. '-history')
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
return arg
|
294
|
+
end
|
295
|
+
|
296
|
+
|
297
|
+
Qless.config.defaults = {
|
298
|
+
['application'] = 'qless',
|
299
|
+
['heartbeat'] = 60,
|
300
|
+
['grace-period'] = 10,
|
301
|
+
['stats-history'] = 30,
|
302
|
+
['histogram-history'] = 7,
|
303
|
+
['jobs-history-count'] = 50000,
|
304
|
+
['jobs-history'] = 604800
|
305
|
+
}
|
306
|
+
|
307
|
+
Qless.config.get = function(key, default)
|
308
|
+
if key then
|
309
|
+
return redis.call('hget', 'ql:config', key) or
|
310
|
+
Qless.config.defaults[key] or default
|
311
|
+
else
|
312
|
+
local reply = redis.call('hgetall', 'ql:config')
|
313
|
+
for i = 1, #reply, 2 do
|
314
|
+
Qless.config.defaults[reply[i]] = reply[i + 1]
|
315
|
+
end
|
316
|
+
return Qless.config.defaults
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
Qless.config.set = function(option, value)
|
321
|
+
assert(option, 'config.set(): Arg "option" missing')
|
322
|
+
assert(value , 'config.set(): Arg "value" missing')
|
323
|
+
Qless.publish('log', cjson.encode({
|
324
|
+
event = 'config_set',
|
325
|
+
option = option,
|
326
|
+
value = value
|
327
|
+
}))
|
328
|
+
|
329
|
+
redis.call('hset', 'ql:config', option, value)
|
330
|
+
end
|
331
|
+
|
332
|
+
Qless.config.unset = function(option)
|
333
|
+
assert(option, 'config.unset(): Arg "option" missing')
|
334
|
+
Qless.publish('log', cjson.encode({
|
335
|
+
event = 'config_unset',
|
336
|
+
option = option
|
337
|
+
}))
|
338
|
+
|
339
|
+
redis.call('hdel', 'ql:config', option)
|
340
|
+
end
|
341
|
+
|
342
|
+
function QlessJob:data(...)
|
343
|
+
local job = redis.call(
|
344
|
+
'hmget', QlessJob.ns .. self.jid, 'jid', 'klass', 'state', 'queue',
|
345
|
+
'worker', 'priority', 'expires', 'retries', 'remaining', 'data',
|
346
|
+
'tags', 'failure', 'spawned_from_jid')
|
347
|
+
|
348
|
+
if not job[1] then
|
349
|
+
return nil
|
350
|
+
end
|
351
|
+
|
352
|
+
local data = {
|
353
|
+
jid = job[1],
|
354
|
+
klass = job[2],
|
355
|
+
state = job[3],
|
356
|
+
queue = job[4],
|
357
|
+
worker = job[5] or '',
|
358
|
+
tracked = redis.call(
|
359
|
+
'zscore', 'ql:tracked', self.jid) ~= false,
|
360
|
+
priority = tonumber(job[6]),
|
361
|
+
expires = tonumber(job[7]) or 0,
|
362
|
+
retries = tonumber(job[8]),
|
363
|
+
remaining = math.floor(tonumber(job[9])),
|
364
|
+
data = job[10],
|
365
|
+
tags = cjson.decode(job[11]),
|
366
|
+
history = self:history(),
|
367
|
+
failure = cjson.decode(job[12] or '{}'),
|
368
|
+
spawned_from_jid = job[13],
|
369
|
+
dependents = redis.call(
|
370
|
+
'smembers', QlessJob.ns .. self.jid .. '-dependents'),
|
371
|
+
dependencies = redis.call(
|
372
|
+
'smembers', QlessJob.ns .. self.jid .. '-dependencies')
|
373
|
+
}
|
374
|
+
|
375
|
+
if #arg > 0 then
|
376
|
+
local response = {}
|
377
|
+
for index, key in ipairs(arg) do
|
378
|
+
table.insert(response, data[key])
|
379
|
+
end
|
380
|
+
return response
|
381
|
+
else
|
382
|
+
return data
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
function QlessJob:complete(now, worker, queue, raw_data, ...)
|
387
|
+
assert(worker, 'Complete(): Arg "worker" missing')
|
388
|
+
assert(queue , 'Complete(): Arg "queue" missing')
|
389
|
+
local data = assert(cjson.decode(raw_data),
|
390
|
+
'Complete(): Arg "data" missing or not JSON: ' .. tostring(raw_data))
|
391
|
+
|
392
|
+
local options = {}
|
393
|
+
for i = 1, #arg, 2 do options[arg[i]] = arg[i + 1] end
|
394
|
+
|
395
|
+
local nextq = options['next']
|
396
|
+
local delay = assert(tonumber(options['delay'] or 0))
|
397
|
+
local depends = assert(cjson.decode(options['depends'] or '[]'),
|
398
|
+
'Complete(): Arg "depends" not JSON: ' .. tostring(options['depends']))
|
399
|
+
|
400
|
+
if options['delay'] and nextq == nil then
|
401
|
+
error('Complete(): "delay" cannot be used without a "next".')
|
402
|
+
end
|
403
|
+
|
404
|
+
if options['depends'] and nextq == nil then
|
405
|
+
error('Complete(): "depends" cannot be used without a "next".')
|
406
|
+
end
|
407
|
+
|
408
|
+
local bin = now - (now % 86400)
|
409
|
+
|
410
|
+
local lastworker, state, priority, retries, current_queue = unpack(
|
411
|
+
redis.call('hmget', QlessJob.ns .. self.jid, 'worker', 'state',
|
412
|
+
'priority', 'retries', 'queue'))
|
413
|
+
|
414
|
+
if lastworker == false then
|
415
|
+
error('Complete(): Job ' .. self.jid .. ' does not exist')
|
416
|
+
elseif (state ~= 'running') then
|
417
|
+
error('Complete(): Job ' .. self.jid .. ' is not currently running: ' ..
|
418
|
+
state)
|
419
|
+
elseif lastworker ~= worker then
|
420
|
+
error('Complete(): Job ' .. self.jid ..
|
421
|
+
' has been handed out to another worker: ' .. tostring(lastworker))
|
422
|
+
elseif queue ~= current_queue then
|
423
|
+
error('Complete(): Job ' .. self.jid .. ' running in another queue: ' ..
|
424
|
+
tostring(current_queue))
|
425
|
+
end
|
426
|
+
|
427
|
+
self:history(now, 'done')
|
428
|
+
|
429
|
+
if raw_data then
|
430
|
+
redis.call('hset', QlessJob.ns .. self.jid, 'data', raw_data)
|
431
|
+
end
|
432
|
+
|
433
|
+
local queue_obj = Qless.queue(queue)
|
434
|
+
queue_obj.work.remove(self.jid)
|
435
|
+
queue_obj.locks.remove(self.jid)
|
436
|
+
queue_obj.scheduled.remove(self.jid)
|
437
|
+
|
438
|
+
local time = tonumber(
|
439
|
+
redis.call('hget', QlessJob.ns .. self.jid, 'time') or now)
|
440
|
+
local waiting = now - time
|
441
|
+
Qless.queue(queue):stat(now, 'run', waiting)
|
442
|
+
redis.call('hset', QlessJob.ns .. self.jid,
|
443
|
+
'time', string.format("%.20f", now))
|
444
|
+
|
445
|
+
redis.call('zrem', 'ql:w:' .. worker .. ':jobs', self.jid)
|
446
|
+
|
447
|
+
if redis.call('zscore', 'ql:tracked', self.jid) ~= false then
|
448
|
+
Qless.publish('completed', self.jid)
|
449
|
+
end
|
450
|
+
|
451
|
+
if nextq then
|
452
|
+
queue_obj = Qless.queue(nextq)
|
453
|
+
Qless.publish('log', cjson.encode({
|
454
|
+
jid = self.jid,
|
455
|
+
event = 'advanced',
|
456
|
+
queue = queue,
|
457
|
+
to = nextq
|
458
|
+
}))
|
459
|
+
|
460
|
+
self:history(now, 'put', {q = nextq})
|
461
|
+
|
462
|
+
if redis.call('zscore', 'ql:queues', nextq) == false then
|
463
|
+
redis.call('zadd', 'ql:queues', now, nextq)
|
464
|
+
end
|
465
|
+
|
466
|
+
redis.call('hmset', QlessJob.ns .. self.jid,
|
467
|
+
'state', 'waiting',
|
468
|
+
'worker', '',
|
469
|
+
'failure', '{}',
|
470
|
+
'queue', nextq,
|
471
|
+
'expires', 0,
|
472
|
+
'remaining', tonumber(retries))
|
473
|
+
|
474
|
+
if (delay > 0) and (#depends == 0) then
|
475
|
+
queue_obj.scheduled.add(now + delay, self.jid)
|
476
|
+
return 'scheduled'
|
477
|
+
else
|
478
|
+
local count = 0
|
479
|
+
for i, j in ipairs(depends) do
|
480
|
+
local state = redis.call('hget', QlessJob.ns .. j, 'state')
|
481
|
+
if (state and state ~= 'complete') then
|
482
|
+
count = count + 1
|
483
|
+
redis.call(
|
484
|
+
'sadd', QlessJob.ns .. j .. '-dependents',self.jid)
|
485
|
+
redis.call(
|
486
|
+
'sadd', QlessJob.ns .. self.jid .. '-dependencies', j)
|
487
|
+
end
|
488
|
+
end
|
489
|
+
if count > 0 then
|
490
|
+
queue_obj.depends.add(now, self.jid)
|
491
|
+
redis.call('hset', QlessJob.ns .. self.jid, 'state', 'depends')
|
492
|
+
if delay > 0 then
|
493
|
+
queue_obj.depends.add(now, self.jid)
|
494
|
+
redis.call('hset', QlessJob.ns .. self.jid, 'scheduled', now + delay)
|
495
|
+
end
|
496
|
+
return 'depends'
|
497
|
+
else
|
498
|
+
queue_obj.work.add(now, priority, self.jid)
|
499
|
+
return 'waiting'
|
500
|
+
end
|
501
|
+
end
|
502
|
+
else
|
503
|
+
Qless.publish('log', cjson.encode({
|
504
|
+
jid = self.jid,
|
505
|
+
event = 'completed',
|
506
|
+
queue = queue
|
507
|
+
}))
|
508
|
+
|
509
|
+
redis.call('hmset', QlessJob.ns .. self.jid,
|
510
|
+
'state', 'complete',
|
511
|
+
'worker', '',
|
512
|
+
'failure', '{}',
|
513
|
+
'queue', '',
|
514
|
+
'expires', 0,
|
515
|
+
'remaining', tonumber(retries))
|
516
|
+
|
517
|
+
local count = Qless.config.get('jobs-history-count')
|
518
|
+
local time = Qless.config.get('jobs-history')
|
519
|
+
|
520
|
+
count = tonumber(count or 50000)
|
521
|
+
time = tonumber(time or 7 * 24 * 60 * 60)
|
522
|
+
|
523
|
+
redis.call('zadd', 'ql:completed', now, self.jid)
|
524
|
+
|
525
|
+
local jids = redis.call('zrangebyscore', 'ql:completed', 0, now - time)
|
526
|
+
for index, jid in ipairs(jids) do
|
527
|
+
local tags = cjson.decode(
|
528
|
+
redis.call('hget', QlessJob.ns .. jid, 'tags') or '{}')
|
529
|
+
for i, tag in ipairs(tags) do
|
530
|
+
redis.call('zrem', 'ql:t:' .. tag, jid)
|
531
|
+
redis.call('zincrby', 'ql:tags', -1, tag)
|
532
|
+
end
|
533
|
+
redis.call('del', QlessJob.ns .. jid)
|
534
|
+
redis.call('del', QlessJob.ns .. jid .. '-history')
|
535
|
+
end
|
536
|
+
redis.call('zremrangebyscore', 'ql:completed', 0, now - time)
|
537
|
+
|
538
|
+
jids = redis.call('zrange', 'ql:completed', 0, (-1-count))
|
539
|
+
for index, jid in ipairs(jids) do
|
540
|
+
local tags = cjson.decode(
|
541
|
+
redis.call('hget', QlessJob.ns .. jid, 'tags') or '{}')
|
542
|
+
for i, tag in ipairs(tags) do
|
543
|
+
redis.call('zrem', 'ql:t:' .. tag, jid)
|
544
|
+
redis.call('zincrby', 'ql:tags', -1, tag)
|
545
|
+
end
|
546
|
+
redis.call('del', QlessJob.ns .. jid)
|
547
|
+
redis.call('del', QlessJob.ns .. jid .. '-history')
|
548
|
+
end
|
549
|
+
redis.call('zremrangebyrank', 'ql:completed', 0, (-1-count))
|
550
|
+
|
551
|
+
for i, j in ipairs(redis.call(
|
552
|
+
'smembers', QlessJob.ns .. self.jid .. '-dependents')) do
|
553
|
+
redis.call('srem', QlessJob.ns .. j .. '-dependencies', self.jid)
|
554
|
+
if redis.call(
|
555
|
+
'scard', QlessJob.ns .. j .. '-dependencies') == 0 then
|
556
|
+
local q, p, scheduled = unpack(
|
557
|
+
redis.call('hmget', QlessJob.ns .. j, 'queue', 'priority', 'scheduled'))
|
558
|
+
if q then
|
559
|
+
local queue = Qless.queue(q)
|
560
|
+
queue.depends.remove(j)
|
561
|
+
if scheduled then
|
562
|
+
queue.scheduled.add(scheduled, j)
|
563
|
+
redis.call('hset', QlessJob.ns .. j, 'state', 'scheduled')
|
564
|
+
redis.call('hdel', QlessJob.ns .. j, 'scheduled')
|
565
|
+
else
|
566
|
+
queue.work.add(now, p, j)
|
567
|
+
redis.call('hset', QlessJob.ns .. j, 'state', 'waiting')
|
568
|
+
end
|
569
|
+
end
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
redis.call('del', QlessJob.ns .. self.jid .. '-dependents')
|
574
|
+
|
575
|
+
return 'complete'
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
function QlessJob:fail(now, worker, group, message, data)
|
580
|
+
local worker = assert(worker , 'Fail(): Arg "worker" missing')
|
581
|
+
local group = assert(group , 'Fail(): Arg "group" missing')
|
582
|
+
local message = assert(message , 'Fail(): Arg "message" missing')
|
583
|
+
|
584
|
+
local bin = now - (now % 86400)
|
585
|
+
|
586
|
+
if data then
|
587
|
+
data = cjson.decode(data)
|
588
|
+
end
|
589
|
+
|
590
|
+
local queue, state, oldworker = unpack(redis.call(
|
591
|
+
'hmget', QlessJob.ns .. self.jid, 'queue', 'state', 'worker'))
|
592
|
+
|
593
|
+
if not state then
|
594
|
+
error('Fail(): Job ' .. self.jid .. 'does not exist')
|
595
|
+
elseif state ~= 'running' then
|
596
|
+
error('Fail(): Job ' .. self.jid .. 'not currently running: ' .. state)
|
597
|
+
elseif worker ~= oldworker then
|
598
|
+
error('Fail(): Job ' .. self.jid .. ' running with another worker: ' ..
|
599
|
+
oldworker)
|
600
|
+
end
|
601
|
+
|
602
|
+
Qless.publish('log', cjson.encode({
|
603
|
+
jid = self.jid,
|
604
|
+
event = 'failed',
|
605
|
+
worker = worker,
|
606
|
+
group = group,
|
607
|
+
message = message
|
608
|
+
}))
|
609
|
+
|
610
|
+
if redis.call('zscore', 'ql:tracked', self.jid) ~= false then
|
611
|
+
Qless.publish('failed', self.jid)
|
612
|
+
end
|
613
|
+
|
614
|
+
redis.call('zrem', 'ql:w:' .. worker .. ':jobs', self.jid)
|
615
|
+
|
616
|
+
self:history(now, 'failed', {worker = worker, group = group})
|
617
|
+
|
618
|
+
redis.call('hincrby', 'ql:s:stats:' .. bin .. ':' .. queue, 'failures', 1)
|
619
|
+
redis.call('hincrby', 'ql:s:stats:' .. bin .. ':' .. queue, 'failed' , 1)
|
620
|
+
|
621
|
+
local queue_obj = Qless.queue(queue)
|
622
|
+
queue_obj.work.remove(self.jid)
|
623
|
+
queue_obj.locks.remove(self.jid)
|
624
|
+
queue_obj.scheduled.remove(self.jid)
|
625
|
+
|
626
|
+
if data then
|
627
|
+
redis.call('hset', QlessJob.ns .. self.jid, 'data', cjson.encode(data))
|
628
|
+
end
|
629
|
+
|
630
|
+
redis.call('hmset', QlessJob.ns .. self.jid,
|
631
|
+
'state', 'failed',
|
632
|
+
'worker', '',
|
633
|
+
'expires', '',
|
634
|
+
'failure', cjson.encode({
|
635
|
+
['group'] = group,
|
636
|
+
['message'] = message,
|
637
|
+
['when'] = math.floor(now),
|
638
|
+
['worker'] = worker
|
639
|
+
}))
|
640
|
+
|
641
|
+
redis.call('sadd', 'ql:failures', group)
|
642
|
+
redis.call('lpush', 'ql:f:' .. group, self.jid)
|
643
|
+
|
644
|
+
|
645
|
+
return self.jid
|
646
|
+
end
|
647
|
+
|
648
|
+
function QlessJob:retry(now, queue, worker, delay, group, message)
|
649
|
+
assert(queue , 'Retry(): Arg "queue" missing')
|
650
|
+
assert(worker, 'Retry(): Arg "worker" missing')
|
651
|
+
delay = assert(tonumber(delay or 0),
|
652
|
+
'Retry(): Arg "delay" not a number: ' .. tostring(delay))
|
653
|
+
|
654
|
+
local oldqueue, state, retries, oldworker, priority, failure = unpack(
|
655
|
+
redis.call('hmget', QlessJob.ns .. self.jid, 'queue', 'state',
|
656
|
+
'retries', 'worker', 'priority', 'failure'))
|
657
|
+
|
658
|
+
if oldworker == false then
|
659
|
+
error('Retry(): Job ' .. self.jid .. ' does not exist')
|
660
|
+
elseif state ~= 'running' then
|
661
|
+
error('Retry(): Job ' .. self.jid .. ' is not currently running: ' ..
|
662
|
+
state)
|
663
|
+
elseif oldworker ~= worker then
|
664
|
+
error('Retry(): Job ' .. self.jid ..
|
665
|
+
' has been given to another worker: ' .. oldworker)
|
666
|
+
end
|
667
|
+
|
668
|
+
local remaining = tonumber(redis.call(
|
669
|
+
'hincrby', QlessJob.ns .. self.jid, 'remaining', -1))
|
670
|
+
redis.call('hdel', QlessJob.ns .. self.jid, 'grace')
|
671
|
+
|
672
|
+
Qless.queue(oldqueue).locks.remove(self.jid)
|
673
|
+
|
674
|
+
redis.call('zrem', 'ql:w:' .. worker .. ':jobs', self.jid)
|
675
|
+
|
676
|
+
if remaining < 0 then
|
677
|
+
local group = group or 'failed-retries-' .. queue
|
678
|
+
self:history(now, 'failed', {['group'] = group})
|
679
|
+
|
680
|
+
redis.call('hmset', QlessJob.ns .. self.jid, 'state', 'failed',
|
681
|
+
'worker', '',
|
682
|
+
'expires', '')
|
683
|
+
if group ~= nil and message ~= nil then
|
684
|
+
redis.call('hset', QlessJob.ns .. self.jid,
|
685
|
+
'failure', cjson.encode({
|
686
|
+
['group'] = group,
|
687
|
+
['message'] = message,
|
688
|
+
['when'] = math.floor(now),
|
689
|
+
['worker'] = worker
|
690
|
+
})
|
691
|
+
)
|
692
|
+
else
|
693
|
+
redis.call('hset', QlessJob.ns .. self.jid,
|
694
|
+
'failure', cjson.encode({
|
695
|
+
['group'] = group,
|
696
|
+
['message'] =
|
697
|
+
'Job exhausted retries in queue "' .. oldqueue .. '"',
|
698
|
+
['when'] = now,
|
699
|
+
['worker'] = unpack(self:data('worker'))
|
700
|
+
}))
|
701
|
+
end
|
702
|
+
|
703
|
+
if redis.call('zscore', 'ql:tracked', self.jid) ~= false then
|
704
|
+
Qless.publish('failed', self.jid)
|
705
|
+
end
|
706
|
+
|
707
|
+
redis.call('sadd', 'ql:failures', group)
|
708
|
+
redis.call('lpush', 'ql:f:' .. group, self.jid)
|
709
|
+
local bin = now - (now % 86400)
|
710
|
+
redis.call('hincrby', 'ql:s:stats:' .. bin .. ':' .. queue, 'failures', 1)
|
711
|
+
redis.call('hincrby', 'ql:s:stats:' .. bin .. ':' .. queue, 'failed' , 1)
|
712
|
+
else
|
713
|
+
local queue_obj = Qless.queue(queue)
|
714
|
+
if delay > 0 then
|
715
|
+
queue_obj.scheduled.add(now + delay, self.jid)
|
716
|
+
redis.call('hset', QlessJob.ns .. self.jid, 'state', 'scheduled')
|
717
|
+
else
|
718
|
+
queue_obj.work.add(now, priority, self.jid)
|
719
|
+
redis.call('hset', QlessJob.ns .. self.jid, 'state', 'waiting')
|
720
|
+
end
|
721
|
+
|
722
|
+
if group ~= nil and message ~= nil then
|
723
|
+
redis.call('hset', QlessJob.ns .. self.jid,
|
724
|
+
'failure', cjson.encode({
|
725
|
+
['group'] = group,
|
726
|
+
['message'] = message,
|
727
|
+
['when'] = math.floor(now),
|
728
|
+
['worker'] = worker
|
729
|
+
})
|
730
|
+
)
|
731
|
+
end
|
732
|
+
end
|
733
|
+
|
734
|
+
return math.floor(remaining)
|
735
|
+
end
|
736
|
+
|
737
|
+
function QlessJob:depends(now, command, ...)
|
738
|
+
assert(command, 'Depends(): Arg "command" missing')
|
739
|
+
local state = redis.call('hget', QlessJob.ns .. self.jid, 'state')
|
740
|
+
if state ~= 'depends' then
|
741
|
+
error('Depends(): Job ' .. self.jid ..
|
742
|
+
' not in the depends state: ' .. tostring(state))
|
743
|
+
end
|
744
|
+
|
745
|
+
if command == 'on' then
|
746
|
+
for i, j in ipairs(arg) do
|
747
|
+
local state = redis.call('hget', QlessJob.ns .. j, 'state')
|
748
|
+
if (state and state ~= 'complete') then
|
749
|
+
redis.call(
|
750
|
+
'sadd', QlessJob.ns .. j .. '-dependents' , self.jid)
|
751
|
+
redis.call(
|
752
|
+
'sadd', QlessJob.ns .. self.jid .. '-dependencies', j)
|
753
|
+
end
|
754
|
+
end
|
755
|
+
return true
|
756
|
+
elseif command == 'off' then
|
757
|
+
if arg[1] == 'all' then
|
758
|
+
for i, j in ipairs(redis.call(
|
759
|
+
'smembers', QlessJob.ns .. self.jid .. '-dependencies')) do
|
760
|
+
redis.call('srem', QlessJob.ns .. j .. '-dependents', self.jid)
|
761
|
+
end
|
762
|
+
redis.call('del', QlessJob.ns .. self.jid .. '-dependencies')
|
763
|
+
local q, p = unpack(redis.call(
|
764
|
+
'hmget', QlessJob.ns .. self.jid, 'queue', 'priority'))
|
765
|
+
if q then
|
766
|
+
local queue_obj = Qless.queue(q)
|
767
|
+
queue_obj.depends.remove(self.jid)
|
768
|
+
queue_obj.work.add(now, p, self.jid)
|
769
|
+
redis.call('hset', QlessJob.ns .. self.jid, 'state', 'waiting')
|
770
|
+
end
|
771
|
+
else
|
772
|
+
for i, j in ipairs(arg) do
|
773
|
+
redis.call('srem', QlessJob.ns .. j .. '-dependents', self.jid)
|
774
|
+
redis.call(
|
775
|
+
'srem', QlessJob.ns .. self.jid .. '-dependencies', j)
|
776
|
+
if redis.call('scard',
|
777
|
+
QlessJob.ns .. self.jid .. '-dependencies') == 0 then
|
778
|
+
local q, p = unpack(redis.call(
|
779
|
+
'hmget', QlessJob.ns .. self.jid, 'queue', 'priority'))
|
780
|
+
if q then
|
781
|
+
local queue_obj = Qless.queue(q)
|
782
|
+
queue_obj.depends.remove(self.jid)
|
783
|
+
queue_obj.work.add(now, p, self.jid)
|
784
|
+
redis.call('hset',
|
785
|
+
QlessJob.ns .. self.jid, 'state', 'waiting')
|
786
|
+
end
|
787
|
+
end
|
788
|
+
end
|
789
|
+
end
|
790
|
+
return true
|
791
|
+
else
|
792
|
+
error('Depends(): Argument "command" must be "on" or "off"')
|
793
|
+
end
|
794
|
+
end
|
795
|
+
|
796
|
+
function QlessJob:heartbeat(now, worker, data)
|
797
|
+
assert(worker, 'Heatbeat(): Arg "worker" missing')
|
798
|
+
|
799
|
+
local queue = redis.call('hget', QlessJob.ns .. self.jid, 'queue') or ''
|
800
|
+
local expires = now + tonumber(
|
801
|
+
Qless.config.get(queue .. '-heartbeat') or
|
802
|
+
Qless.config.get('heartbeat', 60))
|
803
|
+
|
804
|
+
if data then
|
805
|
+
data = cjson.decode(data)
|
806
|
+
end
|
807
|
+
|
808
|
+
local job_worker, state = unpack(
|
809
|
+
redis.call('hmget', QlessJob.ns .. self.jid, 'worker', 'state'))
|
810
|
+
if job_worker == false then
|
811
|
+
error('Heartbeat(): Job ' .. self.jid .. ' does not exist')
|
812
|
+
elseif state ~= 'running' then
|
813
|
+
error(
|
814
|
+
'Heartbeat(): Job ' .. self.jid .. ' not currently running: ' .. state)
|
815
|
+
elseif job_worker ~= worker or #job_worker == 0 then
|
816
|
+
error(
|
817
|
+
'Heartbeat(): Job ' .. self.jid ..
|
818
|
+
' given out to another worker: ' .. job_worker)
|
819
|
+
else
|
820
|
+
if data then
|
821
|
+
redis.call('hmset', QlessJob.ns .. self.jid, 'expires',
|
822
|
+
expires, 'worker', worker, 'data', cjson.encode(data))
|
823
|
+
else
|
824
|
+
redis.call('hmset', QlessJob.ns .. self.jid,
|
825
|
+
'expires', expires, 'worker', worker)
|
826
|
+
end
|
827
|
+
|
828
|
+
redis.call('zadd', 'ql:w:' .. worker .. ':jobs', expires, self.jid)
|
829
|
+
|
830
|
+
redis.call('zadd', 'ql:workers', now, worker)
|
831
|
+
|
832
|
+
local queue = Qless.queue(
|
833
|
+
redis.call('hget', QlessJob.ns .. self.jid, 'queue'))
|
834
|
+
queue.locks.add(expires, self.jid)
|
835
|
+
return expires
|
836
|
+
end
|
837
|
+
end
|
838
|
+
|
839
|
+
function QlessJob:priority(priority)
|
840
|
+
priority = assert(tonumber(priority),
|
841
|
+
'Priority(): Arg "priority" missing or not a number: ' ..
|
842
|
+
tostring(priority))
|
843
|
+
|
844
|
+
local queue = redis.call('hget', QlessJob.ns .. self.jid, 'queue')
|
845
|
+
|
846
|
+
if queue == nil or queue == false then
|
847
|
+
error('Priority(): Job ' .. self.jid .. ' does not exist')
|
848
|
+
elseif queue == '' then
|
849
|
+
redis.call('hset', QlessJob.ns .. self.jid, 'priority', priority)
|
850
|
+
return priority
|
851
|
+
else
|
852
|
+
local queue_obj = Qless.queue(queue)
|
853
|
+
if queue_obj.work.score(self.jid) then
|
854
|
+
queue_obj.work.add(0, priority, self.jid)
|
855
|
+
end
|
856
|
+
redis.call('hset', QlessJob.ns .. self.jid, 'priority', priority)
|
857
|
+
return priority
|
858
|
+
end
|
859
|
+
end
|
860
|
+
|
861
|
+
function QlessJob:update(data)
|
862
|
+
local tmp = {}
|
863
|
+
for k, v in pairs(data) do
|
864
|
+
table.insert(tmp, k)
|
865
|
+
table.insert(tmp, v)
|
866
|
+
end
|
867
|
+
redis.call('hmset', QlessJob.ns .. self.jid, unpack(tmp))
|
868
|
+
end
|
869
|
+
|
870
|
+
function QlessJob:timeout(now)
|
871
|
+
local queue_name, state, worker = unpack(redis.call('hmget',
|
872
|
+
QlessJob.ns .. self.jid, 'queue', 'state', 'worker'))
|
873
|
+
if queue_name == nil or queue_name == false then
|
874
|
+
error('Timeout(): Job ' .. self.jid .. ' does not exist')
|
875
|
+
elseif state ~= 'running' then
|
876
|
+
error('Timeout(): Job ' .. self.jid .. ' not running')
|
877
|
+
else
|
878
|
+
self:history(now, 'timed-out')
|
879
|
+
local queue = Qless.queue(queue_name)
|
880
|
+
queue.locks.remove(self.jid)
|
881
|
+
queue.work.add(now, '+inf', self.jid)
|
882
|
+
redis.call('hmset', QlessJob.ns .. self.jid,
|
883
|
+
'state', 'stalled', 'expires', 0)
|
884
|
+
local encoded = cjson.encode({
|
885
|
+
jid = self.jid,
|
886
|
+
event = 'lock_lost',
|
887
|
+
worker = worker
|
888
|
+
})
|
889
|
+
Qless.publish('w:' .. worker, encoded)
|
890
|
+
Qless.publish('log', encoded)
|
891
|
+
return queue_name
|
892
|
+
end
|
893
|
+
end
|
894
|
+
|
895
|
+
function QlessJob:exists()
|
896
|
+
return redis.call('exists', QlessJob.ns .. self.jid) == 1
|
897
|
+
end
|
898
|
+
|
899
|
+
function QlessJob:history(now, what, item)
|
900
|
+
local history = redis.call('hget', QlessJob.ns .. self.jid, 'history')
|
901
|
+
if history then
|
902
|
+
history = cjson.decode(history)
|
903
|
+
for i, value in ipairs(history) do
|
904
|
+
redis.call('rpush', QlessJob.ns .. self.jid .. '-history',
|
905
|
+
cjson.encode({math.floor(value.put), 'put', {q = value.q}}))
|
906
|
+
|
907
|
+
if value.popped then
|
908
|
+
redis.call('rpush', QlessJob.ns .. self.jid .. '-history',
|
909
|
+
cjson.encode({math.floor(value.popped), 'popped',
|
910
|
+
{worker = value.worker}}))
|
911
|
+
end
|
912
|
+
|
913
|
+
if value.failed then
|
914
|
+
redis.call('rpush', QlessJob.ns .. self.jid .. '-history',
|
915
|
+
cjson.encode(
|
916
|
+
{math.floor(value.failed), 'failed', nil}))
|
917
|
+
end
|
918
|
+
|
919
|
+
if value.done then
|
920
|
+
redis.call('rpush', QlessJob.ns .. self.jid .. '-history',
|
921
|
+
cjson.encode(
|
922
|
+
{math.floor(value.done), 'done', nil}))
|
923
|
+
end
|
924
|
+
end
|
925
|
+
redis.call('hdel', QlessJob.ns .. self.jid, 'history')
|
926
|
+
end
|
927
|
+
|
928
|
+
if what == nil then
|
929
|
+
local response = {}
|
930
|
+
for i, value in ipairs(redis.call('lrange',
|
931
|
+
QlessJob.ns .. self.jid .. '-history', 0, -1)) do
|
932
|
+
value = cjson.decode(value)
|
933
|
+
local dict = value[3] or {}
|
934
|
+
dict['when'] = value[1]
|
935
|
+
dict['what'] = value[2]
|
936
|
+
table.insert(response, dict)
|
937
|
+
end
|
938
|
+
return response
|
939
|
+
else
|
940
|
+
local count = tonumber(Qless.config.get('max-job-history', 100))
|
941
|
+
if count > 0 then
|
942
|
+
local obj = redis.call('lpop', QlessJob.ns .. self.jid .. '-history')
|
943
|
+
redis.call('ltrim', QlessJob.ns .. self.jid .. '-history', -count + 2, -1)
|
944
|
+
if obj ~= nil and obj ~= false then
|
945
|
+
redis.call('lpush', QlessJob.ns .. self.jid .. '-history', obj)
|
946
|
+
end
|
947
|
+
end
|
948
|
+
return redis.call('rpush', QlessJob.ns .. self.jid .. '-history',
|
949
|
+
cjson.encode({math.floor(now), what, item}))
|
950
|
+
end
|
951
|
+
end
|
952
|
+
function Qless.queue(name)
|
953
|
+
assert(name, 'Queue(): no queue name provided')
|
954
|
+
local queue = {}
|
955
|
+
setmetatable(queue, QlessQueue)
|
956
|
+
queue.name = name
|
957
|
+
|
958
|
+
queue.work = {
|
959
|
+
peek = function(count)
|
960
|
+
if count == 0 then
|
961
|
+
return {}
|
962
|
+
end
|
963
|
+
local jids = {}
|
964
|
+
for index, jid in ipairs(redis.call(
|
965
|
+
'zrevrange', queue:prefix('work'), 0, count - 1)) do
|
966
|
+
table.insert(jids, jid)
|
967
|
+
end
|
968
|
+
return jids
|
969
|
+
end, remove = function(...)
|
970
|
+
if #arg > 0 then
|
971
|
+
return redis.call('zrem', queue:prefix('work'), unpack(arg))
|
972
|
+
end
|
973
|
+
end, add = function(now, priority, jid)
|
974
|
+
if priority ~= '+inf' then
|
975
|
+
priority = priority - (now / 10000000000)
|
976
|
+
end
|
977
|
+
return redis.call('zadd',
|
978
|
+
queue:prefix('work'), priority, jid)
|
979
|
+
end, score = function(jid)
|
980
|
+
return redis.call('zscore', queue:prefix('work'), jid)
|
981
|
+
end, length = function()
|
982
|
+
return redis.call('zcard', queue:prefix('work'))
|
983
|
+
end
|
984
|
+
}
|
985
|
+
|
986
|
+
queue.locks = {
|
987
|
+
expired = function(now, offset, count)
|
988
|
+
return redis.call('zrangebyscore',
|
989
|
+
queue:prefix('locks'), '-inf', now, 'LIMIT', offset, count)
|
990
|
+
end, peek = function(now, offset, count)
|
991
|
+
return redis.call('zrangebyscore', queue:prefix('locks'),
|
992
|
+
now, '+inf', 'LIMIT', offset, count)
|
993
|
+
end, add = function(expires, jid)
|
994
|
+
redis.call('zadd', queue:prefix('locks'), expires, jid)
|
995
|
+
end, remove = function(...)
|
996
|
+
if #arg > 0 then
|
997
|
+
return redis.call('zrem', queue:prefix('locks'), unpack(arg))
|
998
|
+
end
|
999
|
+
end, running = function(now)
|
1000
|
+
return redis.call('zcount', queue:prefix('locks'), now, '+inf')
|
1001
|
+
end, length = function(now)
|
1002
|
+
if now then
|
1003
|
+
return redis.call('zcount', queue:prefix('locks'), 0, now)
|
1004
|
+
else
|
1005
|
+
return redis.call('zcard', queue:prefix('locks'))
|
1006
|
+
end
|
1007
|
+
end
|
1008
|
+
}
|
1009
|
+
|
1010
|
+
queue.depends = {
|
1011
|
+
peek = function(now, offset, count)
|
1012
|
+
return redis.call('zrange',
|
1013
|
+
queue:prefix('depends'), offset, offset + count - 1)
|
1014
|
+
end, add = function(now, jid)
|
1015
|
+
redis.call('zadd', queue:prefix('depends'), now, jid)
|
1016
|
+
end, remove = function(...)
|
1017
|
+
if #arg > 0 then
|
1018
|
+
return redis.call('zrem', queue:prefix('depends'), unpack(arg))
|
1019
|
+
end
|
1020
|
+
end, length = function()
|
1021
|
+
return redis.call('zcard', queue:prefix('depends'))
|
1022
|
+
end
|
1023
|
+
}
|
1024
|
+
|
1025
|
+
queue.scheduled = {
|
1026
|
+
peek = function(now, offset, count)
|
1027
|
+
return redis.call('zrange',
|
1028
|
+
queue:prefix('scheduled'), offset, offset + count - 1)
|
1029
|
+
end, ready = function(now, offset, count)
|
1030
|
+
return redis.call('zrangebyscore',
|
1031
|
+
queue:prefix('scheduled'), 0, now, 'LIMIT', offset, count)
|
1032
|
+
end, add = function(when, jid)
|
1033
|
+
redis.call('zadd', queue:prefix('scheduled'), when, jid)
|
1034
|
+
end, remove = function(...)
|
1035
|
+
if #arg > 0 then
|
1036
|
+
return redis.call('zrem', queue:prefix('scheduled'), unpack(arg))
|
1037
|
+
end
|
1038
|
+
end, length = function()
|
1039
|
+
return redis.call('zcard', queue:prefix('scheduled'))
|
1040
|
+
end
|
1041
|
+
}
|
1042
|
+
|
1043
|
+
queue.recurring = {
|
1044
|
+
peek = function(now, offset, count)
|
1045
|
+
return redis.call('zrangebyscore', queue:prefix('recur'),
|
1046
|
+
0, now, 'LIMIT', offset, count)
|
1047
|
+
end, ready = function(now, offset, count)
|
1048
|
+
end, add = function(when, jid)
|
1049
|
+
redis.call('zadd', queue:prefix('recur'), when, jid)
|
1050
|
+
end, remove = function(...)
|
1051
|
+
if #arg > 0 then
|
1052
|
+
return redis.call('zrem', queue:prefix('recur'), unpack(arg))
|
1053
|
+
end
|
1054
|
+
end, update = function(increment, jid)
|
1055
|
+
redis.call('zincrby', queue:prefix('recur'), increment, jid)
|
1056
|
+
end, score = function(jid)
|
1057
|
+
return redis.call('zscore', queue:prefix('recur'), jid)
|
1058
|
+
end, length = function()
|
1059
|
+
return redis.call('zcard', queue:prefix('recur'))
|
1060
|
+
end
|
1061
|
+
}
|
1062
|
+
return queue
|
1063
|
+
end
|
1064
|
+
|
1065
|
+
function QlessQueue:prefix(group)
|
1066
|
+
if group then
|
1067
|
+
return QlessQueue.ns..self.name..'-'..group
|
1068
|
+
else
|
1069
|
+
return QlessQueue.ns..self.name
|
1070
|
+
end
|
1071
|
+
end
|
1072
|
+
|
1073
|
+
function QlessQueue:stats(now, date)
|
1074
|
+
date = assert(tonumber(date),
|
1075
|
+
'Stats(): Arg "date" missing or not a number: '.. (date or 'nil'))
|
1076
|
+
|
1077
|
+
local bin = date - (date % 86400)
|
1078
|
+
|
1079
|
+
local histokeys = {
|
1080
|
+
'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',
|
1081
|
+
'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',
|
1082
|
+
'h1','h2','h3','h4','h5','h6','h7','h8','h9','h10','h11','h12','h13','h14','h15','h16','h17','h18','h19','h20','h21','h22','h23',
|
1083
|
+
'd1','d2','d3','d4','d5','d6'
|
1084
|
+
}
|
1085
|
+
|
1086
|
+
local mkstats = function(name, bin, queue)
|
1087
|
+
local results = {}
|
1088
|
+
|
1089
|
+
local key = 'ql:s:' .. name .. ':' .. bin .. ':' .. queue
|
1090
|
+
local count, mean, vk = unpack(redis.call('hmget', key, 'total', 'mean', 'vk'))
|
1091
|
+
|
1092
|
+
count = tonumber(count) or 0
|
1093
|
+
mean = tonumber(mean) or 0
|
1094
|
+
vk = tonumber(vk)
|
1095
|
+
|
1096
|
+
results.count = count or 0
|
1097
|
+
results.mean = mean or 0
|
1098
|
+
results.histogram = {}
|
1099
|
+
|
1100
|
+
if not count then
|
1101
|
+
results.std = 0
|
1102
|
+
else
|
1103
|
+
if count > 1 then
|
1104
|
+
results.std = math.sqrt(vk / (count - 1))
|
1105
|
+
else
|
1106
|
+
results.std = 0
|
1107
|
+
end
|
1108
|
+
end
|
1109
|
+
|
1110
|
+
local histogram = redis.call('hmget', key, unpack(histokeys))
|
1111
|
+
for i=1,#histokeys do
|
1112
|
+
table.insert(results.histogram, tonumber(histogram[i]) or 0)
|
1113
|
+
end
|
1114
|
+
return results
|
1115
|
+
end
|
1116
|
+
|
1117
|
+
local retries, failed, failures = unpack(redis.call('hmget', 'ql:s:stats:' .. bin .. ':' .. self.name, 'retries', 'failed', 'failures'))
|
1118
|
+
return {
|
1119
|
+
retries = tonumber(retries or 0),
|
1120
|
+
failed = tonumber(failed or 0),
|
1121
|
+
failures = tonumber(failures or 0),
|
1122
|
+
wait = mkstats('wait', bin, self.name),
|
1123
|
+
run = mkstats('run' , bin, self.name)
|
1124
|
+
}
|
1125
|
+
end
|
1126
|
+
|
1127
|
+
function QlessQueue:peek(now, count)
|
1128
|
+
count = assert(tonumber(count),
|
1129
|
+
'Peek(): Arg "count" missing or not a number: ' .. tostring(count))
|
1130
|
+
|
1131
|
+
local jids = self.locks.expired(now, 0, count)
|
1132
|
+
|
1133
|
+
self:check_recurring(now, count - #jids)
|
1134
|
+
|
1135
|
+
self:check_scheduled(now, count - #jids)
|
1136
|
+
|
1137
|
+
tbl_extend(jids, self.work.peek(count - #jids))
|
1138
|
+
|
1139
|
+
return jids
|
1140
|
+
end
|
1141
|
+
|
1142
|
+
function QlessQueue:paused()
|
1143
|
+
return redis.call('sismember', 'ql:paused_queues', self.name) == 1
|
1144
|
+
end
|
1145
|
+
|
1146
|
+
function QlessQueue.pause(now, ...)
|
1147
|
+
redis.call('sadd', 'ql:paused_queues', unpack(arg))
|
1148
|
+
end
|
1149
|
+
|
1150
|
+
function QlessQueue.unpause(...)
|
1151
|
+
redis.call('srem', 'ql:paused_queues', unpack(arg))
|
1152
|
+
end
|
1153
|
+
|
1154
|
+
function QlessQueue:pop(now, worker, count)
|
1155
|
+
assert(worker, 'Pop(): Arg "worker" missing')
|
1156
|
+
count = assert(tonumber(count),
|
1157
|
+
'Pop(): Arg "count" missing or not a number: ' .. tostring(count))
|
1158
|
+
|
1159
|
+
local expires = now + tonumber(
|
1160
|
+
Qless.config.get(self.name .. '-heartbeat') or
|
1161
|
+
Qless.config.get('heartbeat', 60))
|
1162
|
+
|
1163
|
+
if self:paused() then
|
1164
|
+
return {}
|
1165
|
+
end
|
1166
|
+
|
1167
|
+
redis.call('zadd', 'ql:workers', now, worker)
|
1168
|
+
|
1169
|
+
local max_concurrency = tonumber(
|
1170
|
+
Qless.config.get(self.name .. '-max-concurrency', 0))
|
1171
|
+
|
1172
|
+
if max_concurrency > 0 then
|
1173
|
+
local allowed = math.max(0, max_concurrency - self.locks.running(now))
|
1174
|
+
count = math.min(allowed, count)
|
1175
|
+
if count == 0 then
|
1176
|
+
return {}
|
1177
|
+
end
|
1178
|
+
end
|
1179
|
+
|
1180
|
+
local jids = self:invalidate_locks(now, count)
|
1181
|
+
|
1182
|
+
self:check_recurring(now, count - #jids)
|
1183
|
+
|
1184
|
+
self:check_scheduled(now, count - #jids)
|
1185
|
+
|
1186
|
+
tbl_extend(jids, self.work.peek(count - #jids))
|
1187
|
+
|
1188
|
+
local state
|
1189
|
+
for index, jid in ipairs(jids) do
|
1190
|
+
local job = Qless.job(jid)
|
1191
|
+
state = unpack(job:data('state'))
|
1192
|
+
job:history(now, 'popped', {worker = worker})
|
1193
|
+
|
1194
|
+
local time = tonumber(
|
1195
|
+
redis.call('hget', QlessJob.ns .. jid, 'time') or now)
|
1196
|
+
local waiting = now - time
|
1197
|
+
self:stat(now, 'wait', waiting)
|
1198
|
+
redis.call('hset', QlessJob.ns .. jid,
|
1199
|
+
'time', string.format("%.20f", now))
|
1200
|
+
|
1201
|
+
redis.call('zadd', 'ql:w:' .. worker .. ':jobs', expires, jid)
|
1202
|
+
|
1203
|
+
job:update({
|
1204
|
+
worker = worker,
|
1205
|
+
expires = expires,
|
1206
|
+
state = 'running'
|
1207
|
+
})
|
1208
|
+
|
1209
|
+
self.locks.add(expires, jid)
|
1210
|
+
|
1211
|
+
local tracked = redis.call('zscore', 'ql:tracked', jid) ~= false
|
1212
|
+
if tracked then
|
1213
|
+
Qless.publish('popped', jid)
|
1214
|
+
end
|
1215
|
+
end
|
1216
|
+
|
1217
|
+
self.work.remove(unpack(jids))
|
1218
|
+
|
1219
|
+
return jids
|
1220
|
+
end
|
1221
|
+
|
1222
|
+
function QlessQueue:stat(now, stat, val)
|
1223
|
+
local bin = now - (now % 86400)
|
1224
|
+
local key = 'ql:s:' .. stat .. ':' .. bin .. ':' .. self.name
|
1225
|
+
|
1226
|
+
local count, mean, vk = unpack(
|
1227
|
+
redis.call('hmget', key, 'total', 'mean', 'vk'))
|
1228
|
+
|
1229
|
+
count = count or 0
|
1230
|
+
if count == 0 then
|
1231
|
+
mean = val
|
1232
|
+
vk = 0
|
1233
|
+
count = 1
|
1234
|
+
else
|
1235
|
+
count = count + 1
|
1236
|
+
local oldmean = mean
|
1237
|
+
mean = mean + (val - mean) / count
|
1238
|
+
vk = vk + (val - mean) * (val - oldmean)
|
1239
|
+
end
|
1240
|
+
|
1241
|
+
val = math.floor(val)
|
1242
|
+
if val < 60 then -- seconds
|
1243
|
+
redis.call('hincrby', key, 's' .. val, 1)
|
1244
|
+
elseif val < 3600 then -- minutes
|
1245
|
+
redis.call('hincrby', key, 'm' .. math.floor(val / 60), 1)
|
1246
|
+
elseif val < 86400 then -- hours
|
1247
|
+
redis.call('hincrby', key, 'h' .. math.floor(val / 3600), 1)
|
1248
|
+
else -- days
|
1249
|
+
redis.call('hincrby', key, 'd' .. math.floor(val / 86400), 1)
|
1250
|
+
end
|
1251
|
+
redis.call('hmset', key, 'total', count, 'mean', mean, 'vk', vk)
|
1252
|
+
end
|
1253
|
+
|
1254
|
+
function QlessQueue:put(now, worker, jid, klass, raw_data, delay, ...)
|
1255
|
+
assert(jid , 'Put(): Arg "jid" missing')
|
1256
|
+
assert(klass, 'Put(): Arg "klass" missing')
|
1257
|
+
local data = assert(cjson.decode(raw_data),
|
1258
|
+
'Put(): Arg "data" missing or not JSON: ' .. tostring(raw_data))
|
1259
|
+
delay = assert(tonumber(delay),
|
1260
|
+
'Put(): Arg "delay" not a number: ' .. tostring(delay))
|
1261
|
+
|
1262
|
+
if #arg % 2 == 1 then
|
1263
|
+
error('Odd number of additional args: ' .. tostring(arg))
|
1264
|
+
end
|
1265
|
+
local options = {}
|
1266
|
+
for i = 1, #arg, 2 do options[arg[i]] = arg[i + 1] end
|
1267
|
+
|
1268
|
+
local job = Qless.job(jid)
|
1269
|
+
local priority, tags, oldqueue, state, failure, retries, oldworker =
|
1270
|
+
unpack(redis.call('hmget', QlessJob.ns .. jid, 'priority', 'tags',
|
1271
|
+
'queue', 'state', 'failure', 'retries', 'worker'))
|
1272
|
+
|
1273
|
+
if tags then
|
1274
|
+
Qless.tag(now, 'remove', jid, unpack(cjson.decode(tags)))
|
1275
|
+
end
|
1276
|
+
|
1277
|
+
retries = assert(tonumber(options['retries'] or retries or 5) ,
|
1278
|
+
'Put(): Arg "retries" not a number: ' .. tostring(options['retries']))
|
1279
|
+
tags = assert(cjson.decode(options['tags'] or tags or '[]' ),
|
1280
|
+
'Put(): Arg "tags" not JSON' .. tostring(options['tags']))
|
1281
|
+
priority = assert(tonumber(options['priority'] or priority or 0),
|
1282
|
+
'Put(): Arg "priority" not a number' .. tostring(options['priority']))
|
1283
|
+
local depends = assert(cjson.decode(options['depends'] or '[]') ,
|
1284
|
+
'Put(): Arg "depends" not JSON: ' .. tostring(options['depends']))
|
1285
|
+
|
1286
|
+
if #depends > 0 then
|
1287
|
+
local new = {}
|
1288
|
+
for _, d in ipairs(depends) do new[d] = 1 end
|
1289
|
+
|
1290
|
+
local original = redis.call(
|
1291
|
+
'smembers', QlessJob.ns .. jid .. '-dependencies')
|
1292
|
+
for _, dep in pairs(original) do
|
1293
|
+
if new[dep] == nil or new[dep] == false then
|
1294
|
+
redis.call('srem', QlessJob.ns .. dep .. '-dependents' , jid)
|
1295
|
+
redis.call('srem', QlessJob.ns .. jid .. '-dependencies', dep)
|
1296
|
+
end
|
1297
|
+
end
|
1298
|
+
end
|
1299
|
+
|
1300
|
+
Qless.publish('log', cjson.encode({
|
1301
|
+
jid = jid,
|
1302
|
+
event = 'put',
|
1303
|
+
queue = self.name
|
1304
|
+
}))
|
1305
|
+
|
1306
|
+
job:history(now, 'put', {q = self.name})
|
1307
|
+
|
1308
|
+
if oldqueue then
|
1309
|
+
local queue_obj = Qless.queue(oldqueue)
|
1310
|
+
queue_obj.work.remove(jid)
|
1311
|
+
queue_obj.locks.remove(jid)
|
1312
|
+
queue_obj.depends.remove(jid)
|
1313
|
+
queue_obj.scheduled.remove(jid)
|
1314
|
+
end
|
1315
|
+
|
1316
|
+
if oldworker and oldworker ~= '' then
|
1317
|
+
redis.call('zrem', 'ql:w:' .. oldworker .. ':jobs', jid)
|
1318
|
+
if oldworker ~= worker then
|
1319
|
+
local encoded = cjson.encode({
|
1320
|
+
jid = jid,
|
1321
|
+
event = 'lock_lost',
|
1322
|
+
worker = oldworker
|
1323
|
+
})
|
1324
|
+
Qless.publish('w:' .. oldworker, encoded)
|
1325
|
+
Qless.publish('log', encoded)
|
1326
|
+
end
|
1327
|
+
end
|
1328
|
+
|
1329
|
+
if state == 'complete' then
|
1330
|
+
redis.call('zrem', 'ql:completed', jid)
|
1331
|
+
end
|
1332
|
+
|
1333
|
+
for i, tag in ipairs(tags) do
|
1334
|
+
redis.call('zadd', 'ql:t:' .. tag, now, jid)
|
1335
|
+
redis.call('zincrby', 'ql:tags', 1, tag)
|
1336
|
+
end
|
1337
|
+
|
1338
|
+
if state == 'failed' then
|
1339
|
+
failure = cjson.decode(failure)
|
1340
|
+
redis.call('lrem', 'ql:f:' .. failure.group, 0, jid)
|
1341
|
+
if redis.call('llen', 'ql:f:' .. failure.group) == 0 then
|
1342
|
+
redis.call('srem', 'ql:failures', failure.group)
|
1343
|
+
end
|
1344
|
+
local bin = failure.when - (failure.when % 86400)
|
1345
|
+
redis.call('hincrby', 'ql:s:stats:' .. bin .. ':' .. self.name, 'failed' , -1)
|
1346
|
+
end
|
1347
|
+
|
1348
|
+
redis.call('hmset', QlessJob.ns .. jid,
|
1349
|
+
'jid' , jid,
|
1350
|
+
'klass' , klass,
|
1351
|
+
'data' , raw_data,
|
1352
|
+
'priority' , priority,
|
1353
|
+
'tags' , cjson.encode(tags),
|
1354
|
+
'state' , ((delay > 0) and 'scheduled') or 'waiting',
|
1355
|
+
'worker' , '',
|
1356
|
+
'expires' , 0,
|
1357
|
+
'queue' , self.name,
|
1358
|
+
'retries' , retries,
|
1359
|
+
'remaining', retries,
|
1360
|
+
'time' , string.format("%.20f", now))
|
1361
|
+
|
1362
|
+
for i, j in ipairs(depends) do
|
1363
|
+
local state = redis.call('hget', QlessJob.ns .. j, 'state')
|
1364
|
+
if (state and state ~= 'complete') then
|
1365
|
+
redis.call('sadd', QlessJob.ns .. j .. '-dependents' , jid)
|
1366
|
+
redis.call('sadd', QlessJob.ns .. jid .. '-dependencies', j)
|
1367
|
+
end
|
1368
|
+
end
|
1369
|
+
|
1370
|
+
if delay > 0 then
|
1371
|
+
if redis.call('scard', QlessJob.ns .. jid .. '-dependencies') > 0 then
|
1372
|
+
self.depends.add(now, jid)
|
1373
|
+
redis.call('hmset', QlessJob.ns .. jid,
|
1374
|
+
'state', 'depends',
|
1375
|
+
'scheduled', now + delay)
|
1376
|
+
else
|
1377
|
+
self.scheduled.add(now + delay, jid)
|
1378
|
+
end
|
1379
|
+
else
|
1380
|
+
if redis.call('scard', QlessJob.ns .. jid .. '-dependencies') > 0 then
|
1381
|
+
self.depends.add(now, jid)
|
1382
|
+
redis.call('hset', QlessJob.ns .. jid, 'state', 'depends')
|
1383
|
+
else
|
1384
|
+
self.work.add(now, priority, jid)
|
1385
|
+
end
|
1386
|
+
end
|
1387
|
+
|
1388
|
+
if redis.call('zscore', 'ql:queues', self.name) == false then
|
1389
|
+
redis.call('zadd', 'ql:queues', now, self.name)
|
1390
|
+
end
|
1391
|
+
|
1392
|
+
if redis.call('zscore', 'ql:tracked', jid) ~= false then
|
1393
|
+
Qless.publish('put', jid)
|
1394
|
+
end
|
1395
|
+
|
1396
|
+
return jid
|
1397
|
+
end
|
1398
|
+
|
1399
|
+
function QlessQueue:unfail(now, group, count)
|
1400
|
+
assert(group, 'Unfail(): Arg "group" missing')
|
1401
|
+
count = assert(tonumber(count or 25),
|
1402
|
+
'Unfail(): Arg "count" not a number: ' .. tostring(count))
|
1403
|
+
|
1404
|
+
local jids = redis.call('lrange', 'ql:f:' .. group, -count, -1)
|
1405
|
+
|
1406
|
+
local toinsert = {}
|
1407
|
+
for index, jid in ipairs(jids) do
|
1408
|
+
local job = Qless.job(jid)
|
1409
|
+
local data = job:data()
|
1410
|
+
job:history(now, 'put', {q = self.name})
|
1411
|
+
redis.call('hmset', QlessJob.ns .. data.jid,
|
1412
|
+
'state' , 'waiting',
|
1413
|
+
'worker' , '',
|
1414
|
+
'expires' , 0,
|
1415
|
+
'queue' , self.name,
|
1416
|
+
'remaining', data.retries or 5)
|
1417
|
+
self.work.add(now, data.priority, data.jid)
|
1418
|
+
end
|
1419
|
+
|
1420
|
+
redis.call('ltrim', 'ql:f:' .. group, 0, -count - 1)
|
1421
|
+
if (redis.call('llen', 'ql:f:' .. group) == 0) then
|
1422
|
+
redis.call('srem', 'ql:failures', group)
|
1423
|
+
end
|
1424
|
+
|
1425
|
+
return #jids
|
1426
|
+
end
|
1427
|
+
|
1428
|
+
function QlessQueue:recur(now, jid, klass, raw_data, spec, ...)
|
1429
|
+
assert(jid , 'RecurringJob On(): Arg "jid" missing')
|
1430
|
+
assert(klass, 'RecurringJob On(): Arg "klass" missing')
|
1431
|
+
assert(spec , 'RecurringJob On(): Arg "spec" missing')
|
1432
|
+
local data = assert(cjson.decode(raw_data),
|
1433
|
+
'RecurringJob On(): Arg "data" not JSON: ' .. tostring(raw_data))
|
1434
|
+
|
1435
|
+
if spec == 'interval' then
|
1436
|
+
local interval = assert(tonumber(arg[1]),
|
1437
|
+
'Recur(): Arg "interval" not a number: ' .. tostring(arg[1]))
|
1438
|
+
local offset = assert(tonumber(arg[2]),
|
1439
|
+
'Recur(): Arg "offset" not a number: ' .. tostring(arg[2]))
|
1440
|
+
if interval <= 0 then
|
1441
|
+
error('Recur(): Arg "interval" must be greater than 0')
|
1442
|
+
end
|
1443
|
+
|
1444
|
+
if #arg % 2 == 1 then
|
1445
|
+
error('Odd number of additional args: ' .. tostring(arg))
|
1446
|
+
end
|
1447
|
+
|
1448
|
+
local options = {}
|
1449
|
+
for i = 3, #arg, 2 do options[arg[i]] = arg[i + 1] end
|
1450
|
+
options.tags = assert(cjson.decode(options.tags or '{}'),
|
1451
|
+
'Recur(): Arg "tags" must be JSON string array: ' .. tostring(
|
1452
|
+
options.tags))
|
1453
|
+
options.priority = assert(tonumber(options.priority or 0),
|
1454
|
+
'Recur(): Arg "priority" not a number: ' .. tostring(
|
1455
|
+
options.priority))
|
1456
|
+
options.retries = assert(tonumber(options.retries or 0),
|
1457
|
+
'Recur(): Arg "retries" not a number: ' .. tostring(
|
1458
|
+
options.retries))
|
1459
|
+
options.backlog = assert(tonumber(options.backlog or 0),
|
1460
|
+
'Recur(): Arg "backlog" not a number: ' .. tostring(
|
1461
|
+
options.backlog))
|
1462
|
+
|
1463
|
+
local count, old_queue = unpack(redis.call('hmget', 'ql:r:' .. jid, 'count', 'queue'))
|
1464
|
+
count = count or 0
|
1465
|
+
|
1466
|
+
if old_queue then
|
1467
|
+
Qless.queue(old_queue).recurring.remove(jid)
|
1468
|
+
end
|
1469
|
+
|
1470
|
+
redis.call('hmset', 'ql:r:' .. jid,
|
1471
|
+
'jid' , jid,
|
1472
|
+
'klass' , klass,
|
1473
|
+
'data' , raw_data,
|
1474
|
+
'priority', options.priority,
|
1475
|
+
'tags' , cjson.encode(options.tags or {}),
|
1476
|
+
'state' , 'recur',
|
1477
|
+
'queue' , self.name,
|
1478
|
+
'type' , 'interval',
|
1479
|
+
'count' , count,
|
1480
|
+
'interval', interval,
|
1481
|
+
'retries' , options.retries,
|
1482
|
+
'backlog' , options.backlog)
|
1483
|
+
self.recurring.add(now + offset, jid)
|
1484
|
+
|
1485
|
+
if redis.call('zscore', 'ql:queues', self.name) == false then
|
1486
|
+
redis.call('zadd', 'ql:queues', now, self.name)
|
1487
|
+
end
|
1488
|
+
|
1489
|
+
return jid
|
1490
|
+
else
|
1491
|
+
error('Recur(): schedule type "' .. tostring(spec) .. '" unknown')
|
1492
|
+
end
|
1493
|
+
end
|
1494
|
+
|
1495
|
+
function QlessQueue:length()
|
1496
|
+
return self.locks.length() + self.work.length() + self.scheduled.length()
|
1497
|
+
end
|
1498
|
+
|
1499
|
+
function QlessQueue:check_recurring(now, count)
|
1500
|
+
local moved = 0
|
1501
|
+
local r = self.recurring.peek(now, 0, count)
|
1502
|
+
for index, jid in ipairs(r) do
|
1503
|
+
local klass, data, priority, tags, retries, interval, backlog = unpack(
|
1504
|
+
redis.call('hmget', 'ql:r:' .. jid, 'klass', 'data', 'priority',
|
1505
|
+
'tags', 'retries', 'interval', 'backlog'))
|
1506
|
+
local _tags = cjson.decode(tags)
|
1507
|
+
local score = math.floor(tonumber(self.recurring.score(jid)))
|
1508
|
+
interval = tonumber(interval)
|
1509
|
+
|
1510
|
+
backlog = tonumber(backlog or 0)
|
1511
|
+
if backlog ~= 0 then
|
1512
|
+
local num = ((now - score) / interval)
|
1513
|
+
if num > backlog then
|
1514
|
+
score = score + (
|
1515
|
+
math.ceil(num - backlog) * interval
|
1516
|
+
)
|
1517
|
+
end
|
1518
|
+
end
|
1519
|
+
|
1520
|
+
while (score <= now) and (moved < count) do
|
1521
|
+
local count = redis.call('hincrby', 'ql:r:' .. jid, 'count', 1)
|
1522
|
+
moved = moved + 1
|
1523
|
+
|
1524
|
+
local child_jid = jid .. '-' .. count
|
1525
|
+
|
1526
|
+
for i, tag in ipairs(_tags) do
|
1527
|
+
redis.call('zadd', 'ql:t:' .. tag, now, child_jid)
|
1528
|
+
redis.call('zincrby', 'ql:tags', 1, tag)
|
1529
|
+
end
|
1530
|
+
|
1531
|
+
redis.call('hmset', QlessJob.ns .. child_jid,
|
1532
|
+
'jid' , child_jid,
|
1533
|
+
'klass' , klass,
|
1534
|
+
'data' , data,
|
1535
|
+
'priority' , priority,
|
1536
|
+
'tags' , tags,
|
1537
|
+
'state' , 'waiting',
|
1538
|
+
'worker' , '',
|
1539
|
+
'expires' , 0,
|
1540
|
+
'queue' , self.name,
|
1541
|
+
'retries' , retries,
|
1542
|
+
'remaining' , retries,
|
1543
|
+
'time' , string.format("%.20f", score),
|
1544
|
+
'spawned_from_jid', jid)
|
1545
|
+
Qless.job(child_jid):history(score, 'put', {q = self.name})
|
1546
|
+
|
1547
|
+
self.work.add(score, priority, child_jid)
|
1548
|
+
|
1549
|
+
score = score + interval
|
1550
|
+
self.recurring.add(score, jid)
|
1551
|
+
end
|
1552
|
+
end
|
1553
|
+
end
|
1554
|
+
|
1555
|
+
function QlessQueue:check_scheduled(now, count)
|
1556
|
+
local scheduled = self.scheduled.ready(now, 0, count)
|
1557
|
+
for index, jid in ipairs(scheduled) do
|
1558
|
+
local priority = tonumber(
|
1559
|
+
redis.call('hget', QlessJob.ns .. jid, 'priority') or 0)
|
1560
|
+
self.work.add(now, priority, jid)
|
1561
|
+
self.scheduled.remove(jid)
|
1562
|
+
|
1563
|
+
redis.call('hset', QlessJob.ns .. jid, 'state', 'waiting')
|
1564
|
+
end
|
1565
|
+
end
|
1566
|
+
|
1567
|
+
function QlessQueue:invalidate_locks(now, count)
|
1568
|
+
local jids = {}
|
1569
|
+
for index, jid in ipairs(self.locks.expired(now, 0, count)) do
|
1570
|
+
local worker, failure = unpack(
|
1571
|
+
redis.call('hmget', QlessJob.ns .. jid, 'worker', 'failure'))
|
1572
|
+
redis.call('zrem', 'ql:w:' .. worker .. ':jobs', jid)
|
1573
|
+
|
1574
|
+
local grace_period = tonumber(Qless.config.get('grace-period'))
|
1575
|
+
|
1576
|
+
local courtesy_sent = tonumber(
|
1577
|
+
redis.call('hget', QlessJob.ns .. jid, 'grace') or 0)
|
1578
|
+
|
1579
|
+
local send_message = (courtesy_sent ~= 1)
|
1580
|
+
local invalidate = not send_message
|
1581
|
+
|
1582
|
+
if grace_period <= 0 then
|
1583
|
+
send_message = true
|
1584
|
+
invalidate = true
|
1585
|
+
end
|
1586
|
+
|
1587
|
+
if send_message then
|
1588
|
+
if redis.call('zscore', 'ql:tracked', jid) ~= false then
|
1589
|
+
Qless.publish('stalled', jid)
|
1590
|
+
end
|
1591
|
+
Qless.job(jid):history(now, 'timed-out')
|
1592
|
+
redis.call('hset', QlessJob.ns .. jid, 'grace', 1)
|
1593
|
+
|
1594
|
+
local encoded = cjson.encode({
|
1595
|
+
jid = jid,
|
1596
|
+
event = 'lock_lost',
|
1597
|
+
worker = worker
|
1598
|
+
})
|
1599
|
+
Qless.publish('w:' .. worker, encoded)
|
1600
|
+
Qless.publish('log', encoded)
|
1601
|
+
self.locks.add(now + grace_period, jid)
|
1602
|
+
|
1603
|
+
local bin = now - (now % 86400)
|
1604
|
+
redis.call('hincrby',
|
1605
|
+
'ql:s:stats:' .. bin .. ':' .. self.name, 'retries', 1)
|
1606
|
+
end
|
1607
|
+
|
1608
|
+
if invalidate then
|
1609
|
+
redis.call('hdel', QlessJob.ns .. jid, 'grace', 0)
|
1610
|
+
|
1611
|
+
local remaining = tonumber(redis.call(
|
1612
|
+
'hincrby', QlessJob.ns .. jid, 'remaining', -1))
|
1613
|
+
|
1614
|
+
if remaining < 0 then
|
1615
|
+
self.work.remove(jid)
|
1616
|
+
self.locks.remove(jid)
|
1617
|
+
self.scheduled.remove(jid)
|
1618
|
+
|
1619
|
+
local group = 'failed-retries-' .. Qless.job(jid):data()['queue']
|
1620
|
+
local job = Qless.job(jid)
|
1621
|
+
job:history(now, 'failed', {group = group})
|
1622
|
+
redis.call('hmset', QlessJob.ns .. jid, 'state', 'failed',
|
1623
|
+
'worker', '',
|
1624
|
+
'expires', '')
|
1625
|
+
redis.call('hset', QlessJob.ns .. jid,
|
1626
|
+
'failure', cjson.encode({
|
1627
|
+
['group'] = group,
|
1628
|
+
['message'] =
|
1629
|
+
'Job exhausted retries in queue "' .. self.name .. '"',
|
1630
|
+
['when'] = now,
|
1631
|
+
['worker'] = unpack(job:data('worker'))
|
1632
|
+
}))
|
1633
|
+
|
1634
|
+
redis.call('sadd', 'ql:failures', group)
|
1635
|
+
redis.call('lpush', 'ql:f:' .. group, jid)
|
1636
|
+
|
1637
|
+
if redis.call('zscore', 'ql:tracked', jid) ~= false then
|
1638
|
+
Qless.publish('failed', jid)
|
1639
|
+
end
|
1640
|
+
Qless.publish('log', cjson.encode({
|
1641
|
+
jid = jid,
|
1642
|
+
event = 'failed',
|
1643
|
+
group = group,
|
1644
|
+
worker = worker,
|
1645
|
+
message =
|
1646
|
+
'Job exhausted retries in queue "' .. self.name .. '"'
|
1647
|
+
}))
|
1648
|
+
|
1649
|
+
local bin = now - (now % 86400)
|
1650
|
+
redis.call('hincrby',
|
1651
|
+
'ql:s:stats:' .. bin .. ':' .. self.name, 'failures', 1)
|
1652
|
+
redis.call('hincrby',
|
1653
|
+
'ql:s:stats:' .. bin .. ':' .. self.name, 'failed' , 1)
|
1654
|
+
else
|
1655
|
+
table.insert(jids, jid)
|
1656
|
+
end
|
1657
|
+
end
|
1658
|
+
end
|
1659
|
+
|
1660
|
+
return jids
|
1661
|
+
end
|
1662
|
+
|
1663
|
+
function QlessQueue.deregister(...)
|
1664
|
+
redis.call('zrem', Qless.ns .. 'queues', unpack(arg))
|
1665
|
+
end
|
1666
|
+
|
1667
|
+
function QlessQueue.counts(now, name)
|
1668
|
+
if name then
|
1669
|
+
local queue = Qless.queue(name)
|
1670
|
+
local stalled = queue.locks.length(now)
|
1671
|
+
queue:check_scheduled(now, queue.scheduled.length())
|
1672
|
+
return {
|
1673
|
+
name = name,
|
1674
|
+
waiting = queue.work.length(),
|
1675
|
+
stalled = stalled,
|
1676
|
+
running = queue.locks.length() - stalled,
|
1677
|
+
scheduled = queue.scheduled.length(),
|
1678
|
+
depends = queue.depends.length(),
|
1679
|
+
recurring = queue.recurring.length(),
|
1680
|
+
paused = queue:paused()
|
1681
|
+
}
|
1682
|
+
else
|
1683
|
+
local queues = redis.call('zrange', 'ql:queues', 0, -1)
|
1684
|
+
local response = {}
|
1685
|
+
for index, qname in ipairs(queues) do
|
1686
|
+
table.insert(response, QlessQueue.counts(now, qname))
|
1687
|
+
end
|
1688
|
+
return response
|
1689
|
+
end
|
1690
|
+
end
|
1691
|
+
function QlessRecurringJob:data()
|
1692
|
+
local job = redis.call(
|
1693
|
+
'hmget', 'ql:r:' .. self.jid, 'jid', 'klass', 'state', 'queue',
|
1694
|
+
'priority', 'interval', 'retries', 'count', 'data', 'tags', 'backlog')
|
1695
|
+
|
1696
|
+
if not job[1] then
|
1697
|
+
return nil
|
1698
|
+
end
|
1699
|
+
|
1700
|
+
return {
|
1701
|
+
jid = job[1],
|
1702
|
+
klass = job[2],
|
1703
|
+
state = job[3],
|
1704
|
+
queue = job[4],
|
1705
|
+
priority = tonumber(job[5]),
|
1706
|
+
interval = tonumber(job[6]),
|
1707
|
+
retries = tonumber(job[7]),
|
1708
|
+
count = tonumber(job[8]),
|
1709
|
+
data = job[9],
|
1710
|
+
tags = cjson.decode(job[10]),
|
1711
|
+
backlog = tonumber(job[11] or 0)
|
1712
|
+
}
|
1713
|
+
end
|
1714
|
+
|
1715
|
+
function QlessRecurringJob:update(now, ...)
|
1716
|
+
local options = {}
|
1717
|
+
if redis.call('exists', 'ql:r:' .. self.jid) ~= 0 then
|
1718
|
+
for i = 1, #arg, 2 do
|
1719
|
+
local key = arg[i]
|
1720
|
+
local value = arg[i+1]
|
1721
|
+
assert(value, 'No value provided for ' .. tostring(key))
|
1722
|
+
if key == 'priority' or key == 'interval' or key == 'retries' then
|
1723
|
+
value = assert(tonumber(value), 'Recur(): Arg "' .. key .. '" must be a number: ' .. tostring(value))
|
1724
|
+
if key == 'interval' then
|
1725
|
+
local queue, interval = unpack(redis.call('hmget', 'ql:r:' .. self.jid, 'queue', 'interval'))
|
1726
|
+
Qless.queue(queue).recurring.update(
|
1727
|
+
value - tonumber(interval), self.jid)
|
1728
|
+
end
|
1729
|
+
redis.call('hset', 'ql:r:' .. self.jid, key, value)
|
1730
|
+
elseif key == 'data' then
|
1731
|
+
assert(cjson.decode(value), 'Recur(): Arg "data" is not JSON-encoded: ' .. tostring(value))
|
1732
|
+
redis.call('hset', 'ql:r:' .. self.jid, 'data', value)
|
1733
|
+
elseif key == 'klass' then
|
1734
|
+
redis.call('hset', 'ql:r:' .. self.jid, 'klass', value)
|
1735
|
+
elseif key == 'queue' then
|
1736
|
+
local queue_obj = Qless.queue(
|
1737
|
+
redis.call('hget', 'ql:r:' .. self.jid, 'queue'))
|
1738
|
+
local score = queue_obj.recurring.score(self.jid)
|
1739
|
+
queue_obj.recurring.remove(self.jid)
|
1740
|
+
Qless.queue(value).recurring.add(score, self.jid)
|
1741
|
+
redis.call('hset', 'ql:r:' .. self.jid, 'queue', value)
|
1742
|
+
if redis.call('zscore', 'ql:queues', value) == false then
|
1743
|
+
redis.call('zadd', 'ql:queues', now, value)
|
1744
|
+
end
|
1745
|
+
elseif key == 'backlog' then
|
1746
|
+
value = assert(tonumber(value),
|
1747
|
+
'Recur(): Arg "backlog" not a number: ' .. tostring(value))
|
1748
|
+
redis.call('hset', 'ql:r:' .. self.jid, 'backlog', value)
|
1749
|
+
else
|
1750
|
+
error('Recur(): Unrecognized option "' .. key .. '"')
|
1751
|
+
end
|
1752
|
+
end
|
1753
|
+
return true
|
1754
|
+
else
|
1755
|
+
error('Recur(): No recurring job ' .. self.jid)
|
1756
|
+
end
|
1757
|
+
end
|
1758
|
+
|
1759
|
+
function QlessRecurringJob:tag(...)
|
1760
|
+
local tags = redis.call('hget', 'ql:r:' .. self.jid, 'tags')
|
1761
|
+
if tags then
|
1762
|
+
tags = cjson.decode(tags)
|
1763
|
+
local _tags = {}
|
1764
|
+
for i,v in ipairs(tags) do _tags[v] = true end
|
1765
|
+
|
1766
|
+
for i=1,#arg do if _tags[arg[i]] == nil or _tags[arg[i]] == false then table.insert(tags, arg[i]) end end
|
1767
|
+
|
1768
|
+
tags = cjson.encode(tags)
|
1769
|
+
redis.call('hset', 'ql:r:' .. self.jid, 'tags', tags)
|
1770
|
+
return tags
|
1771
|
+
else
|
1772
|
+
error('Tag(): Job ' .. self.jid .. ' does not exist')
|
1773
|
+
end
|
1774
|
+
end
|
1775
|
+
|
1776
|
+
function QlessRecurringJob:untag(...)
|
1777
|
+
local tags = redis.call('hget', 'ql:r:' .. self.jid, 'tags')
|
1778
|
+
if tags then
|
1779
|
+
tags = cjson.decode(tags)
|
1780
|
+
local _tags = {}
|
1781
|
+
for i,v in ipairs(tags) do _tags[v] = true end
|
1782
|
+
for i = 1,#arg do _tags[arg[i]] = nil end
|
1783
|
+
local results = {}
|
1784
|
+
for i, tag in ipairs(tags) do if _tags[tag] then table.insert(results, tag) end end
|
1785
|
+
tags = cjson.encode(results)
|
1786
|
+
redis.call('hset', 'ql:r:' .. self.jid, 'tags', tags)
|
1787
|
+
return tags
|
1788
|
+
else
|
1789
|
+
error('Untag(): Job ' .. self.jid .. ' does not exist')
|
1790
|
+
end
|
1791
|
+
end
|
1792
|
+
|
1793
|
+
function QlessRecurringJob:unrecur()
|
1794
|
+
local queue = redis.call('hget', 'ql:r:' .. self.jid, 'queue')
|
1795
|
+
if queue then
|
1796
|
+
Qless.queue(queue).recurring.remove(self.jid)
|
1797
|
+
redis.call('del', 'ql:r:' .. self.jid)
|
1798
|
+
return true
|
1799
|
+
else
|
1800
|
+
return true
|
1801
|
+
end
|
1802
|
+
end
|
1803
|
+
function QlessWorker.deregister(...)
|
1804
|
+
redis.call('zrem', 'ql:workers', unpack(arg))
|
1805
|
+
end
|
1806
|
+
|
1807
|
+
function QlessWorker.counts(now, worker)
|
1808
|
+
local interval = tonumber(Qless.config.get('max-worker-age', 86400))
|
1809
|
+
|
1810
|
+
local workers = redis.call('zrangebyscore', 'ql:workers', 0, now - interval)
|
1811
|
+
for index, worker in ipairs(workers) do
|
1812
|
+
redis.call('del', 'ql:w:' .. worker .. ':jobs')
|
1813
|
+
end
|
1814
|
+
|
1815
|
+
redis.call('zremrangebyscore', 'ql:workers', 0, now - interval)
|
1816
|
+
|
1817
|
+
if worker then
|
1818
|
+
return {
|
1819
|
+
jobs = redis.call('zrevrangebyscore', 'ql:w:' .. worker .. ':jobs', now + 8640000, now),
|
1820
|
+
stalled = redis.call('zrevrangebyscore', 'ql:w:' .. worker .. ':jobs', now, 0)
|
1821
|
+
}
|
1822
|
+
else
|
1823
|
+
local response = {}
|
1824
|
+
local workers = redis.call('zrevrange', 'ql:workers', 0, -1)
|
1825
|
+
for index, worker in ipairs(workers) do
|
1826
|
+
table.insert(response, {
|
1827
|
+
name = worker,
|
1828
|
+
jobs = redis.call('zcount', 'ql:w:' .. worker .. ':jobs', now, now + 8640000),
|
1829
|
+
stalled = redis.call('zcount', 'ql:w:' .. worker .. ':jobs', 0, now)
|
1830
|
+
})
|
1831
|
+
end
|
1832
|
+
return response
|
1833
|
+
end
|
1834
|
+
end
|
1835
|
+
local QlessAPI = {}
|
1836
|
+
|
1837
|
+
function QlessAPI.get(now, jid)
|
1838
|
+
local data = Qless.job(jid):data()
|
1839
|
+
if not data then
|
1840
|
+
return nil
|
1841
|
+
end
|
1842
|
+
return cjson.encode(data)
|
1843
|
+
end
|
1844
|
+
|
1845
|
+
function QlessAPI.multiget(now, ...)
|
1846
|
+
local results = {}
|
1847
|
+
for i, jid in ipairs(arg) do
|
1848
|
+
table.insert(results, Qless.job(jid):data())
|
1849
|
+
end
|
1850
|
+
return cjson.encode(results)
|
1851
|
+
end
|
1852
|
+
|
1853
|
+
QlessAPI['config.get'] = function(now, key)
|
1854
|
+
if not key then
|
1855
|
+
return cjson.encode(Qless.config.get(key))
|
1856
|
+
else
|
1857
|
+
return Qless.config.get(key)
|
1858
|
+
end
|
1859
|
+
end
|
1860
|
+
|
1861
|
+
QlessAPI['config.set'] = function(now, key, value)
|
1862
|
+
return Qless.config.set(key, value)
|
1863
|
+
end
|
1864
|
+
|
1865
|
+
QlessAPI['config.unset'] = function(now, key)
|
1866
|
+
return Qless.config.unset(key)
|
1867
|
+
end
|
1868
|
+
|
1869
|
+
QlessAPI.queues = function(now, queue)
|
1870
|
+
return cjson.encode(QlessQueue.counts(now, queue))
|
1871
|
+
end
|
1872
|
+
|
1873
|
+
QlessAPI.complete = function(now, jid, worker, queue, data, ...)
|
1874
|
+
return Qless.job(jid):complete(now, worker, queue, data, unpack(arg))
|
1875
|
+
end
|
1876
|
+
|
1877
|
+
QlessAPI.failed = function(now, group, start, limit)
|
1878
|
+
return cjson.encode(Qless.failed(group, start, limit))
|
1879
|
+
end
|
1880
|
+
|
1881
|
+
QlessAPI.fail = function(now, jid, worker, group, message, data)
|
1882
|
+
return Qless.job(jid):fail(now, worker, group, message, data)
|
1883
|
+
end
|
1884
|
+
|
1885
|
+
QlessAPI.jobs = function(now, state, ...)
|
1886
|
+
return Qless.jobs(now, state, unpack(arg))
|
1887
|
+
end
|
1888
|
+
|
1889
|
+
QlessAPI.retry = function(now, jid, queue, worker, delay, group, message)
|
1890
|
+
return Qless.job(jid):retry(now, queue, worker, delay, group, message)
|
1891
|
+
end
|
1892
|
+
|
1893
|
+
QlessAPI.depends = function(now, jid, command, ...)
|
1894
|
+
return Qless.job(jid):depends(now, command, unpack(arg))
|
1895
|
+
end
|
1896
|
+
|
1897
|
+
QlessAPI.heartbeat = function(now, jid, worker, data)
|
1898
|
+
return Qless.job(jid):heartbeat(now, worker, data)
|
1899
|
+
end
|
1900
|
+
|
1901
|
+
QlessAPI.workers = function(now, worker)
|
1902
|
+
return cjson.encode(QlessWorker.counts(now, worker))
|
1903
|
+
end
|
1904
|
+
|
1905
|
+
QlessAPI.track = function(now, command, jid)
|
1906
|
+
return cjson.encode(Qless.track(now, command, jid))
|
1907
|
+
end
|
1908
|
+
|
1909
|
+
QlessAPI.tag = function(now, command, ...)
|
1910
|
+
return cjson.encode(Qless.tag(now, command, unpack(arg)))
|
1911
|
+
end
|
1912
|
+
|
1913
|
+
QlessAPI.stats = function(now, queue, date)
|
1914
|
+
return cjson.encode(Qless.queue(queue):stats(now, date))
|
1915
|
+
end
|
1916
|
+
|
1917
|
+
QlessAPI.priority = function(now, jid, priority)
|
1918
|
+
return Qless.job(jid):priority(priority)
|
1919
|
+
end
|
1920
|
+
|
1921
|
+
QlessAPI.log = function(now, jid, message, data)
|
1922
|
+
assert(jid, "Log(): Argument 'jid' missing")
|
1923
|
+
assert(message, "Log(): Argument 'message' missing")
|
1924
|
+
if data then
|
1925
|
+
data = assert(cjson.decode(data),
|
1926
|
+
"Log(): Argument 'data' not cjson: " .. tostring(data))
|
1927
|
+
end
|
1928
|
+
|
1929
|
+
local job = Qless.job(jid)
|
1930
|
+
assert(job:exists(), 'Log(): Job ' .. jid .. ' does not exist')
|
1931
|
+
job:history(now, message, data)
|
1932
|
+
end
|
1933
|
+
|
1934
|
+
QlessAPI.peek = function(now, queue, count)
|
1935
|
+
local jids = Qless.queue(queue):peek(now, count)
|
1936
|
+
local response = {}
|
1937
|
+
for i, jid in ipairs(jids) do
|
1938
|
+
table.insert(response, Qless.job(jid):data())
|
1939
|
+
end
|
1940
|
+
return cjson.encode(response)
|
1941
|
+
end
|
1942
|
+
|
1943
|
+
QlessAPI.pop = function(now, queue, worker, count)
|
1944
|
+
local jids = Qless.queue(queue):pop(now, worker, count)
|
1945
|
+
local response = {}
|
1946
|
+
for i, jid in ipairs(jids) do
|
1947
|
+
table.insert(response, Qless.job(jid):data())
|
1948
|
+
end
|
1949
|
+
return cjson.encode(response)
|
1950
|
+
end
|
1951
|
+
|
1952
|
+
QlessAPI.pause = function(now, ...)
|
1953
|
+
return QlessQueue.pause(now, unpack(arg))
|
1954
|
+
end
|
1955
|
+
|
1956
|
+
QlessAPI.unpause = function(now, ...)
|
1957
|
+
return QlessQueue.unpause(unpack(arg))
|
1958
|
+
end
|
1959
|
+
|
1960
|
+
QlessAPI.cancel = function(now, ...)
|
1961
|
+
return Qless.cancel(unpack(arg))
|
1962
|
+
end
|
1963
|
+
|
1964
|
+
QlessAPI.timeout = function(now, ...)
|
1965
|
+
for _, jid in ipairs(arg) do
|
1966
|
+
Qless.job(jid):timeout(now)
|
1967
|
+
end
|
1968
|
+
end
|
1969
|
+
|
1970
|
+
QlessAPI.put = function(now, me, queue, jid, klass, data, delay, ...)
|
1971
|
+
return Qless.queue(queue):put(now, me, jid, klass, data, delay, unpack(arg))
|
1972
|
+
end
|
1973
|
+
|
1974
|
+
QlessAPI.requeue = function(now, me, queue, jid, ...)
|
1975
|
+
local job = Qless.job(jid)
|
1976
|
+
assert(job:exists(), 'Requeue(): Job ' .. jid .. ' does not exist')
|
1977
|
+
return QlessAPI.put(now, me, queue, jid, unpack(arg))
|
1978
|
+
end
|
1979
|
+
|
1980
|
+
QlessAPI.unfail = function(now, queue, group, count)
|
1981
|
+
return Qless.queue(queue):unfail(now, group, count)
|
1982
|
+
end
|
1983
|
+
|
1984
|
+
QlessAPI.recur = function(now, queue, jid, klass, data, spec, ...)
|
1985
|
+
return Qless.queue(queue):recur(now, jid, klass, data, spec, unpack(arg))
|
1986
|
+
end
|
1987
|
+
|
1988
|
+
QlessAPI.unrecur = function(now, jid)
|
1989
|
+
return Qless.recurring(jid):unrecur()
|
1990
|
+
end
|
1991
|
+
|
1992
|
+
QlessAPI['recur.get'] = function(now, jid)
|
1993
|
+
local data = Qless.recurring(jid):data()
|
1994
|
+
if not data then
|
1995
|
+
return nil
|
1996
|
+
end
|
1997
|
+
return cjson.encode(data)
|
1998
|
+
end
|
1999
|
+
|
2000
|
+
QlessAPI['recur.update'] = function(now, jid, ...)
|
2001
|
+
return Qless.recurring(jid):update(now, unpack(arg))
|
2002
|
+
end
|
2003
|
+
|
2004
|
+
QlessAPI['recur.tag'] = function(now, jid, ...)
|
2005
|
+
return Qless.recurring(jid):tag(unpack(arg))
|
2006
|
+
end
|
2007
|
+
|
2008
|
+
QlessAPI['recur.untag'] = function(now, jid, ...)
|
2009
|
+
return Qless.recurring(jid):untag(unpack(arg))
|
2010
|
+
end
|
2011
|
+
|
2012
|
+
QlessAPI.length = function(now, queue)
|
2013
|
+
return Qless.queue(queue):length()
|
2014
|
+
end
|
2015
|
+
|
2016
|
+
QlessAPI['worker.deregister'] = function(now, ...)
|
2017
|
+
return QlessWorker.deregister(unpack(arg))
|
2018
|
+
end
|
2019
|
+
|
2020
|
+
QlessAPI['queue.forget'] = function(now, ...)
|
2021
|
+
QlessQueue.deregister(unpack(arg))
|
2022
|
+
end
|
2023
|
+
|
2024
|
+
|
2025
|
+
if #KEYS > 0 then error('No Keys should be provided') end
|
2026
|
+
|
2027
|
+
local command_name = assert(table.remove(ARGV, 1), 'Must provide a command')
|
2028
|
+
local command = assert(
|
2029
|
+
QlessAPI[command_name], 'Unknown command ' .. command_name)
|
2030
|
+
|
2031
|
+
local now = tonumber(table.remove(ARGV, 1))
|
2032
|
+
local now = assert(
|
2033
|
+
now, 'Arg "now" missing or not a number: ' .. (now or 'nil'))
|
2034
|
+
|
2035
|
+
return command(now, unpack(ARGV))
|
2036
|
+
|
2037
|
+
LUA_SOURCE
|
2038
|
+
|
2039
|
+
SOURCE_SHA = Digest::SHA1.hexdigest(SOURCE).freeze
|
2040
|
+
end
|