qless 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/Gemfile +8 -0
  2. data/HISTORY.md +168 -0
  3. data/README.md +571 -0
  4. data/Rakefile +28 -0
  5. data/bin/qless-campfire +106 -0
  6. data/bin/qless-growl +99 -0
  7. data/bin/qless-web +23 -0
  8. data/lib/qless.rb +185 -0
  9. data/lib/qless/config.rb +31 -0
  10. data/lib/qless/job.rb +259 -0
  11. data/lib/qless/job_reservers/ordered.rb +23 -0
  12. data/lib/qless/job_reservers/round_robin.rb +34 -0
  13. data/lib/qless/lua.rb +25 -0
  14. data/lib/qless/qless-core/cancel.lua +71 -0
  15. data/lib/qless/qless-core/complete.lua +218 -0
  16. data/lib/qless/qless-core/config.lua +44 -0
  17. data/lib/qless/qless-core/depends.lua +65 -0
  18. data/lib/qless/qless-core/fail.lua +107 -0
  19. data/lib/qless/qless-core/failed.lua +83 -0
  20. data/lib/qless/qless-core/get.lua +37 -0
  21. data/lib/qless/qless-core/heartbeat.lua +50 -0
  22. data/lib/qless/qless-core/jobs.lua +41 -0
  23. data/lib/qless/qless-core/peek.lua +155 -0
  24. data/lib/qless/qless-core/pop.lua +278 -0
  25. data/lib/qless/qless-core/priority.lua +32 -0
  26. data/lib/qless/qless-core/put.lua +156 -0
  27. data/lib/qless/qless-core/queues.lua +58 -0
  28. data/lib/qless/qless-core/recur.lua +181 -0
  29. data/lib/qless/qless-core/retry.lua +73 -0
  30. data/lib/qless/qless-core/ruby/lib/qless-core.rb +1 -0
  31. data/lib/qless/qless-core/ruby/lib/qless/core.rb +13 -0
  32. data/lib/qless/qless-core/ruby/lib/qless/core/version.rb +5 -0
  33. data/lib/qless/qless-core/ruby/spec/qless_core_spec.rb +13 -0
  34. data/lib/qless/qless-core/stats.lua +92 -0
  35. data/lib/qless/qless-core/tag.lua +100 -0
  36. data/lib/qless/qless-core/track.lua +79 -0
  37. data/lib/qless/qless-core/workers.lua +69 -0
  38. data/lib/qless/queue.rb +141 -0
  39. data/lib/qless/server.rb +411 -0
  40. data/lib/qless/tasks.rb +10 -0
  41. data/lib/qless/version.rb +3 -0
  42. data/lib/qless/worker.rb +195 -0
  43. metadata +239 -0
