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