qless_lua 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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