@@ -0,0 +1,44 @@
1
+ -- config(0, 'get', [option])
2
+ -- config(0, 'set', option, value)
3
+ -- config(0, 'unset', option)
4
+ -- -------------------------------------------------------------------------------------------------------
5
+ -- This script provides an interface to get, set, and unset configuration
6
+ -- options.
7
+ --
8
+ -- Args:
9
+ -- 1) [option]
10
+
11
+ if #KEYS > 0 then error('Config(): No keys should be provided') end
12
+
13
+ local command = ARGV[1]
14
+
15
+ local defaults = {
16
+ ['application'] = 'qless',
17
+ ['heartbeat'] = 60,
18
+ ['stats-history'] = 30,
19
+ ['histogram-history'] = 7,
20
+ ['jobs-history-count'] = 50000,
21
+ ['jobs-history'] = 604800
22
+ }
23
+
24
+ if command == 'get' then
25
+ if ARGV[2] then
26
+ return redis.call('hget', 'ql:config', ARGV[2]) or defaults[ARGV[2]]
27
+ else
28
+ -- Inspired by redis-lua https://github.com/nrk/redis-lua/blob/version-2.0/src/redis.lua
29
+ local reply = redis.call('hgetall', 'ql:config')
30
+ for i = 1, #reply, 2 do defaults[reply[i]] = reply[i + 1] end
31
+ return cjson.encode(defaults)
32
+ end
33
+ elseif command == 'set' then
34
+ local option = assert(ARGV[2], 'Config(): Arg "option" missing')
35
+ local value = assert(ARGV[3], 'Config(): Arg "value" missing')
36
+ redis.call('hset', 'ql:config', option, value)
37
+ elseif command == 'unset' then
38
+ local option = assert(ARGV[2], 'Config(): Arg "option" missing')
39
+ redis.call('hdel', 'ql:config', option)
40
+ else
41
+ error('Config(): Unrecognized command ' .. command)
42
+ end
43
+
44
+
@@ -0,0 +1,65 @@
1
+ -- Depends(0, jid, ('on', [jid, [jid, [...]]]) | ('off', ('all' | [jid, [jid, [...]]]))
2
+ -- ------------------------------------------------------------------------------------
3
+ -- Add or remove dependencies a job has. If 'on' is provided, the provided jids are
4
+ -- added as dependencies. If 'off' and 'all' are provided, then all the current dependencies
5
+ -- are removed. If 'off' is provided and the next argument is not 'all', then those
6
+ -- jids are removed as dependencies.
7
+ --
8
+ -- If a job is not already in the 'depends' state, then this call will return false.
9
+ -- Otherwise, it will return true
10
+ --
11
+ -- Args:
12
+ -- 1) jid
13
+
14
+ if #KEYS > 0 then error('Depends(): No Keys should be provided') end
15
+
16
+ local jid = assert(ARGV[1], 'Depends(): Arg "jid" missing.')
17
+ local command = assert(ARGV[2], 'Depends(): Arg 2 missing')
18
+
19
+ if redis.call('hget', 'ql:j:' .. jid, 'state') ~= 'depends' then
20
+ return false
21
+ end
22
+
23
+ if ARGV[2] == 'on' then
24
+ -- These are the jids we legitimately have to wait on
25
+ for i=3,#ARGV do
26
+ local j = ARGV[i]
27
+ -- Make sure it's something other than 'nil' or complete.
28
+ local state = redis.call('hget', 'ql:j:' .. j, 'state')
29
+ if (state and state ~= 'complete') then
30
+ redis.call('sadd', 'ql:j:' .. j .. '-dependents' , jid)
31
+ redis.call('sadd', 'ql:j:' .. jid .. '-dependencies', j)
32
+ end
33
+ end
34
+ return true
35
+ elseif ARGV[2] == 'off' then
36
+ if ARGV[3] == 'all' then
37
+ for i, j in ipairs(redis.call('smembers', 'ql:j:' .. jid .. '-dependencies')) do
38
+ redis.call('srem', 'ql:j:' .. j .. '-dependents', jid)
39
+ end
40
+ redis.call('del', 'ql:j:' .. jid .. '-dependencies')
41
+ local q, p = unpack(redis.call('hmget', 'ql:j:' .. jid, 'queue', 'priority'))
42
+ if q then
43
+ redis.call('zrem', 'ql:q:' .. q .. '-depends', jid)
44
+ redis.call('zadd', 'ql:q:' .. q .. '-work', p, jid)
45
+ redis.call('hset', 'ql:j:' .. jid, 'state', 'waiting')
46
+ end
47
+ else
48
+ for i=3,#ARGV do
49
+ local j = ARGV[i]
50
+ redis.call('srem', 'ql:j:' .. j .. '-dependents', jid)
51
+ redis.call('srem', 'ql:j:' .. jid .. '-dependencies', j)
52
+ if redis.call('scard', 'ql:j:' .. jid .. '-dependencies') == 0 then
53
+ local q, p = unpack(redis.call('hmget', 'ql:j:' .. jid, 'queue', 'priority'))
54
+ if q then
55
+ redis.call('zrem', 'ql:q:' .. q .. '-depends', jid)
56
+ redis.call('zadd', 'ql:q:' .. q .. '-work', p, jid)
57
+ redis.call('hset', 'ql:j:' .. jid, 'state', 'waiting')
58
+ end
59
+ end
60
+ end
61
+ end
62
+ return true
63
+ else
64
+ error('Depends(): Second arg must be "on" or "off"')
65
+ end
@@ -0,0 +1,107 @@
1
+ -- Fail(0, jid, worker, group, message, now, [data])
2
+ -- -------------------------------------------------
3
+ -- Mark the particular job as failed, with the provided group, and a more specific
4
+ -- message. By `group`, we mean some phrase that might be one of several categorical
5
+ -- modes of failure. The `message` is something more job-specific, like perhaps
6
+ -- a traceback.
7
+ --
8
+ -- This method should __not__ be used to note that a job has been dropped or has
9
+ -- failed in a transient way. This method __should__ be used to note that a job has
10
+ -- something really wrong with it that must be remedied.
11
+ --
12
+ -- The motivation behind the `group` is so that similar errors can be grouped together.
13
+ -- Optionally, updated data can be provided for the job. A job in any state can be
14
+ -- marked as failed. If it has been given to a worker as a job, then its subsequent
15
+ -- requests to heartbeat or complete that job will fail. Failed jobs are kept until
16
+ -- they are canceled or completed. __Returns__ the id of the failed job if successful,
17
+ -- or `False` on failure.
18
+ --
19
+ -- Args:
20
+ -- 1) jid
21
+ -- 2) worker
22
+ -- 3) group
23
+ -- 4) message
24
+ -- 5) the current time
25
+ -- 6) [data]
26
+
27
+ if #KEYS > 0 then error('Fail(): No Keys should be provided') end
28
+
29
+ local jid = assert(ARGV[1] , 'Fail(): Arg "jid" missing')
30
+ local worker = assert(ARGV[2] , 'Fail(): Arg "worker" missing')
31
+ local group = assert(ARGV[3] , 'Fail(): Arg "group" missing')
32
+ local message = assert(ARGV[4] , 'Fail(): Arg "message" missing')
33
+ local now = assert(tonumber(ARGV[5]), 'Fail(): Arg "now" missing or malformed: ' .. (ARGV[5] or 'nil'))
34
+ local data = ARGV[6]
35
+
36
+ -- The bin is midnight of the provided day
37
+ -- 24 * 60 * 60 = 86400
38
+ local bin = now - (now % 86400)
39
+
40
+ if data then
41
+ data = cjson.decode(data)
42
+ end
43
+
44
+ -- First things first, we should get the history
45
+ local history, queue, state = unpack(redis.call('hmget', 'ql:j:' .. jid, 'history', 'queue', 'state'))
46
+
47
+ -- If the job has been completed, we cannot fail it
48
+ if state ~= 'running' then
49
+ return false
50
+ end
51
+
52
+ if redis.call('zscore', 'ql:tracked', jid) ~= false then
53
+ redis.call('publish', 'failed', jid)
54
+ end
55
+
56
+ -- Remove this job from the jobs that the worker that was running it has
57
+ redis.call('zrem', 'ql:w:' .. worker .. ':jobs', jid)
58
+
59
+ -- Now, take the element of the history for which our provided worker is the worker, and update 'failed'
60
+ history = cjson.decode(history or '[]')
61
+ if #history > 0 then
62
+ for i=#history,1,-1 do
63
+ if history[i]['worker'] == worker then
64
+ history[i]['failed'] = math.floor(now)
65
+ end
66
+ end
67
+ else
68
+ history = {
69
+ {
70
+ worker = worker,
71
+ failed = math.floor(now)
72
+ }
73
+ }
74
+ end
75
+
76
+ -- Increment the number of failures for that queue for the
77
+ -- given day.
78
+ redis.call('hincrby', 'ql:s:stats:' .. bin .. ':' .. queue, 'failures', 1)
79
+ redis.call('hincrby', 'ql:s:stats:' .. bin .. ':' .. queue, 'failed' , 1)
80
+
81
+ -- Now remove the instance from the schedule, and work queues for the queue it's in
82
+ redis.call('zrem', 'ql:q:' .. queue .. '-work', jid)
83
+ redis.call('zrem', 'ql:q:' .. queue .. '-locks', jid)
84
+ redis.call('zrem', 'ql:q:' .. queue .. '-scheduled', jid)
85
+
86
+ -- The reason that this appears here is that the above will fail if the job doesn't exist
87
+ if data then
88
+ redis.call('hset', 'ql:j:' .. jid, 'data', cjson.encode(data))
89
+ end
90
+
91
+ redis.call('hmset', 'ql:j:' .. jid, 'state', 'failed', 'worker', '',
92
+ 'expires', '', 'history', cjson.encode(history), 'failure', cjson.encode({
93
+ ['group'] = group,
94
+ ['message'] = message,
95
+ ['when'] = math.floor(now),
96
+ ['worker'] = worker
97
+ }))
98
+
99
+ -- Add this group of failure to the list of failures
100
+ redis.call('sadd', 'ql:failures', group)
101
+ -- And add this particular instance to the failed groups
102
+ redis.call('lpush', 'ql:f:' .. group, jid)
103
+
104
+ -- Here is where we'd intcrement stats about the particular stage
105
+ -- and possibly the workers
106
+
107
+ return jid
@@ -0,0 +1,83 @@
1
+ -- Failed(0, [group, [start, [limit]]])
2
+ -- ------------------------------------
3
+ -- If no group is provided, this returns a JSON blob of the counts of the various
4
+ -- groups of failures known. If a group is provided, it will report up to `limit`
5
+ -- from `start` of the jobs affected by that issue. __Returns__ a JSON blob.
6
+ --
7
+ -- # If no group, then...
8
+ -- {
9
+ -- 'group1': 1,
10
+ -- 'group2': 5,
11
+ -- ...
12
+ -- }
13
+ --
14
+ -- # If a group is provided, then...
15
+ -- {
16
+ -- 'total': 20,
17
+ -- 'jobs': [
18
+ -- {
19
+ -- # All the normal keys for a job
20
+ -- 'jid': ...,
21
+ -- 'data': ...
22
+ -- # The message for this particular instance
23
+ -- 'message': ...,
24
+ -- 'group': ...,
25
+ -- }, ...
26
+ -- ]
27
+ -- }
28
+ --
29
+ -- Args:
30
+ -- 1) [group]
31
+ -- 2) [start]
32
+ -- 3) [limit]
33
+
34
+ if #KEYS > 0 then error('Failed(): No Keys should be provided') end
35
+
36
+ local group = ARGV[1]
37
+ local start = assert(tonumber(ARGV[2] or 0), 'Failed(): Arg "start" is not a number: ' .. (ARGV[2] or 'nil'))
38
+ local limit = assert(tonumber(ARGV[3] or 25), 'Failed(): Arg "limit" is not a number: ' .. (ARGV[3] or 'nil'))
39
+
40
+ if group then
41
+ -- If a group was provided, then we should do paginated lookup into that
42
+ local response = {
43
+ total = redis.call('llen', 'ql:f:' .. group),
44
+ jobs = {}
45
+ }
46
+ local jids = redis.call('lrange', 'ql:f:' .. group, start, limit)
47
+ for index, jid in ipairs(jids) do
48
+ local job = redis.call(
49
+ 'hmget', 'ql:j:' .. jid, 'jid', 'klass', 'state', 'queue', 'worker', 'priority',
50
+ 'expires', 'retries', 'remaining', 'data', 'tags', 'history', 'failure')
51
+
52
+ table.insert(response.jobs, {
53
+ jid = job[1],
54
+ klass = job[2],
55
+ state = job[3],
56
+ queue = job[4],
57
+ worker = job[5] or '',
58
+ tracked = redis.call('zscore', 'ql:tracked', jid) ~= false,
59
+ priority = tonumber(job[6]),
60
+ expires = tonumber(job[7]) or 0,
61
+ retries = tonumber(job[8]),
62
+ remaining = tonumber(job[9]),
63
+ data = cjson.decode(job[10]),
64
+ tags = cjson.decode(job[11]),
65
+ history = cjson.decode(job[12]),
66
+ failure = cjson.decode(job[13] or '{}'),
67
+ dependents = redis.call('smembers', 'ql:j:' .. jid .. '-dependents'),
68
+ -- A job in the failed state can not have dependencies
69
+ -- because it has been popped off of a queue, which
70
+ -- means all of its dependencies have been satisfied
71
+ dependencies = {}
72
+ })
73
+ end
74
+ return cjson.encode(response)
75
+ else
76
+ -- Otherwise, we should just list all the known failure groups we have
77
+ local response = {}
78
+ local groups = redis.call('smembers', 'ql:failures')
79
+ for index, group in ipairs(groups) do
80
+ response[group] = redis.call('llen', 'ql:f:' .. group)
81
+ end
82
+ return cjson.encode(response)
83
+ end
@@ -0,0 +1,37 @@
1
+ -- This gets all the data associated with the job with the
2
+ -- provided id.
3
+ --
4
+ -- Args:
5
+ -- 1) jid
6
+
7
+ if #KEYS > 0 then error('Get(): No Keys should be provided') end
8
+
9
+ local jid = assert(ARGV[1], 'Get(): Arg "jid" missing')
10
+
11
+ -- Let's get all the data we can
12
+ local job = redis.call(
13
+ 'hmget', 'ql:j:' .. jid, 'jid', 'klass', 'state', 'queue', 'worker', 'priority',
14
+ 'expires', 'retries', 'remaining', 'data', 'tags', 'history', 'failure')
15
+
16
+ if not job[1] then
17
+ return false
18
+ end
19
+
20
+ return cjson.encode({
21
+ jid = job[1],
22
+ klass = job[2],
23
+ state = job[3],
24
+ queue = job[4],
25
+ worker = job[5] or '',
26
+ tracked = redis.call('zscore', 'ql:tracked', jid) ~= false,
27
+ priority = tonumber(job[6]),
28
+ expires = tonumber(job[7]) or 0,
29
+ retries = tonumber(job[8]),
30
+ remaining = tonumber(job[9]),
31
+ data = cjson.decode(job[10]),
32
+ tags = cjson.decode(job[11]),
33
+ history = cjson.decode(job[12]),
34
+ failure = cjson.decode(job[13] or '{}'),
35
+ dependents = redis.call('smembers', 'ql:j:' .. jid .. '-dependents'),
36
+ dependencies = redis.call('smembers', 'ql:j:' .. jid .. '-dependencies')
37
+ })
@@ -0,0 +1,50 @@
1
+ -- This scripts conducts a heartbeat for a job, and returns
2
+ -- either the new expiration or False if the lock has been
3
+ -- given to another node
4
+ --
5
+ -- Args:
6
+ -- 1) jid
7
+ -- 2) worker
8
+ -- 3) now
9
+ -- 4) [data]
10
+
11
+ if #KEYS > 0 then error('Heartbeat(): No Keys should be provided') end
12
+
13
+ local jid = assert(ARGV[1] , 'Heartbeat(): Arg "jid" missing')
14
+ local worker = assert(ARGV[2] , 'Heartbeat(): Arg "worker" missing')
15
+ local now = assert(tonumber(ARGV[3]), 'Heartbeat(): Arg "now" missing')
16
+ local data = ARGV[4]
17
+
18
+ -- We should find the heartbeat interval for this queue
19
+ -- heartbeat. First, though, we need to find the queue
20
+ -- this particular job is in
21
+ local queue = redis.call('hget', 'ql:j:' .. jid, 'queue') or ''
22
+ local _hb, _qhb = unpack(redis.call('hmget', 'ql:config', 'heartbeat', queue .. '-heartbeat'))
23
+ local expires = now + tonumber(_qhb or _hb or 60)
24
+
25
+ if data then
26
+ data = cjson.decode(data)
27
+ end
28
+
29
+ -- First, let's see if the worker still owns this job, and there is a worker
30
+ if redis.call('hget', 'ql:j:' .. jid, 'worker') ~= worker or #worker == 0 then
31
+ return false
32
+ else
33
+ -- Otherwise, optionally update the user data, and the heartbeat
34
+ if data then
35
+ -- I don't know if this is wise, but I'm decoding and encoding
36
+ -- the user data to hopefully ensure its sanity
37
+ redis.call('hmset', 'ql:j:' .. jid, 'expires', expires, 'worker', worker, 'data', cjson.encode(data))
38
+ else
39
+ redis.call('hmset', 'ql:j:' .. jid, 'expires', expires, 'worker', worker)
40
+ end
41
+
42
+ -- Update hwen this job was last updated on that worker
43
+ -- Add this job to the list of jobs handled by this worker
44
+ redis.call('zadd', 'ql:w:' .. worker .. ':jobs', expires, jid)
45
+
46
+ -- And now we should just update the locks
47
+ local queue = redis.call('hget', 'ql:j:' .. jid, 'queue')
48
+ redis.call('zadd', 'ql:q:'.. queue .. '-locks', expires, jid)
49
+ return expires
50
+ end
@@ -0,0 +1,41 @@
1
+ -- Jobs(0, 'complete' | (('stalled' | 'running' | 'scheduled' | 'depends', 'recurring'), now, queue) [offset, [count]])
2
+ -- -------------------------------------------------------------------------------------------------------
3
+ --
4
+ -- Return all the job ids currently considered to be in the provided state
5
+ -- in a particular queue. The response is a list of job ids:
6
+ --
7
+ -- [
8
+ -- jid1,
9
+ -- jid2,
10
+ -- ...
11
+ -- ]
12
+
13
+ if #KEYS > 0 then
14
+ error('Jobs(): Got '.. #KEYS .. ' expected 0 KEYS arguments')
15
+ end
16
+
17
+ local t = assert(ARGV[1] , 'Jobs(): Arg "type" missing')
18
+ if t == 'complete' then
19
+ local offset = assert(tonumber(ARGV[2] or 0) , 'Jobs(): Arg "offset" not a number: ' .. tostring(ARGV[2]))
20
+ local count = assert(tonumber(ARGV[3] or 25), 'Jobs(): Arg "count" not a number: ' .. tostring(ARGV[3]))
21
+ return redis.call('zrevrange', 'ql:completed', offset, offset + count - 1)
22
+ else
23
+ local now = assert(tonumber(ARGV[2]) , 'Jobs(): Arg "now" missing or not a number: ' .. tostring(ARGV[2]))
24
+ local queue = assert(ARGV[3] , 'Jobs(): Arg "queue" missing')
25
+ local offset = assert(tonumber(ARGV[4] or 0) , 'Jobs(): Arg "offset" not a number: ' .. tostring(ARGV[4]))
26
+ local count = assert(tonumber(ARGV[5] or 25), 'Jobs(): Arg "count" not a number: ' .. tostring(ARGV[5]))
27
+
28
+ if t == 'running' then
29
+ return redis.call('zrangebyscore', 'ql:q:' .. queue .. '-locks', now, 133389432700, 'limit', offset, count)
30
+ elseif t == 'stalled' then
31
+ return redis.call('zrangebyscore', 'ql:q:' .. queue .. '-locks', 0, now, 'limit', offset, count)
32
+ elseif t == 'scheduled' then
33
+ return redis.call('zrange', 'ql:q:' .. queue .. '-scheduled', offset, offset + count - 1)
34
+ elseif t == 'depends' then
35
+ return redis.call('zrange', 'ql:q:' .. queue .. '-depends', offset, offset + count - 1)
36
+ elseif t == 'recurring' then
37
+ return redis.call('zrange', 'ql:q:' .. queue .. '-recur', offset, offset + count - 1)
38
+ else
39
+ error('Jobs(): Unknown type "' .. t .. '"')
40
+ end
41
+ end
@@ -0,0 +1,155 @@
1
+ -- This script takes the name of the queue and then checks
2
+ -- for any expired locks, then inserts any scheduled items
3
+ -- that are now valid, and lastly returns any work items
4
+ -- that can be handed over.
5
+ --
6
+ -- Keys:
7
+ -- 1) queue name
8
+ -- Args:
9
+ -- 1) the number of items to return
10
+ -- 2) the current time
11
+
12
+ if #KEYS ~= 1 then
13
+ if #KEYS < 1 then
14
+ error('Peek(): Expected 1 KEYS argument')
15
+ else
16
+ error('Peek(): Got ' .. #KEYS .. ', expected 1 KEYS argument')
17
+ end
18
+ end
19
+
20
+ local queue = assert(KEYS[1] , 'Peek(): Key "queue" missing')
21
+ local key = 'ql:q:' .. queue
22
+ local count = assert(tonumber(ARGV[1]) , 'Peek(): Arg "count" missing or not a number: ' .. (ARGV[2] or 'nil'))
23
+ local now = assert(tonumber(ARGV[2]) , 'Peek(): Arg "now" missing or not a number: ' .. (ARGV[3] or 'nil'))
24
+
25
+ -- These are the ids that we're going to return
26
+ local keys = {}
27
+
28
+ -- Iterate through all the expired locks and add them to the list
29
+ -- of keys that we'll return
30
+ for index, jid in ipairs(redis.call('zrangebyscore', key .. '-locks', 0, now, 'LIMIT', 0, count)) do
31
+ table.insert(keys, jid)
32
+ end
33
+
34
+ -- If we still need jobs in order to meet demand, then we should
35
+ -- look for all the recurring jobs that need jobs run
36
+ if #keys < count then
37
+ local r = redis.call('zrangebyscore', key .. '-recur', 0, now)
38
+ for index, jid in ipairs(r) do
39
+ -- For each of the jids that need jobs scheduled, first
40
+ -- get the last time each of them was run, and then increment
41
+ -- it by its interval. While this time is less than now,
42
+ -- we need to keep putting jobs on the queue
43
+ local klass, data, priority, tags, retries, interval = unpack(redis.call('hmget', 'ql:r:' .. jid, 'klass', 'data', 'priority', 'tags', 'retries', 'interval'))
44
+ local _tags = cjson.decode(tags)
45
+
46
+ while math.floor(tonumber(redis.call('zscore', key .. '-recur', jid))) < now do
47
+ local count = redis.call('hincrby', 'ql:r:' .. jid, 'count', 1)
48
+
49
+ -- Add this job to the list of jobs tagged with whatever tags were supplied
50
+ for i, tag in ipairs(_tags) do
51
+ redis.call('zadd', 'ql:t:' .. tag, now, jid .. '-' .. count)
52
+ redis.call('zincrby', 'ql:tags', 1, tag)
53
+ end
54
+
55
+ -- First, let's save its data
56
+ redis.call('hmset', 'ql:j:' .. jid .. '-' .. count,
57
+ 'jid' , jid .. '-' .. count,
58
+ 'klass' , klass,
59
+ 'data' , data,
60
+ 'priority' , priority,
61
+ 'tags' , tags,
62
+ 'state' , 'waiting',
63
+ 'worker' , '',
64
+ 'expires' , 0,
65
+ 'queue' , queue,
66
+ 'retries' , retries,
67
+ 'remaining', retries,
68
+ 'history' , cjson.encode({{
69
+ q = queue,
70
+ put = math.floor(now)
71
+ }}))
72
+
73
+ -- Now, if a delay was provided, and if it's in the future,
74
+ -- then we'll have to schedule it. Otherwise, we're just
75
+ -- going to add it to the work queue.
76
+ redis.call('zadd', key .. '-work', priority - (now / 10000000000), jid .. '-' .. count)
77
+
78
+ redis.call('zincrby', key .. '-recur', interval, jid)
79
+ end
80
+ end
81
+ end
82
+
83
+ -- Now we've checked __all__ the locks for this queue the could
84
+ -- have expired, and are no more than the number requested. If
85
+ -- we still need values in order to meet the demand, then we
86
+ -- should check if any scheduled items, and if so, we should
87
+ -- insert them to ensure correctness when pulling off the next
88
+ -- unit of work.
89
+ if #keys < count then
90
+ -- zadd is a list of arguments that we'll be able to use to
91
+ -- insert into the work queue
92
+ local zadd = {}
93
+ local r = redis.call('zrangebyscore', key .. '-scheduled', 0, now, 'LIMIT', 0, (count - #keys))
94
+ for index, jid in ipairs(r) do
95
+ -- With these in hand, we'll have to go out and find the
96
+ -- priorities of these jobs, and then we'll insert them
97
+ -- into the work queue and then when that's complete, we'll
98
+ -- remove them from the scheduled queue
99
+ table.insert(zadd, tonumber(redis.call('hget', 'ql:j:' .. jid, 'priority') or 0))
100
+ table.insert(zadd, jid)
101
+ -- We should also update them to have the state 'waiting'
102
+ -- instead of 'scheduled'
103
+ redis.call('hset', 'ql:j:' .. jid, 'state', 'waiting')
104
+ end
105
+
106
+ if #zadd > 0 then
107
+ -- Now add these to the work list, and then remove them
108
+ -- from the scheduled list
109
+ redis.call('zadd', key .. '-work', unpack(zadd))
110
+ redis.call('zrem', key .. '-scheduled', unpack(r))
111
+ end
112
+
113
+ -- And now we should get up to the maximum number of requested
114
+ -- work items from the work queue.
115
+ for index, jid in ipairs(redis.call('zrevrange', key .. '-work', 0, (count - #keys) - 1)) do
116
+ table.insert(keys, jid)
117
+ end
118
+ end
119
+
120
+ -- Alright, now the `keys` table is filled with all the job
121
+ -- ids which we'll be returning. Now we need to get the
122
+ -- metadeata about each of these, update their metadata to
123
+ -- reflect which worker they're on, when the lock expires,
124
+ -- etc., add them to the locks queue and then we have to
125
+ -- finally return a list of json blobs
126
+
127
+ local response = {}
128
+ for index, jid in ipairs(keys) do
129
+ local job = redis.call(
130
+ 'hmget', 'ql:j:' .. jid, 'jid', 'klass', 'state', 'queue', 'worker', 'priority',
131
+ 'expires', 'retries', 'remaining', 'data', 'tags', 'history', 'failure')
132
+
133
+ table.insert(response, cjson.encode({
134
+ jid = job[1],
135
+ klass = job[2],
136
+ state = job[3],
137
+ queue = job[4],
138
+ worker = job[5] or '',
139
+ tracked = redis.call('zscore', 'ql:tracked', jid) ~= false,
140
+ priority = tonumber(job[6]),
141
+ expires = tonumber(job[7]) or 0,
142
+ retries = tonumber(job[8]),
143
+ remaining = tonumber(job[9]),
144
+ data = cjson.decode(job[10]),
145
+ tags = cjson.decode(job[11]),
146
+ history = cjson.decode(job[12]),
147
+ failure = cjson.decode(job[13] or '{}'),
148
+ dependents = redis.call('smembers', 'ql:j:' .. jid .. '-dependents'),
149
+ -- A job in the waiting state can not have dependencies
150
+ dependencies = {}
151
+
152
+ }))
153
+ end
154
+
155
+ return response