qless_lua 1.0.0

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