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