qless 0.9.3 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/Gemfile +9 -3
  2. data/README.md +70 -25
  3. data/Rakefile +125 -9
  4. data/exe/install_phantomjs +21 -0
  5. data/lib/qless.rb +115 -76
  6. data/lib/qless/config.rb +11 -9
  7. data/lib/qless/failure_formatter.rb +43 -0
  8. data/lib/qless/job.rb +201 -102
  9. data/lib/qless/job_reservers/ordered.rb +7 -1
  10. data/lib/qless/job_reservers/round_robin.rb +16 -6
  11. data/lib/qless/job_reservers/shuffled_round_robin.rb +9 -2
  12. data/lib/qless/lua/qless-lib.lua +2463 -0
  13. data/lib/qless/lua/qless.lua +2012 -0
  14. data/lib/qless/lua_script.rb +63 -12
  15. data/lib/qless/middleware/memory_usage_monitor.rb +62 -0
  16. data/lib/qless/middleware/metriks.rb +45 -0
  17. data/lib/qless/middleware/redis_reconnect.rb +6 -3
  18. data/lib/qless/middleware/requeue_exceptions.rb +94 -0
  19. data/lib/qless/middleware/retry_exceptions.rb +38 -9
  20. data/lib/qless/middleware/sentry.rb +3 -7
  21. data/lib/qless/middleware/timeout.rb +64 -0
  22. data/lib/qless/queue.rb +90 -55
  23. data/lib/qless/server.rb +177 -130
  24. data/lib/qless/server/views/_job.erb +33 -15
  25. data/lib/qless/server/views/completed.erb +11 -0
  26. data/lib/qless/server/views/layout.erb +70 -11
  27. data/lib/qless/server/views/overview.erb +93 -53
  28. data/lib/qless/server/views/queue.erb +9 -8
  29. data/lib/qless/server/views/queues.erb +18 -1
  30. data/lib/qless/subscriber.rb +37 -22
  31. data/lib/qless/tasks.rb +5 -10
  32. data/lib/qless/test_helpers/worker_helpers.rb +55 -0
  33. data/lib/qless/version.rb +3 -1
  34. data/lib/qless/worker.rb +4 -413
  35. data/lib/qless/worker/base.rb +247 -0
  36. data/lib/qless/worker/forking.rb +245 -0
  37. data/lib/qless/worker/serial.rb +41 -0
  38. metadata +135 -52
  39. data/lib/qless/qless-core/cancel.lua +0 -101
  40. data/lib/qless/qless-core/complete.lua +0 -233
  41. data/lib/qless/qless-core/config.lua +0 -56
  42. data/lib/qless/qless-core/depends.lua +0 -65
  43. data/lib/qless/qless-core/deregister_workers.lua +0 -12
  44. data/lib/qless/qless-core/fail.lua +0 -117
  45. data/lib/qless/qless-core/failed.lua +0 -83
  46. data/lib/qless/qless-core/get.lua +0 -37
  47. data/lib/qless/qless-core/heartbeat.lua +0 -51
  48. data/lib/qless/qless-core/jobs.lua +0 -41
  49. data/lib/qless/qless-core/pause.lua +0 -18
  50. data/lib/qless/qless-core/peek.lua +0 -165
  51. data/lib/qless/qless-core/pop.lua +0 -314
  52. data/lib/qless/qless-core/priority.lua +0 -32
  53. data/lib/qless/qless-core/put.lua +0 -169
  54. data/lib/qless/qless-core/qless-lib.lua +0 -2354
  55. data/lib/qless/qless-core/qless.lua +0 -1862
  56. data/lib/qless/qless-core/queues.lua +0 -58
  57. data/lib/qless/qless-core/recur.lua +0 -190
  58. data/lib/qless/qless-core/retry.lua +0 -73
  59. data/lib/qless/qless-core/stats.lua +0 -92
  60. data/lib/qless/qless-core/tag.lua +0 -100
  61. data/lib/qless/qless-core/track.lua +0 -79
  62. data/lib/qless/qless-core/unfail.lua +0 -54
  63. data/lib/qless/qless-core/unpause.lua +0 -12
  64. data/lib/qless/qless-core/workers.lua +0 -69
  65. data/lib/qless/wait_until.rb +0 -19
@@ -1,32 +0,0 @@
1
- -- priority(0, jid, priority)
2
- -- --------------------------
3
- -- Accepts a jid, and a new priority for the job. If the job
4
- -- doesn't exist, then return false. Otherwise, return the
5
- -- updated priority. If the job is waiting, then the change
6
- -- will be reflected in the order in which it's popped
7
-
8
- if #KEYS ~= 0 then
9
- error('Priority(): Got ' .. #KEYS .. ', expected 0')
10
- end
11
-
12
- local jid = assert(ARGV[1] , 'Priority(): Arg "jid" missing')
13
- local priority = assert(tonumber(ARGV[2]), 'Priority(): Arg "priority" missing or not a number: ' .. tostring(ARGV[2]))
14
-
15
- -- Get the queue the job is currently in, if any
16
- local queue = redis.call('hget', 'ql:j:' .. jid, 'queue')
17
-
18
- if queue == nil then
19
- return false
20
- elseif queue == '' then
21
- -- Just adjust the priority
22
- redis.call('hset', 'ql:j:' .. jid, 'priority', priority)
23
- return priority
24
- else
25
- -- Adjust the priority and see if it's a candidate for updating
26
- -- its priority in the queue it's currently in
27
- if redis.call('zscore', 'ql:q:' .. queue .. '-work', jid) then
28
- redis.call('zadd', 'ql:q:' .. queue .. '-work', priority, jid)
29
- end
30
- redis.call('hset', 'ql:j:' .. jid, 'priority', priority)
31
- return priority
32
- end
@@ -1,169 +0,0 @@
1
- -- Put(1, queue, jid, klass, data, now, delay, [priority, p], [tags, t], [retries, r], [depends, '[...]'])
2
- -- -------------------------------------------------------------------------------------------------------
3
- -- This script takes the name of the queue and then the
4
- -- info about the work item, and makes sure that it's
5
- -- enqueued.
6
- --
7
- -- At some point, I'd like to able to provide functionality
8
- -- that enables this to generate a unique ID for this piece
9
- -- of work. As such, client libraries should not expose
10
- -- setting the id from the user, as this is an implementation
11
- -- detail that's likely to change and users should not grow
12
- -- to depend on it.
13
- --
14
- -- Keys:
15
- -- 1) queue name
16
- -- Args:
17
- -- 1) jid
18
- -- 2) klass
19
- -- 3) data
20
- -- 4) now
21
- -- 5) delay
22
- -- *) [priority, p], [tags, t], [retries, r], [depends, '[...]']
23
-
24
- if #KEYS ~= 1 then
25
- if #KEYS < 1 then
26
- error('Put(): Expected 1 KEYS argument')
27
- else
28
- error('Put(): Got ' .. #KEYS .. ', expected 1 KEYS argument')
29
- end
30
- end
31
-
32
- local queue = assert(KEYS[1] , 'Put(): Key "queue" missing')
33
- local jid = assert(ARGV[1] , 'Put(): Arg "jid" missing')
34
- local klass = assert(ARGV[2] , 'Put(): Arg "klass" missing')
35
- local data = assert(cjson.decode(ARGV[3]) , 'Put(): Arg "data" missing or not JSON: ' .. tostring(ARGV[3]))
36
- local now = assert(tonumber(ARGV[4]) , 'Put(): Arg "now" missing or not a number: ' .. tostring(ARGV[4]))
37
- local delay = assert(tonumber(ARGV[5]) , 'Put(): Arg "delay" not a number: ' .. tostring(ARGV[5]))
38
-
39
- -- Read in all the optional parameters
40
- local options = {}
41
- for i = 6, #ARGV, 2 do options[ARGV[i]] = ARGV[i + 1] end
42
-
43
- -- Let's see what the old priority, history and tags were
44
- local history, priority, tags, oldqueue, state, failure, retries, worker = unpack(redis.call('hmget', 'ql:j:' .. jid, 'history', 'priority', 'tags', 'queue', 'state', 'failure', 'retries', 'worker'))
45
-
46
- -- Sanity check on optional args
47
- retries = assert(tonumber(options['retries'] or retries or 5) , 'Put(): Arg "retries" not a number: ' .. tostring(options['retries']))
48
- tags = assert(cjson.decode(options['tags'] or tags or '[]' ), 'Put(): Arg "tags" not JSON' .. tostring(options['tags']))
49
- priority = assert(tonumber(options['priority'] or priority or 0), 'Put(): Arg "priority" not a number' .. tostring(options['priority']))
50
- local depends = assert(cjson.decode(options['depends'] or '[]') , 'Put(): Arg "depends" not JSON: ' .. tostring(options['depends']))
51
-
52
- -- Delay and depends are not allowed together
53
- if delay > 0 and #depends > 0 then
54
- error('Put(): "delay" and "depends" are not allowed to be used together')
55
- end
56
-
57
- -- Send out a log message
58
- redis.call('publish', 'ql:log', cjson.encode({
59
- jid = jid,
60
- event = 'put',
61
- queue = queue
62
- }))
63
-
64
- -- Update the history to include this new change
65
- local history = cjson.decode(history or '{}')
66
- table.insert(history, {
67
- q = queue,
68
- put = math.floor(now)
69
- })
70
-
71
- -- If this item was previously in another queue, then we should remove it from there
72
- if oldqueue then
73
- redis.call('zrem', 'ql:q:' .. oldqueue .. '-work', jid)
74
- redis.call('zrem', 'ql:q:' .. oldqueue .. '-locks', jid)
75
- redis.call('zrem', 'ql:q:' .. oldqueue .. '-scheduled', jid)
76
- redis.call('zrem', 'ql:q:' .. oldqueue .. '-depends', jid)
77
- end
78
-
79
- -- If this had previously been given out to a worker,
80
- -- make sure to remove it from that worker's jobs
81
- if worker then
82
- redis.call('zrem', 'ql:w:' .. worker .. ':jobs', jid)
83
- -- We need to inform whatever worker had that job
84
- redis.call('publish', 'ql:w:' .. worker, cjson.encode({
85
- jid = jid,
86
- event = 'put',
87
- queue = queue
88
- }))
89
- end
90
-
91
- -- If the job was previously in the 'completed' state, then we should remove
92
- -- it from being enqueued for destructination
93
- if state == 'complete' then
94
- redis.call('zrem', 'ql:completed', jid)
95
- end
96
-
97
- -- Add this job to the list of jobs tagged with whatever tags were supplied
98
- for i, tag in ipairs(tags) do
99
- redis.call('zadd', 'ql:t:' .. tag, now, jid)
100
- redis.call('zincrby', 'ql:tags', 1, tag)
101
- end
102
-
103
- -- If we're in the failed state, remove all of our data
104
- if state == 'failed' then
105
- failure = cjson.decode(failure)
106
- -- We need to make this remove it from the failed queues
107
- redis.call('lrem', 'ql:f:' .. failure.group, 0, jid)
108
- if redis.call('llen', 'ql:f:' .. failure.group) == 0 then
109
- redis.call('srem', 'ql:failures', failure.group)
110
- end
111
- -- The bin is midnight of the provided day
112
- -- 24 * 60 * 60 = 86400
113
- local bin = failure.when - (failure.when % 86400)
114
- -- We also need to decrement the stats about the queue on
115
- -- the day that this failure actually happened.
116
- redis.call('hincrby', 'ql:s:stats:' .. bin .. ':' .. queue, 'failed' , -1)
117
- end
118
-
119
- -- First, let's save its data
120
- redis.call('hmset', 'ql:j:' .. jid,
121
- 'jid' , jid,
122
- 'klass' , klass,
123
- 'data' , cjson.encode(data),
124
- 'priority' , priority,
125
- 'tags' , cjson.encode(tags),
126
- 'state' , ((delay > 0) and 'scheduled') or 'waiting',
127
- 'worker' , '',
128
- 'expires' , 0,
129
- 'queue' , queue,
130
- 'retries' , retries,
131
- 'remaining', retries,
132
- 'history' , cjson.encode(history))
133
-
134
- -- These are the jids we legitimately have to wait on
135
- for i, j in ipairs(depends) do
136
- -- Make sure it's something other than 'nil' or complete.
137
- local state = redis.call('hget', 'ql:j:' .. j, 'state')
138
- if (state and state ~= 'complete') then
139
- redis.call('sadd', 'ql:j:' .. j .. '-dependents' , jid)
140
- redis.call('sadd', 'ql:j:' .. jid .. '-dependencies', j)
141
- end
142
- end
143
-
144
- -- Now, if a delay was provided, and if it's in the future,
145
- -- then we'll have to schedule it. Otherwise, we're just
146
- -- going to add it to the work queue.
147
- if delay > 0 then
148
- redis.call('zadd', 'ql:q:' .. queue .. '-scheduled', now + delay, jid)
149
- else
150
- if redis.call('scard', 'ql:j:' .. jid .. '-dependencies') > 0 then
151
- redis.call('zadd', 'ql:q:' .. queue .. '-depends', now, jid)
152
- redis.call('hset', 'ql:j:' .. jid, 'state', 'depends')
153
- else
154
- redis.call('zadd', 'ql:q:' .. queue .. '-work', priority - (now / 10000000000), jid)
155
- end
156
- end
157
-
158
- -- Lastly, we're going to make sure that this item is in the
159
- -- set of known queues. We should keep this sorted by the
160
- -- order in which we saw each of these queues
161
- if redis.call('zscore', 'ql:queues', queue) == false then
162
- redis.call('zadd', 'ql:queues', now, queue)
163
- end
164
-
165
- if redis.call('zscore', 'ql:tracked', jid) ~= false then
166
- redis.call('publish', 'put', jid)
167
- end
168
-
169
- return jid
@@ -1,2354 +0,0 @@
1
- -------------------------------------------------------------------------------
2
- -- Forward declarations to make everything happy
3
- -------------------------------------------------------------------------------
4
- local Qless = {
5
- ns = 'ql:'
6
- }
7
-
8
- -- Queue forward delcaration
9
- local QlessQueue = {
10
- ns = Qless.ns .. 'q:'
11
- }
12
- QlessQueue.__index = QlessQueue
13
-
14
- -- Worker forward declaration
15
- local QlessWorker = {
16
- ns = Qless.ns .. 'w:'
17
- }
18
- QlessWorker.__index = QlessWorker
19
-
20
- -- Job forward declaration
21
- local QlessJob = {
22
- ns = Qless.ns .. 'j:'
23
- }
24
- QlessJob.__index = QlessJob
25
-
26
- -- RecurringJob forward declaration
27
- local QlessRecurringJob = {}
28
- QlessRecurringJob.__index = QlessRecurringJob
29
-
30
- -- Config forward declaration
31
- Qless.config = {}
32
-
33
- -- Extend a table. This comes up quite frequently
34
- function table.extend(self, other)
35
- for i, v in ipairs(other) do
36
- table.insert(self, v)
37
- end
38
- end
39
-
40
- function Qless.debug(message)
41
- redis.call('publish', 'debug', tostring(message))
42
- end
43
-
44
- function Qless.publish(channel, message)
45
- redis.call('publish', Qless.ns .. channel, message)
46
- end
47
-
48
- -- Return a job object
49
- function Qless.job(jid)
50
- assert(jid, 'Job(): no jid provided')
51
- local job = {}
52
- setmetatable(job, QlessJob)
53
- job.jid = jid
54
- return job
55
- end
56
-
57
- -- Return a recurring job object
58
- function Qless.recurring(jid)
59
- assert(jid, 'Recurring(): no jid provided')
60
- local job = {}
61
- setmetatable(job, QlessRecurringJob)
62
- job.jid = jid
63
- return job
64
- end
65
-
66
- -- Failed([group, [start, [limit]]])
67
- -- ------------------------------------
68
- -- If no group is provided, this returns a JSON blob of the counts of the
69
- -- various groups of failures known. If a group is provided, it will report up
70
- -- to `limit` from `start` of the jobs affected by that issue.
71
- --
72
- -- # If no group, then...
73
- -- {
74
- -- 'group1': 1,
75
- -- 'group2': 5,
76
- -- ...
77
- -- }
78
- --
79
- -- # If a group is provided, then...
80
- -- {
81
- -- 'total': 20,
82
- -- 'jobs': [
83
- -- {
84
- -- # All the normal keys for a job
85
- -- 'jid': ...,
86
- -- 'data': ...
87
- -- # The message for this particular instance
88
- -- 'message': ...,
89
- -- 'group': ...,
90
- -- }, ...
91
- -- ]
92
- -- }
93
- --
94
- function Qless.failed(group, start, limit)
95
- start = assert(tonumber(start or 0),
96
- 'Failed(): Arg "start" is not a number: ' .. (start or 'nil'))
97
- limit = assert(tonumber(limit or 25),
98
- 'Failed(): Arg "limit" is not a number: ' .. (limit or 'nil'))
99
-
100
- if group then
101
- -- If a group was provided, then we should do paginated lookup
102
- return {
103
- total = redis.call('llen', 'ql:f:' .. group),
104
- jobs = redis.call('lrange', 'ql:f:' .. group, start, limit - 1)
105
- }
106
- else
107
- -- Otherwise, we should just list all the known failure groups we have
108
- local response = {}
109
- local groups = redis.call('smembers', 'ql:failures')
110
- for index, group in ipairs(groups) do
111
- response[group] = redis.call('llen', 'ql:f:' .. group)
112
- end
113
- return response
114
- end
115
- end
116
-
117
- -- Jobs(0, now, 'complete' | (
118
- -- (
119
- -- 'stalled' | 'running' | 'scheduled' | 'depends', 'recurring'
120
- -- ), queue)
121
- -- [offset, [count]])
122
- -------------------------------------------------------------------------------
123
- --
124
- -- Return all the job ids currently considered to be in the provided state
125
- -- in a particular queue. The response is a list of job ids:
126
- --
127
- -- [
128
- -- jid1,
129
- -- jid2,
130
- -- ...
131
- -- ]
132
- function Qless.jobs(now, state, ...)
133
- assert(state, 'Jobs(): Arg "state" missing')
134
- if state == 'complete' then
135
- local offset = assert(tonumber(arg[1] or 0),
136
- 'Jobs(): Arg "offset" not a number: ' .. tostring(arg[1]))
137
- local count = assert(tonumber(arg[2] or 25),
138
- 'Jobs(): Arg "count" not a number: ' .. tostring(arg[2]))
139
- return redis.call('zrevrange', 'ql:completed', offset,
140
- offset + count - 1)
141
- else
142
- local name = assert(arg[1], 'Jobs(): Arg "queue" missing')
143
- local offset = assert(tonumber(arg[2] or 0),
144
- 'Jobs(): Arg "offset" not a number: ' .. tostring(arg[2]))
145
- local count = assert(tonumber(arg[3] or 25),
146
- 'Jobs(): Arg "count" not a number: ' .. tostring(arg[3]))
147
-
148
- local queue = Qless.queue(name)
149
- if state == 'running' then
150
- return queue.locks.peek(now, offset, count)
151
- elseif state == 'stalled' then
152
- return queue.locks.expired(now, offset, count)
153
- elseif state == 'scheduled' then
154
- return queue.scheduled.peek(now, offset, count)
155
- elseif state == 'depends' then
156
- return queue.depends.peek(now, offset, count)
157
- elseif state == 'recurring' then
158
- return queue.recurring.peek(now, offset, count)
159
- else
160
- error('Jobs(): Unknown type "' .. state .. '"')
161
- end
162
- end
163
- end
164
-
165
- -- Track(0)
166
- -- Track(0, ('track' | 'untrack'), jid, now)
167
- -- ------------------------------------------
168
- -- If no arguments are provided, it returns details of all currently-tracked
169
- -- jobs. If the first argument is 'track', then it will start tracking the job
170
- -- associated with that id, and 'untrack' stops tracking it. In this context,
171
- -- tracking is nothing more than saving the job to a list of jobs that are
172
- -- considered special.
173
- --
174
- -- {
175
- -- 'jobs': [
176
- -- {
177
- -- 'jid': ...,
178
- -- # All the other details you'd get from 'get'
179
- -- }, {
180
- -- ...
181
- -- }
182
- -- ], 'expired': [
183
- -- # These are all the jids that are completed and whose data expired
184
- -- 'deadbeef',
185
- -- ...,
186
- -- ...,
187
- -- ]
188
- -- }
189
- --
190
- function Qless.track(now, command, jid)
191
- if command ~= nil then
192
- assert(jid, 'Track(): Arg "jid" missing')
193
- if string.lower(ARGV[1]) == 'track' then
194
- Qless.publish('track', jid)
195
- return redis.call('zadd', 'ql:tracked', now, jid)
196
- elseif string.lower(ARGV[1]) == 'untrack' then
197
- Qless.publish('untrack', jid)
198
- return redis.call('zrem', 'ql:tracked', jid)
199
- else
200
- error('Track(): Unknown action "' .. command .. '"')
201
- end
202
- else
203
- local response = {
204
- jobs = {},
205
- expired = {}
206
- }
207
- local jids = redis.call('zrange', 'ql:tracked', 0, -1)
208
- for index, jid in ipairs(jids) do
209
- local data = Qless.job(jid):data()
210
- if data then
211
- table.insert(response.jobs, data)
212
- else
213
- table.insert(response.expired, jid)
214
- end
215
- end
216
- return response
217
- end
218
- end
219
-
220
- -- tag(0, now, ('add' | 'remove'), jid, tag, [tag, ...])
221
- -- tag(0, now, 'get', tag, [offset, [count]])
222
- -- tag(0, now, 'top', [offset, [count]])
223
- -- ------------------------------------------------------------------------------------------------------------------
224
- -- Accepts a jid, 'add' or 'remove', and then a list of tags
225
- -- to either add or remove from the job. Alternatively, 'get',
226
- -- a tag to get jobs associated with that tag, and offset and
227
- -- count
228
- --
229
- -- If 'add' or 'remove', the response is a list of the jobs
230
- -- current tags, or False if the job doesn't exist. If 'get',
231
- -- the response is of the form:
232
- --
233
- -- {
234
- -- total: ...,
235
- -- jobs: [
236
- -- jid,
237
- -- ...
238
- -- ]
239
- -- }
240
- --
241
- -- If 'top' is supplied, it returns the most commonly-used tags
242
- -- in a paginated fashion.
243
- function Qless.tag(now, command, ...)
244
- assert(command, 'Tag(): Arg "command" must be "add", "remove", "get" or "top"')
245
-
246
- if command == 'add' then
247
- local jid = assert(arg[1], 'Tag(): Arg "jid" missing')
248
- local tags = redis.call('hget', QlessJob.ns .. jid, 'tags')
249
- -- If the job has been canceled / deleted, then return false
250
- if tags then
251
- -- Decode the json blob, convert to dictionary
252
- tags = cjson.decode(tags)
253
- local _tags = {}
254
- for i,v in ipairs(tags) do _tags[v] = true end
255
-
256
- -- Otherwise, add the job to the sorted set with that tags
257
- for i=2,#arg do
258
- local tag = arg[i]
259
- if _tags[tag] == nil then
260
- table.insert(tags, tag)
261
- end
262
- redis.call('zadd', 'ql:t:' .. tag, now, jid)
263
- redis.call('zincrby', 'ql:tags', 1, tag)
264
- end
265
-
266
- tags = cjson.encode(tags)
267
- redis.call('hset', QlessJob.ns .. jid, 'tags', tags)
268
- return tags
269
- else
270
- return false
271
- end
272
- elseif command == 'remove' then
273
- local jid = assert(arg[1], 'Tag(): Arg "jid" missing')
274
- local tags = redis.call('hget', QlessJob.ns .. jid, 'tags')
275
- -- If the job has been canceled / deleted, then return false
276
- if tags then
277
- -- Decode the json blob, convert to dictionary
278
- tags = cjson.decode(tags)
279
- local _tags = {}
280
- for i,v in ipairs(tags) do _tags[v] = true end
281
-
282
- -- Otherwise, add the job to the sorted set with that tags
283
- for i=2,#arg do
284
- local tag = arg[i]
285
- _tags[tag] = nil
286
- redis.call('zrem', 'ql:t:' .. tag, jid)
287
- redis.call('zincrby', 'ql:tags', -1, tag)
288
- end
289
-
290
- local results = {}
291
- for i,tag in ipairs(tags) do if _tags[tag] then table.insert(results, tag) end end
292
-
293
- tags = cjson.encode(results)
294
- redis.call('hset', QlessJob.ns .. jid, 'tags', tags)
295
- return results
296
- else
297
- return false
298
- end
299
- elseif command == 'get' then
300
- local tag = assert(arg[1], 'Tag(): Arg "tag" missing')
301
- local offset = assert(tonumber(arg[2] or 0),
302
- 'Tag(): Arg "offset" not a number: ' .. tostring(arg[2]))
303
- local count = assert(tonumber(arg[3] or 25),
304
- 'Tag(): Arg "count" not a number: ' .. tostring(arg[3]))
305
- return {
306
- total = redis.call('zcard', 'ql:t:' .. tag),
307
- jobs = redis.call('zrange', 'ql:t:' .. tag, offset, count)
308
- }
309
- elseif command == 'top' then
310
- local offset = assert(tonumber(arg[1] or 0) , 'Tag(): Arg "offset" not a number: ' .. tostring(arg[1]))
311
- local count = assert(tonumber(arg[2] or 25), 'Tag(): Arg "count" not a number: ' .. tostring(arg[2]))
312
- return redis.call('zrevrangebyscore', 'ql:tags', '+inf', 2, 'limit', offset, count)
313
- else
314
- error('Tag(): First argument must be "add", "remove" or "get"')
315
- end
316
- end
317
-
318
- -- Cancel(0)
319
- -- --------------
320
- -- Cancel a job from taking place. It will be deleted from the system, and any
321
- -- attempts to renew a heartbeat will fail, and any attempts to complete it
322
- -- will fail. If you try to get the data on the object, you will get nothing.
323
- function Qless.cancel(...)
324
- -- Dependents is a mapping of a job to its dependent jids
325
- local dependents = {}
326
- for _, jid in ipairs(arg) do
327
- dependents[jid] = redis.call(
328
- 'smembers', QlessJob.ns .. jid .. '-dependents') or {}
329
- end
330
-
331
- -- Now, we'll loop through every jid we intend to cancel, and we'll go
332
- -- make sure that this operation will be ok
333
- for i, jid in ipairs(arg) do
334
- for j, dep in ipairs(dependents[jid]) do
335
- if dependents[dep] == nil then
336
- error('Cancel(): ' .. jid .. ' is a dependency of ' .. dep ..
337
- ' but is not mentioned to be canceled')
338
- end
339
- end
340
- end
341
-
342
- -- If we've made it this far, then we are good to go. We can now just
343
- -- remove any trace of all these jobs, as they form a dependent clique
344
- for _, jid in ipairs(arg) do
345
- -- Find any stage it's associated with and remove its from that stage
346
- local state, queue, failure, worker = unpack(redis.call(
347
- 'hmget', QlessJob.ns .. jid, 'state', 'queue', 'failure', 'worker'))
348
-
349
- if state ~= 'complete' then
350
- -- Send a message out on the appropriate channels
351
- local encoded = cjson.encode({
352
- jid = jid,
353
- worker = worker,
354
- event = 'canceled',
355
- queue = queue
356
- })
357
- Qless.publish('log', encoded)
358
-
359
- -- Remove this job from whatever worker has it, if any
360
- if worker and (worker ~= '') then
361
- redis.call('zrem', 'ql:w:' .. worker .. ':jobs', jid)
362
- -- If necessary, send a message to the appropriate worker, too
363
- Qless.publish('w:', worker, encoded)
364
- end
365
-
366
- -- Remove it from that queue
367
- if queue then
368
- local queue = Qless.queue(queue)
369
- queue.work.remove(jid)
370
- queue.locks.remove(jid)
371
- queue.scheduled.remove(jid)
372
- queue.depends.remove(jid)
373
- end
374
-
375
- -- We should probably go through all our dependencies and remove
376
- -- ourselves from the list of dependents
377
- for i, j in ipairs(redis.call(
378
- 'smembers', QlessJob.ns .. jid .. '-dependencies')) do
379
- redis.call('srem', QlessJob.ns .. j .. '-dependents', jid)
380
- end
381
-
382
- -- Delete any notion of dependencies it has
383
- redis.call('del', QlessJob.ns .. jid .. '-dependencies')
384
-
385
- -- If we're in the failed state, remove all of our data
386
- if state == 'failed' then
387
- failure = cjson.decode(failure)
388
- -- We need to make this remove it from the failed queues
389
- redis.call('lrem', 'ql:f:' .. failure.group, 0, jid)
390
- if redis.call('llen', 'ql:f:' .. failure.group) == 0 then
391
- redis.call('srem', 'ql:failures', failure.group)
392
- end
393
- end
394
-
395
- -- Remove it as a job that's tagged with this particular tag
396
- local tags = cjson.decode(
397
- redis.call('hget', QlessJob.ns .. jid, 'tags') or '{}')
398
- for i, tag in ipairs(tags) do
399
- redis.call('zrem', 'ql:t:' .. tag, jid)
400
- redis.call('zincrby', 'ql:tags', -1, tag)
401
- end
402
-
403
- -- If the job was being tracked, we should notify
404
- if redis.call('zscore', 'ql:tracked', jid) ~= false then
405
- Qless.publish('canceled', jid)
406
- end
407
-
408
- -- Just go ahead and delete our data
409
- redis.call('del', QlessJob.ns .. jid)
410
- redis.call('del', QlessJob.ns .. jid .. '-history')
411
- end
412
- end
413
-
414
- return arg
415
- end
416
-
417
- -------------------------------------------------------------------------------
418
- -- Configuration interactions
419
- -------------------------------------------------------------------------------
420
-
421
- -- This represents our default configuration settings
422
- Qless.config.defaults = {
423
- ['application'] = 'qless',
424
- ['heartbeat'] = 60,
425
- ['grace-period'] = 10,
426
- ['stats-history'] = 30,
427
- ['histogram-history'] = 7,
428
- ['jobs-history-count'] = 50000,
429
- ['jobs-history'] = 604800
430
- }
431
-
432
- -- Get one or more of the keys
433
- Qless.config.get = function(key, default)
434
- if key then
435
- return redis.call('hget', 'ql:config', key) or
436
- Qless.config.defaults[key] or default
437
- else
438
- -- Inspired by redis-lua https://github.com/nrk/redis-lua/blob/version-2.0/src/redis.lua
439
- local reply = redis.call('hgetall', 'ql:config')
440
- for i = 1, #reply, 2 do
441
- Qless.config.defaults[reply[i]] = reply[i + 1]
442
- end
443
- return Qless.config.defaults
444
- end
445
- end
446
-
447
- -- Set a configuration variable
448
- Qless.config.set = function(option, value)
449
- assert(option, 'config.set(): Arg "option" missing')
450
- assert(value , 'config.set(): Arg "value" missing')
451
- -- Send out a log message
452
- Qless.publish('log', cjson.encode({
453
- event = 'config_set',
454
- option = option,
455
- value = value
456
- }))
457
-
458
- redis.call('hset', 'ql:config', option, value)
459
- end
460
-
461
- -- Unset a configuration option
462
- Qless.config.unset = function(option)
463
- assert(option, 'config.unset(): Arg "option" missing')
464
- -- Send out a log message
465
- Qless.publish('log', cjson.encode({
466
- event = 'config_unset',
467
- option = option
468
- }))
469
-
470
- redis.call('hdel', 'ql:config', option)
471
- end
472
- -------------------------------------------------------------------------------
473
- -- Job Class
474
- --
475
- -- It returns an object that represents the job with the provided JID
476
- -------------------------------------------------------------------------------
477
-
478
- -- This gets all the data associated with the job with the provided id. If the
479
- -- job is not found, it returns nil. If found, it returns an object with the
480
- -- appropriate properties
481
- function QlessJob:data(...)
482
- local job = redis.call(
483
- 'hmget', QlessJob.ns .. self.jid, 'jid', 'klass', 'state', 'queue',
484
- 'worker', 'priority', 'expires', 'retries', 'remaining', 'data',
485
- 'tags', 'failure')
486
-
487
- -- Return nil if we haven't found it
488
- if not job[1] then
489
- return nil
490
- end
491
-
492
- local data = {
493
- jid = job[1],
494
- klass = job[2],
495
- state = job[3],
496
- queue = job[4],
497
- worker = job[5] or '',
498
- tracked = redis.call(
499
- 'zscore', 'ql:tracked', self.jid) ~= false,
500
- priority = tonumber(job[6]),
501
- expires = tonumber(job[7]) or 0,
502
- retries = tonumber(job[8]),
503
- remaining = math.floor(tonumber(job[9])),
504
- data = cjson.decode(job[10]),
505
- tags = cjson.decode(job[11]),
506
- history = self:history(),
507
- failure = cjson.decode(job[12] or '{}'),
508
- dependents = redis.call(
509
- 'smembers', QlessJob.ns .. self.jid .. '-dependents'),
510
- dependencies = redis.call(
511
- 'smembers', QlessJob.ns .. self.jid .. '-dependencies')
512
- }
513
-
514
- if #arg > 0 then
515
- -- This section could probably be optimized, but I wanted the interface
516
- -- in place first
517
- local response = {}
518
- for index, key in ipairs(arg) do
519
- table.insert(response, data[key])
520
- end
521
- return response
522
- else
523
- return data
524
- end
525
- end
526
-
527
- -- Complete a job and optionally put it in another queue, either scheduled or
528
- -- to be considered waiting immediately. It can also optionally accept other
529
- -- jids on which this job will be considered dependent before it's considered
530
- -- valid.
531
- --
532
- -- The variable-length arguments may be pairs of the form:
533
- --
534
- -- ('next' , queue) : The queue to advance it to next
535
- -- ('delay' , delay) : The delay for the next queue
536
- -- ('depends', : Json of jobs it depends on in the new queue
537
- -- '["jid1", "jid2", ...]')
538
- ---
539
- function QlessJob:complete(now, worker, queue, data, ...)
540
- assert(worker, 'Complete(): Arg "worker" missing')
541
- assert(queue , 'Complete(): Arg "queue" missing')
542
- data = assert(cjson.decode(data),
543
- 'Complete(): Arg "data" missing or not JSON: ' .. tostring(data))
544
-
545
- -- Read in all the optional parameters
546
- local options = {}
547
- for i = 1, #arg, 2 do options[arg[i]] = arg[i + 1] end
548
-
549
- -- Sanity check on optional args
550
- local nextq = options['next']
551
- local delay = assert(tonumber(options['delay'] or 0))
552
- local depends = assert(cjson.decode(options['depends'] or '[]'),
553
- 'Complete(): Arg "depends" not JSON: ' .. tostring(options['depends']))
554
-
555
- -- Delay and depends are not allowed together
556
- if delay > 0 and #depends > 0 then
557
- error('Complete(): "delay" and "depends" are not allowed together')
558
- end
559
-
560
- -- Depends doesn't make sense without nextq
561
- if options['delay'] and nextq == nil then
562
- error('Complete(): "delay" cannot be used without a "next".')
563
- end
564
-
565
- -- Depends doesn't make sense without nextq
566
- if options['depends'] and nextq == nil then
567
- error('Complete(): "depends" cannot be used without a "next".')
568
- end
569
-
570
- -- The bin is midnight of the provided day
571
- -- 24 * 60 * 60 = 86400
572
- local bin = now - (now % 86400)
573
-
574
- -- First things first, we should see if the worker still owns this job
575
- local lastworker, state, priority, retries = unpack(
576
- redis.call('hmget', QlessJob.ns .. self.jid, 'worker', 'state',
577
- 'priority', 'retries', 'dependents'))
578
-
579
- if lastworker == false then
580
- error('Complete(): Job does not exist')
581
- elseif (state ~= 'running') then
582
- error('Complete(): Job is not currently running: ' .. state)
583
- elseif lastworker ~= worker then
584
- error('Complete(): Job has been handed out to another worker: ' ..
585
- tostring(lastworker))
586
- end
587
-
588
- -- Now we can assume that the worker does own the job. We need to
589
- -- 1) Remove the job from the 'locks' from the old queue
590
- -- 2) Enqueue it in the next stage if necessary
591
- -- 3) Update the data
592
- -- 4) Mark the job as completed, remove the worker, remove expires, and
593
- -- update history
594
- self:history(now, 'done')
595
-
596
- if data then
597
- redis.call('hset', QlessJob.ns .. self.jid, 'data', cjson.encode(data))
598
- end
599
-
600
- -- Remove the job from the previous queue
601
- local queue_obj = Qless.queue(queue)
602
- queue_obj.work.remove(self.jid)
603
- queue_obj.locks.remove(self.jid)
604
- queue_obj.scheduled.remove(self.jid)
605
-
606
- ----------------------------------------------------------
607
- -- This is the massive stats update that we have to do
608
- ----------------------------------------------------------
609
- -- This is how long we've been waiting to get popped
610
- -- local waiting = math.floor(now) - history[#history]['popped']
611
- local waiting = 0
612
- Qless.queue(queue):stat(now, 'run', waiting)
613
-
614
- -- Remove this job from the jobs that the worker that was running it has
615
- redis.call('zrem', 'ql:w:' .. worker .. ':jobs', self.jid)
616
-
617
- if redis.call('zscore', 'ql:tracked', self.jid) ~= false then
618
- Qless.publish('completed', self.jid)
619
- end
620
-
621
- if nextq then
622
- queue_obj = Qless.queue(nextq)
623
- -- Send a message out to log
624
- Qless.publish('log', cjson.encode({
625
- jid = self.jid,
626
- event = 'advanced',
627
- queue = queue,
628
- to = nextq
629
- }))
630
-
631
- -- Enqueue the job
632
- self:history(now, 'put', {q = nextq})
633
-
634
- -- We're going to make sure that this queue is in the
635
- -- set of known queues
636
- if redis.call('zscore', 'ql:queues', nextq) == false then
637
- redis.call('zadd', 'ql:queues', now, nextq)
638
- end
639
-
640
- redis.call('hmset', QlessJob.ns .. self.jid,
641
- 'state', 'waiting',
642
- 'worker', '',
643
- 'failure', '{}',
644
- 'queue', nextq,
645
- 'expires', 0,
646
- 'remaining', tonumber(retries))
647
-
648
- if delay > 0 then
649
- queue_obj.scheduled.add(now + delay, self.jid)
650
- return 'scheduled'
651
- else
652
- -- These are the jids we legitimately have to wait on
653
- local count = 0
654
- for i, j in ipairs(depends) do
655
- -- Make sure it's something other than 'nil' or complete.
656
- local state = redis.call('hget', QlessJob.ns .. j, 'state')
657
- if (state and state ~= 'complete') then
658
- count = count + 1
659
- redis.call(
660
- 'sadd', QlessJob.ns .. j .. '-dependents',self.jid)
661
- redis.call(
662
- 'sadd', QlessJob.ns .. self.jid .. '-dependencies', j)
663
- end
664
- end
665
- if count > 0 then
666
- queue_obj.depends.add(now, self.jid)
667
- redis.call('hset', QlessJob.ns .. self.jid, 'state', 'depends')
668
- return 'depends'
669
- else
670
- queue_obj.work.add(now, priority, self.jid)
671
- return 'waiting'
672
- end
673
- end
674
- else
675
- -- Send a message out to log
676
- Qless.publish('log', cjson.encode({
677
- jid = self.jid,
678
- event = 'completed',
679
- queue = queue
680
- }))
681
-
682
- redis.call('hmset', QlessJob.ns .. self.jid,
683
- 'state', 'complete',
684
- 'worker', '',
685
- 'failure', '{}',
686
- 'queue', '',
687
- 'expires', 0,
688
- 'remaining', tonumber(retries))
689
-
690
- -- Do the completion dance
691
- local count = Qless.config.get('jobs-history-count')
692
- local time = Qless.config.get('jobs-history')
693
-
694
- -- These are the default values
695
- count = tonumber(count or 50000)
696
- time = tonumber(time or 7 * 24 * 60 * 60)
697
-
698
- -- Schedule this job for destructination eventually
699
- redis.call('zadd', 'ql:completed', now, self.jid)
700
-
701
- -- Now look at the expired job data. First, based on the current time
702
- local jids = redis.call('zrangebyscore', 'ql:completed', 0, now - time)
703
- -- Any jobs that need to be expired... delete
704
- for index, jid in ipairs(jids) do
705
- local tags = cjson.decode(redis.call('hget', QlessJob.ns .. jid, 'tags') or '{}')
706
- for i, tag in ipairs(tags) do
707
- redis.call('zrem', 'ql:t:' .. tag, jid)
708
- redis.call('zincrby', 'ql:tags', -1, tag)
709
- end
710
- redis.call('del', QlessJob.ns .. jid)
711
- redis.call('del', QlessJob.ns .. jid .. '-history')
712
- end
713
- -- And now remove those from the queued-for-cleanup queue
714
- redis.call('zremrangebyscore', 'ql:completed', 0, now - time)
715
-
716
- -- Now take the all by the most recent 'count' ids
717
- jids = redis.call('zrange', 'ql:completed', 0, (-1-count))
718
- for index, jid in ipairs(jids) do
719
- local tags = cjson.decode(redis.call('hget', QlessJob.ns .. jid, 'tags') or '{}')
720
- for i, tag in ipairs(tags) do
721
- redis.call('zrem', 'ql:t:' .. tag, jid)
722
- redis.call('zincrby', 'ql:tags', -1, tag)
723
- end
724
- redis.call('del', QlessJob.ns .. jid)
725
- redis.call('del', QlessJob.ns .. jid .. '-history')
726
- end
727
- redis.call('zremrangebyrank', 'ql:completed', 0, (-1-count))
728
-
729
- -- Alright, if this has any dependents, then we should go ahead
730
- -- and unstick those guys.
731
- for i, j in ipairs(redis.call('smembers', QlessJob.ns .. self.jid .. '-dependents')) do
732
- redis.call('srem', QlessJob.ns .. j .. '-dependencies', self.jid)
733
- if redis.call('scard', QlessJob.ns .. j .. '-dependencies') == 0 then
734
- local q, p = unpack(redis.call('hmget', QlessJob.ns .. j, 'queue', 'priority'))
735
- if q then
736
- local queue = Qless.queue(q)
737
- queue.depends.remove(j)
738
- queue.work.add(now, p, j)
739
- redis.call('hset', QlessJob.ns .. j, 'state', 'waiting')
740
- end
741
- end
742
- end
743
-
744
- -- Delete our dependents key
745
- redis.call('del', QlessJob.ns .. self.jid .. '-dependents')
746
-
747
- return 'complete'
748
- end
749
- end
750
-
751
- -- Fail(jid, worker, group, message, now, [data])
752
- -- -------------------------------------------------
753
- -- Mark the particular job as failed, with the provided group, and a more
754
- -- specific message. By `group`, we mean some phrase that might be one of
755
- -- several categorical modes of failure. The `message` is something more
756
- -- job-specific, like perhaps a traceback.
757
- --
758
- -- This method should __not__ be used to note that a job has been dropped or
759
- -- has failed in a transient way. This method __should__ be used to note that
760
- -- a job has something really wrong with it that must be remedied.
761
- --
762
- -- The motivation behind the `group` is so that similar errors can be grouped
763
- -- together. Optionally, updated data can be provided for the job. A job in
764
- -- any state can be marked as failed. If it has been given to a worker as a
765
- -- job, then its subsequent requests to heartbeat or complete that job will
766
- -- fail. Failed jobs are kept until they are canceled or completed.
767
- --
768
- -- __Returns__ the id of the failed job if successful, or `False` on failure.
769
- --
770
- -- Args:
771
- -- 1) jid
772
- -- 2) worker
773
- -- 3) group
774
- -- 4) message
775
- -- 5) the current time
776
- -- 6) [data]
777
- function QlessJob:fail(now, worker, group, message, data)
778
- local worker = assert(worker , 'Fail(): Arg "worker" missing')
779
- local group = assert(group , 'Fail(): Arg "group" missing')
780
- local message = assert(message , 'Fail(): Arg "message" missing')
781
-
782
- -- The bin is midnight of the provided day
783
- -- 24 * 60 * 60 = 86400
784
- local bin = now - (now % 86400)
785
-
786
- if data then
787
- data = cjson.decode(data)
788
- end
789
-
790
- -- First things first, we should get the history
791
- local queue, state = unpack(redis.call('hmget', QlessJob.ns .. self.jid,
792
- 'queue', 'state'))
793
-
794
- -- If the job has been completed, we cannot fail it
795
- if state ~= 'running' then
796
- error('Fail(): Job not currently running: ' .. state)
797
- end
798
-
799
- -- Send out a log message
800
- Qless.publish('log', cjson.encode({
801
- jid = self.jid,
802
- event = 'failed',
803
- worker = worker,
804
- group = group,
805
- message = message
806
- }))
807
-
808
- if redis.call('zscore', 'ql:tracked', self.jid) ~= false then
809
- Qless.publish('failed', self.jid)
810
- end
811
-
812
- -- Remove this job from the jobs that the worker that was running it has
813
- redis.call('zrem', 'ql:w:' .. worker .. ':jobs', self.jid)
814
-
815
- -- Now, take the element of the history for which our provided worker is the worker, and update 'failed'
816
- self:history(now, 'failed', {worker = worker, group = group})
817
-
818
- -- Increment the number of failures for that queue for the
819
- -- given day.
820
- redis.call('hincrby', 'ql:s:stats:' .. bin .. ':' .. queue, 'failures', 1)
821
- redis.call('hincrby', 'ql:s:stats:' .. bin .. ':' .. queue, 'failed' , 1)
822
-
823
- -- Now remove the instance from the schedule, and work queues for the queue it's in
824
- local queue_obj = Qless.queue(queue)
825
- queue_obj.work.remove(self.jid)
826
- queue_obj.locks.remove(self.jid)
827
- queue_obj.scheduled.remove(self.jid)
828
-
829
- -- The reason that this appears here is that the above will fail if the job doesn't exist
830
- if data then
831
- redis.call('hset', QlessJob.ns .. self.jid, 'data', cjson.encode(data))
832
- end
833
-
834
- redis.call('hmset', QlessJob.ns .. self.jid,
835
- 'state', 'failed',
836
- 'worker', '',
837
- 'expires', '',
838
- 'failure', cjson.encode({
839
- ['group'] = group,
840
- ['message'] = message,
841
- ['when'] = math.floor(now),
842
- ['worker'] = worker
843
- }))
844
-
845
- -- Add this group of failure to the list of failures
846
- redis.call('sadd', 'ql:failures', group)
847
- -- And add this particular instance to the failed groups
848
- redis.call('lpush', 'ql:f:' .. group, self.jid)
849
-
850
- -- Here is where we'd intcrement stats about the particular stage
851
- -- and possibly the workers
852
-
853
- return self.jid
854
- end
855
-
856
- -- retry(0, now, queue, worker, [delay])
857
- -- ------------------------------------------
858
- -- This script accepts jid, queue, worker and delay for
859
- -- retrying a job. This is similar in functionality to
860
- -- `put`, except that this counts against the retries
861
- -- a job has for a stage.
862
- --
863
- -- If the worker is not the worker with a lock on the job,
864
- -- then it returns false. If the job is not actually running,
865
- -- then it returns false. Otherwise, it returns the number
866
- -- of retries remaining. If the allowed retries have been
867
- -- exhausted, then it is automatically failed, and a negative
868
- -- number is returned.
869
- function QlessJob:retry(now, queue, worker, delay, group, message)
870
- assert(queue , 'Retry(): Arg "queue" missing')
871
- assert(worker, 'Retry(): Arg "worker" missing')
872
- delay = assert(tonumber(delay or 0),
873
- 'Retry(): Arg "delay" not a number: ' .. tostring(delay))
874
-
875
- -- Let's see what the old priority, and tags were
876
- local oldqueue, state, retries, oldworker, priority, failure = unpack(redis.call('hmget', QlessJob.ns .. self.jid, 'queue', 'state', 'retries', 'worker', 'priority', 'failure'))
877
-
878
- -- If this isn't the worker that owns
879
- if oldworker == false then
880
- error('Retry(): Job does not exist')
881
- elseif state ~= 'running' then
882
- error('Retry(): Job is not currently running: ' .. state)
883
- elseif oldworker ~= worker then
884
- error('Retry(): Job has been handed out to another worker: ' .. oldworker)
885
- end
886
-
887
- -- For each of these, decrement their retries. If any of them
888
- -- have exhausted their retries, then we should mark them as
889
- -- failed.
890
- local remaining = tonumber(redis.call(
891
- 'hincrbyfloat', QlessJob.ns .. self.jid, 'remaining', -0.5))
892
- if (remaining * 2) % 2 == 1 then
893
- local remaining = tonumber(redis.call(
894
- 'hincrbyfloat', QlessJob.ns .. self.jid, 'remaining', -0.5))
895
- end
896
-
897
- -- Remove it from the locks key of the old queue
898
- Qless.queue(oldqueue).locks.remove(self.jid)
899
-
900
- -- Remove this job from the worker that was previously working it
901
- redis.call('zrem', 'ql:w:' .. worker .. ':jobs', self.jid)
902
-
903
- if remaining < 0 then
904
- -- Now remove the instance from the schedule, and work queues for the queue it's in
905
- local group = 'failed-retries-' .. queue
906
- self:history(now, 'failed', {group = group})
907
-
908
- redis.call('hmset', QlessJob.ns .. self.jid, 'state', 'failed',
909
- 'worker', '',
910
- 'expires', '')
911
- -- If the failure has not already been set, then set it
912
- if failure == {} then
913
- redis.call('hset', QlessJob.ns .. self.jid,
914
- 'failure', cjson.encode({
915
- ['group'] = group,
916
- ['message'] =
917
- 'Job exhausted retries in queue "' .. self.name .. '"',
918
- ['when'] = now,
919
- ['worker'] = unpack(job:data('worker'))
920
- }))
921
- end
922
-
923
- -- Add this type of failure to the list of failures
924
- redis.call('sadd', 'ql:failures', group)
925
- -- And add this particular instance to the failed types
926
- redis.call('lpush', 'ql:f:' .. group, self.jid)
927
- else
928
- -- Put it in the queue again with a delay. Like put()
929
- local queue_obj = Qless.queue(queue)
930
- if delay > 0 then
931
- queue_obj.scheduled.add(now + delay, self.jid)
932
- redis.call('hset', QlessJob.ns .. self.jid, 'state', 'scheduled')
933
- else
934
- queue_obj.work.add(now, priority, self.jid)
935
- redis.call('hset', QlessJob.ns .. self.jid, 'state', 'waiting')
936
- end
937
-
938
- -- If a group and a message was provided, then we should save it
939
- if group ~= nil and message ~= nil then
940
- redis.call('hset', QlessJob.ns .. self.jid,
941
- 'failure', cjson.encode({
942
- ['group'] = group,
943
- ['message'] = message,
944
- ['when'] = math.floor(now),
945
- ['worker'] = worker
946
- })
947
- )
948
- end
949
- end
950
-
951
- return math.floor(remaining)
952
- end
953
-
954
- -- Depends(0, jid,
955
- -- ('on', [jid, [jid, [...]]]) |
956
- -- ('off',
957
- -- ('all' | [jid, [jid, [...]]]))
958
- -------------------------------------------------------------------------------
959
- -- Add or remove dependencies a job has. If 'on' is provided, the provided
960
- -- jids are added as dependencies. If 'off' and 'all' are provided, then all
961
- -- the current dependencies are removed. If 'off' is provided and the next
962
- -- argument is not 'all', then those jids are removed as dependencies.
963
- --
964
- -- If a job is not already in the 'depends' state, then this call will return
965
- -- false. Otherwise, it will return true
966
- --
967
- -- Args:
968
- -- 1) jid
969
- function QlessJob:depends(now, command, ...)
970
- assert(command, 'Depends(): Arg "command" missing')
971
- if redis.call('hget', QlessJob.ns .. self.jid, 'state') ~= 'depends' then
972
- return false
973
- end
974
-
975
- if command == 'on' then
976
- -- These are the jids we legitimately have to wait on
977
- for i, j in ipairs(arg) do
978
- -- Make sure it's something other than 'nil' or complete.
979
- local state = redis.call('hget', QlessJob.ns .. j, 'state')
980
- if (state and state ~= 'complete') then
981
- redis.call('sadd', QlessJob.ns .. j .. '-dependents' , self.jid)
982
- redis.call('sadd', QlessJob.ns .. self.jid .. '-dependencies', j)
983
- end
984
- end
985
- return true
986
- elseif command == 'off' then
987
- if arg[1] == 'all' then
988
- for i, j in ipairs(redis.call('smembers', QlessJob.ns .. self.jid .. '-dependencies')) do
989
- redis.call('srem', QlessJob.ns .. j .. '-dependents', self.jid)
990
- end
991
- redis.call('del', QlessJob.ns .. self.jid .. '-dependencies')
992
- local q, p = unpack(redis.call('hmget', QlessJob.ns .. self.jid, 'queue', 'priority'))
993
- if q then
994
- local queue_obj = Qless.queue(q)
995
- queue_obj.depends.remove(self.jid)
996
- queue_obj.work.add(now, p, self.jid)
997
- redis.call('hset', QlessJob.ns .. self.jid, 'state', 'waiting')
998
- end
999
- else
1000
- for i, j in ipairs(arg) do
1001
- redis.call('srem', QlessJob.ns .. j .. '-dependents', self.jid)
1002
- redis.call('srem', QlessJob.ns .. self.jid .. '-dependencies', j)
1003
- if redis.call('scard', QlessJob.ns .. self.jid .. '-dependencies') == 0 then
1004
- local q, p = unpack(redis.call('hmget', QlessJob.ns .. self.jid, 'queue', 'priority'))
1005
- if q then
1006
- local queue_obj = Qless.queue(q)
1007
- queue_obj.depends.remove(self.jid)
1008
- queue_obj.work.add(now, p, self.jid)
1009
- redis.call('hset', QlessJob.ns .. self.jid, 'state', 'waiting')
1010
- end
1011
- end
1012
- end
1013
- end
1014
- return true
1015
- else
1016
- error('Depends(): Argument "command" must be "on" or "off"')
1017
- end
1018
- end
1019
-
1020
- -- This scripts conducts a heartbeat for a job, and returns
1021
- -- either the new expiration or False if the lock has been
1022
- -- given to another node
1023
- --
1024
- -- Args:
1025
- -- 1) now
1026
- -- 2) worker
1027
- -- 3) [data]
1028
- function QlessJob:heartbeat(now, worker, data)
1029
- assert(worker, 'Heatbeat(): Arg "worker" missing')
1030
-
1031
- -- We should find the heartbeat interval for this queue
1032
- -- heartbeat. First, though, we need to find the queue
1033
- -- this particular job is in
1034
- local queue = redis.call('hget', QlessJob.ns .. self.jid, 'queue') or ''
1035
- local expires = now + tonumber(
1036
- Qless.config.get(queue .. '-heartbeat') or
1037
- Qless.config.get('heartbeat', 60))
1038
-
1039
- if data then
1040
- data = cjson.decode(data)
1041
- end
1042
-
1043
- -- First, let's see if the worker still owns this job, and there is a worker
1044
- local job_worker, state = unpack(redis.call('hmget', QlessJob.ns .. self.jid, 'worker', 'state'))
1045
- if job_worker == false then
1046
- -- This means the job doesn't exist
1047
- error('Heartbeat(): Job does not exist')
1048
- elseif state ~= 'running' then
1049
- error('Heartbeat(): Job not currently running: ' .. state)
1050
- elseif job_worker ~= worker or #job_worker == 0 then
1051
- error('Heartbeat(): Job has been handed out to another worker: ' .. job_worker)
1052
- else
1053
- -- Otherwise, optionally update the user data, and the heartbeat
1054
- if data then
1055
- -- I don't know if this is wise, but I'm decoding and encoding
1056
- -- the user data to hopefully ensure its sanity
1057
- redis.call('hmset', QlessJob.ns .. self.jid, 'expires', expires, 'worker', worker, 'data', cjson.encode(data))
1058
- else
1059
- redis.call('hmset', QlessJob.ns .. self.jid, 'expires', expires, 'worker', worker)
1060
- end
1061
-
1062
- -- Update hwen this job was last updated on that worker
1063
- -- Add this job to the list of jobs handled by this worker
1064
- redis.call('zadd', 'ql:w:' .. worker .. ':jobs', expires, self.jid)
1065
-
1066
- -- And now we should just update the locks
1067
- local queue = Qless.queue(redis.call('hget', QlessJob.ns .. self.jid, 'queue'))
1068
- queue.locks.add(expires, self.jid)
1069
- return expires
1070
- end
1071
- end
1072
-
1073
- -- priority(0, jid, priority)
1074
- -- --------------------------
1075
- -- Accepts a jid, and a new priority for the job. If the job
1076
- -- doesn't exist, then return false. Otherwise, return the
1077
- -- updated priority. If the job is waiting, then the change
1078
- -- will be reflected in the order in which it's popped
1079
- function QlessJob:priority(priority)
1080
- priority = assert(tonumber(priority),
1081
- 'Priority(): Arg "priority" missing or not a number: ' .. tostring(priority))
1082
-
1083
- -- Get the queue the job is currently in, if any
1084
- local queue = redis.call('hget', QlessJob.ns .. self.jid, 'queue')
1085
-
1086
- if queue == nil then
1087
- return false
1088
- elseif queue == '' then
1089
- -- Just adjust the priority
1090
- redis.call('hset', QlessJob.ns .. self.jid, 'priority', priority)
1091
- return priority
1092
- else
1093
- -- Adjust the priority and see if it's a candidate for updating
1094
- -- its priority in the queue it's currently in
1095
- local queue_obj = Qless.queue(queue)
1096
- if queue_obj.work.score(self.jid) then
1097
- queue_obj.work.add(0, priority, self.jid)
1098
- end
1099
- redis.call('hset', QlessJob.ns .. self.jid, 'priority', priority)
1100
- return priority
1101
- end
1102
- end
1103
-
1104
- -- Update the jobs' attributes with the provided dictionary
1105
- function QlessJob:update(data)
1106
- local tmp = {}
1107
- for k, v in pairs(data) do
1108
- table.insert(tmp, k)
1109
- table.insert(tmp, v)
1110
- end
1111
- redis.call('hmset', QlessJob.ns .. self.jid, unpack(tmp))
1112
- end
1113
-
1114
- -- Times out the job
1115
- function QlessJob:timeout(now)
1116
- local queue_name, state, worker = unpack(redis.call('hmget',
1117
- QlessJob.ns .. self.jid, 'queue', 'state', 'worker'))
1118
- if queue_name == nil then
1119
- error('Timeout(): Job does not exist')
1120
- elseif state ~= 'running' then
1121
- error('Timeout(): Job not running')
1122
- else
1123
- -- Time out the job
1124
- self:history(now, 'timed-out')
1125
- local queue = Qless.queue(queue_name)
1126
- queue.locks.remove(self.jid)
1127
- queue.work.add(now, math.huge, self.jid)
1128
- redis.call('hmset', QlessJob.ns .. self.jid,
1129
- 'state', 'stalled', 'expires', 0)
1130
- local encoded = cjson.encode({
1131
- jid = self.jid,
1132
- event = 'lock_lost',
1133
- worker = worker
1134
- })
1135
- Qless.publish('w:' .. worker, encoded)
1136
- Qless.publish('log', encoded)
1137
- return queue
1138
- end
1139
- end
1140
-
1141
- -- Get or append to history
1142
- function QlessJob:history(now, what, item)
1143
- -- First, check if there's an old-style history, and update it if there is
1144
- local history = redis.call('hget', QlessJob.ns .. self.jid, 'history')
1145
- if history then
1146
- history = cjson.decode(history)
1147
- for i, value in ipairs(history) do
1148
- redis.call('rpush', QlessJob.ns .. self.jid .. '-history',
1149
- cjson.encode({math.floor(value.put), 'put', {q = value.q}}))
1150
-
1151
- -- If there's any popped time
1152
- if value.popped then
1153
- redis.call('rpush', QlessJob.ns .. self.jid .. '-history',
1154
- cjson.encode({math.floor(value.popped), 'popped',
1155
- {worker = value.worker}}))
1156
- end
1157
-
1158
- -- If there's any failure
1159
- if value.failed then
1160
- redis.call('rpush', QlessJob.ns .. self.jid .. '-history',
1161
- cjson.encode(
1162
- {math.floor(value.failed), 'failed', nil}))
1163
- end
1164
-
1165
- -- If it was completed
1166
- if value.done then
1167
- redis.call('rpush', QlessJob.ns .. self.jid .. '-history',
1168
- cjson.encode(
1169
- {math.floor(value.done), 'done', nil}))
1170
- end
1171
- end
1172
- -- With all this ported forward, delete the old-style history
1173
- redis.call('hdel', QlessJob.ns .. self.jid, 'history')
1174
- end
1175
-
1176
- -- Now to the meat of the function
1177
- if what == nil then
1178
- -- Get the history
1179
- local response = {}
1180
- for i, value in ipairs(redis.call('lrange',
1181
- QlessJob.ns .. self.jid .. '-history', 0, -1)) do
1182
- value = cjson.decode(value)
1183
- local dict = value[3] or {}
1184
- dict['when'] = value[1]
1185
- dict['what'] = value[2]
1186
- table.insert(response, dict)
1187
- end
1188
- return response
1189
- else
1190
- -- Append to the history
1191
- return redis.call('rpush', QlessJob.ns .. self.jid .. '-history',
1192
- cjson.encode({math.floor(now), what, item}))
1193
- end
1194
- end
1195
- -------------------------------------------------------------------------------
1196
- -- Queue class
1197
- -------------------------------------------------------------------------------
1198
- -- Return a queue object
1199
- function Qless.queue(name)
1200
- assert(name, 'Queue(): no queue name provided')
1201
- local queue = {}
1202
- setmetatable(queue, QlessQueue)
1203
- queue.name = name
1204
-
1205
- -- Access to our work
1206
- queue.work = {
1207
- peek = function(count)
1208
- if count == 0 then
1209
- return {}
1210
- end
1211
- local jids = {}
1212
- for index, jid in ipairs(redis.call(
1213
- 'zrevrange', queue:prefix('work'), 0, count - 1)) do
1214
- table.insert(jids, jid)
1215
- end
1216
- return jids
1217
- end, remove = function(...)
1218
- if #arg > 0 then
1219
- return redis.call('zrem', queue:prefix('work'), unpack(arg))
1220
- end
1221
- end, add = function(now, priority, jid)
1222
- return redis.call('zadd',
1223
- queue:prefix('work'), priority - (now / 10000000000), jid)
1224
- end, score = function(jid)
1225
- return redis.call('zscore', queue:prefix('work'), jid)
1226
- end, length = function()
1227
- return redis.call('zcard', queue:prefix('work'))
1228
- end
1229
- }
1230
-
1231
- -- Access to our locks
1232
- queue.locks = {
1233
- expired = function(now, offset, count)
1234
- return redis.call('zrangebyscore',
1235
- queue:prefix('locks'), -math.huge, now, 'LIMIT', offset, count)
1236
- end, peek = function(now, offset, count)
1237
- return redis.call('zrangebyscore', queue:prefix('locks'),
1238
- now, math.huge, 'LIMIT', offset, count)
1239
- end, add = function(expires, jid)
1240
- redis.call('zadd', queue:prefix('locks'), expires, jid)
1241
- end, remove = function(...)
1242
- if #arg > 0 then
1243
- return redis.call('zrem', queue:prefix('locks'), unpack(arg))
1244
- end
1245
- end, running = function(now)
1246
- return redis.call('zcount', queue:prefix('locks'), now, math.huge)
1247
- end, length = function(now)
1248
- -- If a 'now' is provided, we're interested in how many are before
1249
- -- that time
1250
- if now then
1251
- return redis.call('zcount', queue:prefix('locks'), 0, now)
1252
- else
1253
- return redis.call('zcard', queue:prefix('locks'))
1254
- end
1255
- end
1256
- }
1257
-
1258
- -- Access to our dependendant jobs
1259
- queue.depends = {
1260
- peek = function(now, offset, count)
1261
- return redis.call('zrange',
1262
- queue:prefix('depends'), offset, offset + count - 1)
1263
- end, add = function(now, jid)
1264
- redis.call('zadd', queue:prefix('depends'), now, jid)
1265
- end, remove = function(...)
1266
- if #arg > 0 then
1267
- return redis.call('zrem', queue:prefix('depends'), unpack(arg))
1268
- end
1269
- end, length = function()
1270
- return redis.call('zcard', queue:prefix('depends'))
1271
- end
1272
- }
1273
-
1274
- -- Access to our scheduled jobs
1275
- queue.scheduled = {
1276
- peek = function(now, offset, count)
1277
- return redis.call('zrange',
1278
- queue:prefix('scheduled'), offset, offset + count - 1)
1279
- end, ready = function(now, offset, count)
1280
- return redis.call('zrangebyscore',
1281
- queue:prefix('scheduled'), 0, now, 'LIMIT', offset, count)
1282
- end, add = function(when, jid)
1283
- redis.call('zadd', queue:prefix('scheduled'), when, jid)
1284
- end, remove = function(...)
1285
- if #arg > 0 then
1286
- return redis.call('zrem', queue:prefix('scheduled'), unpack(arg))
1287
- end
1288
- end, length = function()
1289
- return redis.call('zcard', queue:prefix('scheduled'))
1290
- end
1291
- }
1292
-
1293
- -- Access to our recurring jobs
1294
- queue.recurring = {
1295
- peek = function(now, offset, count)
1296
- return redis.call('zrangebyscore', queue:prefix('recur'),
1297
- 0, now, 'LIMIT', offset, count)
1298
- end, ready = function(now, offset, count)
1299
- end, add = function(when, jid)
1300
- redis.call('zadd', queue:prefix('recur'), when, jid)
1301
- end, remove = function(...)
1302
- if #arg > 0 then
1303
- return redis.call('zrem', queue:prefix('recur'), unpack(arg))
1304
- end
1305
- end, update = function(increment, jid)
1306
- redis.call('zincrby', queue:prefix('recur'), increment, jid)
1307
- end, score = function(jid)
1308
- return redis.call('zscore', queue:prefix('recur'), jid)
1309
- end, length = function()
1310
- return redis.call('zcard', queue:prefix('recur'))
1311
- end
1312
- }
1313
- return queue
1314
- end
1315
-
1316
- --! @brief Return the prefix for this particular queue
1317
- function QlessQueue:prefix(group)
1318
- if group then
1319
- return QlessQueue.ns..self.name..'-'..group
1320
- else
1321
- return QlessQueue.ns..self.name
1322
- end
1323
- end
1324
-
1325
- -- Stats(0, queue, date)
1326
- -- ---------------------
1327
- -- Return the current statistics for a given queue on a given date. The
1328
- -- results are returned are a JSON blob:
1329
- --
1330
- --
1331
- -- {
1332
- -- # These are unimplemented as of yet
1333
- -- 'failed': 3,
1334
- -- 'retries': 5,
1335
- -- 'wait' : {
1336
- -- 'total' : ...,
1337
- -- 'mean' : ...,
1338
- -- 'variance' : ...,
1339
- -- 'histogram': [
1340
- -- ...
1341
- -- ]
1342
- -- }, 'run': {
1343
- -- 'total' : ...,
1344
- -- 'mean' : ...,
1345
- -- 'variance' : ...,
1346
- -- 'histogram': [
1347
- -- ...
1348
- -- ]
1349
- -- }
1350
- -- }
1351
- --
1352
- -- The histogram's data points are at the second resolution for the first
1353
- -- minute, the minute resolution for the first hour, the 15-minute resolution
1354
- -- for the first day, the hour resolution for the first 3 days, and then at
1355
- -- the day resolution from there on out. The `histogram` key is a list of
1356
- -- those values.
1357
- --
1358
- -- Args:
1359
- -- 1) queue
1360
- -- 2) time
1361
- function QlessQueue:stats(now, date)
1362
- date = assert(tonumber(date),
1363
- 'Stats(): Arg "date" missing or not a number: '.. (date or 'nil'))
1364
-
1365
- -- The bin is midnight of the provided day
1366
- -- 24 * 60 * 60 = 86400
1367
- local bin = date - (date % 86400)
1368
-
1369
- -- This a table of all the keys we want to use in order to produce a histogram
1370
- local histokeys = {
1371
- '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',
1372
- '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',
1373
- 'h1','h2','h3','h4','h5','h6','h7','h8','h9','h10','h11','h12','h13','h14','h15','h16','h17','h18','h19','h20','h21','h22','h23',
1374
- 'd1','d2','d3','d4','d5','d6'
1375
- }
1376
-
1377
- local mkstats = function(name, bin, queue)
1378
- -- The results we'll be sending back
1379
- local results = {}
1380
-
1381
- local key = 'ql:s:' .. name .. ':' .. bin .. ':' .. queue
1382
- local count, mean, vk = unpack(redis.call('hmget', key, 'total', 'mean', 'vk'))
1383
-
1384
- count = tonumber(count) or 0
1385
- mean = tonumber(mean) or 0
1386
- vk = tonumber(vk)
1387
-
1388
- results.count = count or 0
1389
- results.mean = mean or 0
1390
- results.histogram = {}
1391
-
1392
- if not count then
1393
- results.std = 0
1394
- else
1395
- if count > 1 then
1396
- results.std = math.sqrt(vk / (count - 1))
1397
- else
1398
- results.std = 0
1399
- end
1400
- end
1401
-
1402
- local histogram = redis.call('hmget', key, unpack(histokeys))
1403
- for i=1,#histokeys do
1404
- table.insert(results.histogram, tonumber(histogram[i]) or 0)
1405
- end
1406
- return results
1407
- end
1408
-
1409
- local retries, failed, failures = unpack(redis.call('hmget', 'ql:s:stats:' .. bin .. ':' .. self.name, 'retries', 'failed', 'failures'))
1410
- return {
1411
- retries = tonumber(retries or 0),
1412
- failed = tonumber(failed or 0),
1413
- failures = tonumber(failures or 0),
1414
- wait = mkstats('wait', bin, self.name),
1415
- run = mkstats('run' , bin, self.name)
1416
- }
1417
- end
1418
-
1419
- -- This script takes the name of the queue and then checks
1420
- -- for any expired locks, then inserts any scheduled items
1421
- -- that are now valid, and lastly returns any work items
1422
- -- that can be handed over.
1423
- --
1424
- -- Keys:
1425
- -- 1) queue name
1426
- -- Args:
1427
- -- 1) the number of items to return
1428
- -- 2) the current time
1429
- function QlessQueue:peek(now, count)
1430
- count = assert(tonumber(count),
1431
- 'Peek(): Arg "count" missing or not a number: ' .. tostring(count))
1432
-
1433
- -- These are the ids that we're going to return. We'll begin with any jobs
1434
- -- that have lost their locks
1435
- local jids = self.locks.expired(now, 0, count)
1436
-
1437
- -- If we still need jobs in order to meet demand, then we should
1438
- -- look for all the recurring jobs that need jobs run
1439
- self:check_recurring(now, count - #jids)
1440
-
1441
- -- Now we've checked __all__ the locks for this queue the could
1442
- -- have expired, and are no more than the number requested. If
1443
- -- we still need values in order to meet the demand, then we
1444
- -- should check if any scheduled items, and if so, we should
1445
- -- insert them to ensure correctness when pulling off the next
1446
- -- unit of work.
1447
- self:check_scheduled(now, count - #jids)
1448
-
1449
- -- With these in place, we can expand this list of jids based on the work
1450
- -- queue itself and the priorities therein
1451
- table.extend(jids, self.work.peek(count - #jids))
1452
-
1453
- return jids
1454
- end
1455
-
1456
- --! @brief Return true if this queue is paused
1457
- function QlessQueue:paused()
1458
- return redis.call('sismember', 'ql:paused_queues', self.name) == 1
1459
- end
1460
-
1461
- -- This script takes the name of the queue(s) and adds it
1462
- -- to the ql:paused_queues set.
1463
- --
1464
- -- Args: The list of queues to pause.
1465
- --
1466
- -- Note: long term, we have discussed adding a rate-limiting
1467
- -- feature to qless-core, which would be more flexible and
1468
- -- could be used for pausing (i.e. pause = set the rate to 0).
1469
- -- For now, this is far simpler, but we should rewrite this
1470
- -- in terms of the rate limiting feature if/when that is added.
1471
- function QlessQueue.pause(...)
1472
- redis.call('sadd', 'ql:paused_queues', unpack(arg))
1473
- end
1474
-
1475
- -- This script takes the name of the queue(s) and removes it
1476
- -- from the ql:paused_queues set.
1477
- --
1478
- -- Args: The list of queues to pause.
1479
- function QlessQueue.unpause(...)
1480
- redis.call('srem', 'ql:paused_queues', unpack(arg))
1481
- end
1482
-
1483
- -- This script takes the name of the queue and then checks
1484
- -- for any expired locks, then inserts any scheduled items
1485
- -- that are now valid, and lastly returns any work items
1486
- -- that can be handed over.
1487
- --
1488
- -- Keys:
1489
- -- 1) queue name
1490
- -- Args:
1491
- -- 1) worker name
1492
- -- 2) the number of items to return
1493
- -- 3) the current time
1494
- function QlessQueue:pop(now, worker, count)
1495
- assert(worker, 'Pop(): Arg "worker" missing')
1496
- count = assert(tonumber(count),
1497
- 'Pop(): Arg "count" missing or not a number: ' .. tostring(count))
1498
-
1499
- -- We should find the heartbeat interval for this queue heartbeat
1500
- local expires = now + tonumber(
1501
- Qless.config.get(self.name .. '-heartbeat') or
1502
- Qless.config.get('heartbeat', 60))
1503
-
1504
- -- If this queue is paused, then return no jobs
1505
- if self:paused() then
1506
- return {}
1507
- end
1508
-
1509
- -- Make sure we this worker to the list of seen workers
1510
- redis.call('zadd', 'ql:workers', now, worker)
1511
-
1512
- -- Check our max concurrency, and limit the count
1513
- local max_concurrency = tonumber(
1514
- Qless.config.get(self.name .. '-max-concurrency', 0))
1515
-
1516
- if max_concurrency > 0 then
1517
- -- Allow at most max_concurrency - #running
1518
- local allowed = math.max(0, max_concurrency - self.locks.running(now))
1519
- count = math.min(allowed, count)
1520
- if count == 0 then
1521
- return {}
1522
- end
1523
- end
1524
-
1525
- local jids = self:invalidate_locks(now, count)
1526
- -- Now we've checked __all__ the locks for this queue the could
1527
- -- have expired, and are no more than the number requested.
1528
-
1529
- -- If we still need jobs in order to meet demand, then we should
1530
- -- look for all the recurring jobs that need jobs run
1531
- self:check_recurring(now, count - #jids)
1532
-
1533
- -- If we still need values in order to meet the demand, then we
1534
- -- should check if any scheduled items, and if so, we should
1535
- -- insert them to ensure correctness when pulling off the next
1536
- -- unit of work.
1537
- self:check_scheduled(now, count - #jids)
1538
-
1539
- -- With these in place, we can expand this list of jids based on the work
1540
- -- queue itself and the priorities therein
1541
- table.extend(jids, self.work.peek(count - #jids))
1542
-
1543
- local state
1544
- for index, jid in ipairs(jids) do
1545
- local job = Qless.job(jid)
1546
- state = unpack(job:data('state'))
1547
- job:history(now, 'popped', {worker = worker})
1548
-
1549
- -- Update the wait time statistics
1550
- -- local waiting = math.floor(now) - history[#history]['put']
1551
- local waiting = 0
1552
- self:stat(now, 'wait', waiting)
1553
-
1554
- -- Add this job to the list of jobs handled by this worker
1555
- redis.call('zadd', 'ql:w:' .. worker .. ':jobs', expires, jid)
1556
-
1557
- -- Update the jobs data, and add its locks, and return the job
1558
- job:update({
1559
- worker = worker,
1560
- expires = expires,
1561
- state = 'running'
1562
- })
1563
-
1564
- self.locks.add(expires, jid)
1565
-
1566
- local tracked = redis.call('zscore', 'ql:tracked', jid) ~= false
1567
- if tracked then
1568
- Qless.publish('popped', jid)
1569
- end
1570
- end
1571
-
1572
- -- If we are returning any jobs, then we should remove them from the work
1573
- -- queue
1574
- self.work.remove(unpack(jids))
1575
-
1576
- return jids
1577
- end
1578
-
1579
- --! @brief Update the stats for this queue
1580
- --! @param stat - name of the statistic to be updated ('wait', 'run', etc.)
1581
- --! @param val - the value to update the statistics with
1582
- function QlessQueue:stat(now, stat, val)
1583
- -- The bin is midnight of the provided day
1584
- local bin = now - (now % 86400)
1585
- local key = 'ql:s:' .. stat .. ':' .. bin .. ':' .. self.name
1586
-
1587
- -- Get the current data
1588
- local count, mean, vk = unpack(
1589
- redis.call('hmget', key, 'total', 'mean', 'vk'))
1590
-
1591
- -- If there isn't any data there presently, then we must initialize it
1592
- count = count or 0
1593
- if count == 0 then
1594
- mean = val
1595
- vk = 0
1596
- count = 1
1597
- else
1598
- count = count + 1
1599
- local oldmean = mean
1600
- mean = mean + (val - mean) / count
1601
- vk = vk + (val - mean) * (val - oldmean)
1602
- end
1603
-
1604
- -- Now, update the histogram
1605
- -- - `s1`, `s2`, ..., -- second-resolution histogram counts
1606
- -- - `m1`, `m2`, ..., -- minute-resolution
1607
- -- - `h1`, `h2`, ..., -- hour-resolution
1608
- -- - `d1`, `d2`, ..., -- day-resolution
1609
- val = math.floor(val)
1610
- if val < 60 then -- seconds
1611
- redis.call('hincrby', key, 's' .. val, 1)
1612
- elseif val < 3600 then -- minutes
1613
- redis.call('hincrby', key, 'm' .. math.floor(val / 60), 1)
1614
- elseif val < 86400 then -- hours
1615
- redis.call('hincrby', key, 'h' .. math.floor(val / 3600), 1)
1616
- else -- days
1617
- redis.call('hincrby', key, 'd' .. math.floor(val / 86400), 1)
1618
- end
1619
- redis.call('hmset', key, 'total', count, 'mean', mean, 'vk', vk)
1620
- end
1621
-
1622
- -- Put(1, jid, klass, data, now, delay, [priority, p], [tags, t], [retries, r], [depends, '[...]'])
1623
- -- ----------------------------------------------------------------------------
1624
- -- This script takes the name of the queue and then the
1625
- -- info about the work item, and makes sure that it's
1626
- -- enqueued.
1627
- --
1628
- -- At some point, I'd like to able to provide functionality
1629
- -- that enables this to generate a unique ID for this piece
1630
- -- of work. As such, client libraries should not expose
1631
- -- setting the id from the user, as this is an implementation
1632
- -- detail that's likely to change and users should not grow
1633
- -- to depend on it.
1634
- --
1635
- -- Args:
1636
- -- 1) jid
1637
- -- 2) klass
1638
- -- 3) data
1639
- -- 4) now
1640
- -- 5) delay
1641
- -- *) [priority, p], [tags, t], [retries, r], [depends, '[...]']
1642
- function QlessQueue:put(now, jid, klass, data, delay, ...)
1643
- assert(jid , 'Put(): Arg "jid" missing')
1644
- assert(klass, 'Put(): Arg "klass" missing')
1645
- data = assert(cjson.decode(data),
1646
- 'Put(): Arg "data" missing or not JSON: ' .. tostring(data))
1647
- delay = assert(tonumber(delay),
1648
- 'Put(): Arg "delay" not a number: ' .. tostring(delay))
1649
-
1650
- -- Read in all the optional parameters
1651
- local options = {}
1652
- for i = 1, #arg, 2 do options[arg[i]] = arg[i + 1] end
1653
-
1654
- -- Let's see what the old priority and tags were
1655
- local job = Qless.job(jid)
1656
- local priority, tags, oldqueue, state, failure, retries, worker = unpack(redis.call('hmget', QlessJob.ns .. jid, 'priority', 'tags', 'queue', 'state', 'failure', 'retries', 'worker'))
1657
-
1658
- -- Sanity check on optional args
1659
- retries = assert(tonumber(options['retries'] or retries or 5) , 'Put(): Arg "retries" not a number: ' .. tostring(options['retries']))
1660
- tags = assert(cjson.decode(options['tags'] or tags or '[]' ), 'Put(): Arg "tags" not JSON' .. tostring(options['tags']))
1661
- priority = assert(tonumber(options['priority'] or priority or 0), 'Put(): Arg "priority" not a number' .. tostring(options['priority']))
1662
- local depends = assert(cjson.decode(options['depends'] or '[]') , 'Put(): Arg "depends" not JSON: ' .. tostring(options['depends']))
1663
-
1664
- -- Delay and depends are not allowed together
1665
- if delay > 0 and #depends > 0 then
1666
- error('Put(): "delay" and "depends" are not allowed to be used together')
1667
- end
1668
-
1669
- -- Send out a log message
1670
- Qless.publish('log', cjson.encode({
1671
- jid = jid,
1672
- event = 'put',
1673
- queue = self.name
1674
- }))
1675
-
1676
- -- Update the history to include this new change
1677
- job:history(now, 'put', {q = self.name})
1678
-
1679
- -- If this item was previously in another queue, then we should remove it from there
1680
- if oldqueue then
1681
- local queue_obj = Qless.queue(oldqueue)
1682
- queue_obj.work.remove(jid)
1683
- queue_obj.locks.remove(jid)
1684
- queue_obj.depends.remove(jid)
1685
- queue_obj.scheduled.remove(jid)
1686
- end
1687
-
1688
- -- If this had previously been given out to a worker,
1689
- -- make sure to remove it from that worker's jobs
1690
- if worker then
1691
- redis.call('zrem', 'ql:w:' .. worker .. ':jobs', jid)
1692
- -- We need to inform whatever worker had that job
1693
- Qless.publish('w:' .. worker, cjson.encode({
1694
- jid = jid,
1695
- event = 'put',
1696
- queue = self.name
1697
- }))
1698
- end
1699
-
1700
- -- If the job was previously in the 'completed' state, then we should remove
1701
- -- it from being enqueued for destructination
1702
- if state == 'complete' then
1703
- redis.call('zrem', 'ql:completed', jid)
1704
- end
1705
-
1706
- -- Add this job to the list of jobs tagged with whatever tags were supplied
1707
- for i, tag in ipairs(tags) do
1708
- redis.call('zadd', 'ql:t:' .. tag, now, jid)
1709
- redis.call('zincrby', 'ql:tags', 1, tag)
1710
- end
1711
-
1712
- -- If we're in the failed state, remove all of our data
1713
- if state == 'failed' then
1714
- failure = cjson.decode(failure)
1715
- -- We need to make this remove it from the failed queues
1716
- redis.call('lrem', 'ql:f:' .. failure.group, 0, jid)
1717
- if redis.call('llen', 'ql:f:' .. failure.group) == 0 then
1718
- redis.call('srem', 'ql:failures', failure.group)
1719
- end
1720
- -- The bin is midnight of the provided day
1721
- -- 24 * 60 * 60 = 86400
1722
- local bin = failure.when - (failure.when % 86400)
1723
- -- We also need to decrement the stats about the queue on
1724
- -- the day that this failure actually happened.
1725
- redis.call('hincrby', 'ql:s:stats:' .. bin .. ':' .. self.name, 'failed' , -1)
1726
- end
1727
-
1728
- -- First, let's save its data
1729
- redis.call('hmset', QlessJob.ns .. jid,
1730
- 'jid' , jid,
1731
- 'klass' , klass,
1732
- 'data' , cjson.encode(data),
1733
- 'priority' , priority,
1734
- 'tags' , cjson.encode(tags),
1735
- 'state' , ((delay > 0) and 'scheduled') or 'waiting',
1736
- 'worker' , '',
1737
- 'expires' , 0,
1738
- 'queue' , self.name,
1739
- 'retries' , retries,
1740
- 'remaining', retries)
1741
-
1742
- -- These are the jids we legitimately have to wait on
1743
- for i, j in ipairs(depends) do
1744
- -- Make sure it's something other than 'nil' or complete.
1745
- local state = redis.call('hget', QlessJob.ns .. j, 'state')
1746
- if (state and state ~= 'complete') then
1747
- redis.call('sadd', QlessJob.ns .. j .. '-dependents' , jid)
1748
- redis.call('sadd', QlessJob.ns .. jid .. '-dependencies', j)
1749
- end
1750
- end
1751
-
1752
- -- Now, if a delay was provided, and if it's in the future,
1753
- -- then we'll have to schedule it. Otherwise, we're just
1754
- -- going to add it to the work queue.
1755
- if delay > 0 then
1756
- self.scheduled.add(now + delay, jid)
1757
- else
1758
- if redis.call('scard', QlessJob.ns .. jid .. '-dependencies') > 0 then
1759
- self.depends.add(now, jid)
1760
- redis.call('hset', QlessJob.ns .. jid, 'state', 'depends')
1761
- else
1762
- self.work.add(now, priority, jid)
1763
- end
1764
- end
1765
-
1766
- -- Lastly, we're going to make sure that this item is in the
1767
- -- set of known queues. We should keep this sorted by the
1768
- -- order in which we saw each of these queues
1769
- if redis.call('zscore', 'ql:queues', self.name) == false then
1770
- redis.call('zadd', 'ql:queues', now, self.name)
1771
- end
1772
-
1773
- if redis.call('zscore', 'ql:tracked', jid) ~= false then
1774
- Qless.publish('put', jid)
1775
- end
1776
-
1777
- return jid
1778
- end
1779
-
1780
- -- Unfail(0, now, group, queue, [count])
1781
- --
1782
- -- Move `count` jobs out of the failed state and into the provided queue
1783
- function QlessQueue:unfail(now, group, count)
1784
- assert(group, 'Unfail(): Arg "group" missing')
1785
- count = assert(tonumber(count or 25),
1786
- 'Unfail(): Arg "count" not a number: ' .. tostring(count))
1787
-
1788
- -- Get up to that many jobs, and we'll put them in the appropriate queue
1789
- local jids = redis.call('lrange', 'ql:f:' .. group, -count, -1)
1790
-
1791
- -- And now set each job's state, and put it into the appropriate queue
1792
- local toinsert = {}
1793
- for index, jid in ipairs(jids) do
1794
- local job = Qless.job(job)
1795
- local data = job:data()
1796
- job:history(now, 'put', {q = self.name})
1797
- redis.call('hmset', QlessJob.ns .. data.jid,
1798
- 'state' , 'waiting',
1799
- 'worker' , '',
1800
- 'expires' , 0,
1801
- 'queue' , self.name,
1802
- 'remaining', data.retries or 5)
1803
- self.work.add(now, data.priority, data.jid)
1804
- end
1805
-
1806
- -- Remove these jobs from the failed state
1807
- redis.call('ltrim', 'ql:f:' .. group, 0, -count - 1)
1808
- if (redis.call('llen', 'ql:f:' .. group) == 0) then
1809
- redis.call('srem', 'ql:failures', group)
1810
- end
1811
-
1812
- return #jids
1813
- end
1814
-
1815
- function QlessQueue:recur(now, jid, klass, data, spec, ...)
1816
- assert(jid , 'RecurringJob On(): Arg "jid" missing')
1817
- assert(klass, 'RecurringJob On(): Arg "klass" missing')
1818
- assert(spec , 'RecurringJob On(): Arg "spec" missing')
1819
- data = assert(cjson.decode(data),
1820
- 'RecurringJob On(): Arg "data" not JSON: ' .. tostring(data))
1821
-
1822
- -- At some point in the future, we may have different types of recurring
1823
- -- jobs, but for the time being, we only have 'interval'-type jobs
1824
- if spec == 'interval' then
1825
- local interval = assert(tonumber(arg[1]),
1826
- 'Recur(): Arg "interval" not a number: ' .. tostring(arg[1]))
1827
- local offset = assert(tonumber(arg[2]),
1828
- 'Recur(): Arg "offset" not a number: ' .. tostring(arg[2]))
1829
- if interval <= 0 then
1830
- error('Recur(): Arg "interval" must be greater than or equal to 0')
1831
- end
1832
-
1833
- -- Read in all the optional parameters
1834
- local options = {}
1835
- for i = 3, #arg, 2 do options[arg[i]] = arg[i + 1] end
1836
- options.tags = assert(cjson.decode(options.tags or {}),
1837
- 'Recur(): Arg "tags" must be JSON string array: ' .. tostring(
1838
- options.tags))
1839
- options.priority = assert(tonumber(options.priority or 0),
1840
- 'Recur(): Arg "priority" not a number: ' .. tostring(
1841
- options.priority))
1842
- options.retries = assert(tonumber(options.retries or 0),
1843
- 'Recur(): Arg "retries" not a number: ' .. tostring(
1844
- options.retries))
1845
- options.backlog = assert(tonumber(options.backlog or 0),
1846
- 'Recur(): Arg "backlog" not a number: ' .. tostring(
1847
- options.backlog))
1848
-
1849
- local count, old_queue = unpack(redis.call('hmget', 'ql:r:' .. jid, 'count', 'queue'))
1850
- count = count or 0
1851
-
1852
- -- If it has previously been in another queue, then we should remove
1853
- -- some information about it
1854
- if old_queue then
1855
- Qless.queue(old_queue).recurring.remove(jid)
1856
- end
1857
-
1858
- -- Do some insertions
1859
- redis.call('hmset', 'ql:r:' .. jid,
1860
- 'jid' , jid,
1861
- 'klass' , klass,
1862
- 'data' , cjson.encode(data),
1863
- 'priority', options.priority,
1864
- 'tags' , cjson.encode(options.tags or {}),
1865
- 'state' , 'recur',
1866
- 'queue' , self.name,
1867
- 'type' , 'interval',
1868
- -- How many jobs we've spawned from this
1869
- 'count' , count,
1870
- 'interval', interval,
1871
- 'retries' , options.retries,
1872
- 'backlog' , options.backlog)
1873
- -- Now, we should schedule the next run of the job
1874
- self.recurring.add(now + offset, jid)
1875
-
1876
- -- Lastly, we're going to make sure that this item is in the
1877
- -- set of known queues. We should keep this sorted by the
1878
- -- order in which we saw each of these queues
1879
- if redis.call('zscore', 'ql:queues', self.name) == false then
1880
- redis.call('zadd', 'ql:queues', now, self.name)
1881
- end
1882
-
1883
- return jid
1884
- else
1885
- error('Recur(): schedule type "' .. tostring(spec) .. '" unknown')
1886
- end
1887
- end
1888
-
1889
- -- Return the length of the queue
1890
- function QlessQueue:length()
1891
- return self.locks.length() + self.work.length() + self.scheduled.length()
1892
- end
1893
-
1894
- -------------------------------------------------------------------------------
1895
- -- Housekeeping methods
1896
- -------------------------------------------------------------------------------
1897
- --! @brief Instantiate any recurring jobs that are ready
1898
- function QlessQueue:check_recurring(now, count)
1899
- -- This is how many jobs we've moved so far
1900
- local moved = 0
1901
- -- These are the recurring jobs that need work
1902
- local r = self.recurring.peek(now, 0, count)
1903
- for index, jid in ipairs(r) do
1904
- -- For each of the jids that need jobs scheduled, first
1905
- -- get the last time each of them was run, and then increment
1906
- -- it by its interval. While this time is less than now,
1907
- -- we need to keep putting jobs on the queue
1908
- local klass, data, priority, tags, retries, interval, backlog = unpack(
1909
- redis.call('hmget', 'ql:r:' .. jid, 'klass', 'data', 'priority',
1910
- 'tags', 'retries', 'interval', 'backlog'))
1911
- local _tags = cjson.decode(tags)
1912
- local score = math.floor(tonumber(self.recurring.score(jid)))
1913
- interval = tonumber(interval)
1914
-
1915
- -- If the backlog is set for this job, then see if it's been a long
1916
- -- time since the last pop
1917
- backlog = tonumber(backlog or 0)
1918
- if backlog ~= 0 then
1919
- -- Check how many jobs we could concievably generate
1920
- local num = ((now - score) / interval)
1921
- if num > backlog then
1922
- -- Update the score
1923
- score = score + (
1924
- math.ceil(num - backlog) * interval
1925
- )
1926
- end
1927
- end
1928
-
1929
- -- We're saving this value so that in the history, we can accurately
1930
- -- reflect when the job would normally have been scheduled
1931
- while (score <= now) and (moved < count) do
1932
- local count = redis.call('hincrby', 'ql:r:' .. jid, 'count', 1)
1933
- moved = moved + 1
1934
-
1935
- -- Add this job to the list of jobs tagged with whatever tags were
1936
- -- supplied
1937
- for i, tag in ipairs(_tags) do
1938
- redis.call('zadd', 'ql:t:' .. tag, now, jid .. '-' .. count)
1939
- redis.call('zincrby', 'ql:tags', 1, tag)
1940
- end
1941
-
1942
- -- First, let's save its data
1943
- local child_jid = jid .. '-' .. count
1944
- redis.call('hmset', QlessJob.ns .. child_jid,
1945
- 'jid' , jid .. '-' .. count,
1946
- 'klass' , klass,
1947
- 'data' , data,
1948
- 'priority' , priority,
1949
- 'tags' , tags,
1950
- 'state' , 'waiting',
1951
- 'worker' , '',
1952
- 'expires' , 0,
1953
- 'queue' , self.name,
1954
- 'retries' , retries,
1955
- 'remaining', retries)
1956
- Qless.job(child_jid):history(score, 'put', {q = self.name})
1957
-
1958
- -- Now, if a delay was provided, and if it's in the future,
1959
- -- then we'll have to schedule it. Otherwise, we're just
1960
- -- going to add it to the work queue.
1961
- self.work.add(score, priority, jid .. '-' .. count)
1962
-
1963
- score = score + interval
1964
- self.recurring.add(score, jid)
1965
- end
1966
- end
1967
- end
1968
-
1969
- --! @brief Check for any jobs that have been scheduled, and shovel them onto
1970
- --! the work queue. Returns nothing, but afterwards, up to `count`
1971
- --! scheduled jobs will be moved into the work queue
1972
- function QlessQueue:check_scheduled(now, count, execute)
1973
- -- zadd is a list of arguments that we'll be able to use to
1974
- -- insert into the work queue
1975
- local zadd = {}
1976
- local scheduled = self.scheduled.ready(now, 0, count)
1977
- for index, jid in ipairs(scheduled) do
1978
- -- With these in hand, we'll have to go out and find the
1979
- -- priorities of these jobs, and then we'll insert them
1980
- -- into the work queue and then when that's complete, we'll
1981
- -- remove them from the scheduled queue
1982
- local priority = tonumber(
1983
- redis.call('hget', QlessJob.ns .. jid, 'priority') or 0)
1984
- self.work.add(now, priority, jid)
1985
-
1986
- -- We should also update them to have the state 'waiting'
1987
- -- instead of 'scheduled'
1988
- redis.call('hset', QlessJob.ns .. jid, 'state', 'waiting')
1989
- end
1990
-
1991
- if #zadd > 0 then
1992
- -- Now add these to the work list, and then remove them
1993
- -- from the scheduled list
1994
- self.scheduled.remove(unpack(scheduled))
1995
- end
1996
- end
1997
-
1998
- --! @brief Check for and invalidate any locks that have been lost. Returns the
1999
- --! list of jids that have been invalidated
2000
- function QlessQueue:invalidate_locks(now, count)
2001
- local jids = {}
2002
- -- Iterate through all the expired locks and add them to the list
2003
- -- of keys that we'll return
2004
- for index, jid in ipairs(self.locks.expired(now, 0, count)) do
2005
- -- Remove this job from the jobs that the worker that was running it
2006
- -- has
2007
- local worker, failure = unpack(
2008
- redis.call('hmget', QlessJob.ns .. jid, 'worker', 'failure'))
2009
- redis.call('zrem', 'ql:w:' .. worker .. ':jobs', jid)
2010
-
2011
- -- We'll provide a grace period after jobs time out for them to give
2012
- -- some indication of the failure mode. After that time, however, we'll
2013
- -- consider the worker dust in the wind
2014
- local grace_period = tonumber(Qless.config.get('grace-period'))
2015
-
2016
- -- For each of these, decrement their retries. If any of them
2017
- -- have exhausted their retries, then we should mark them as
2018
- -- failed.
2019
- local remaining = tonumber(redis.call(
2020
- 'hincrbyfloat', QlessJob.ns .. jid, 'remaining', -0.5))
2021
-
2022
- -- If the remaining value is an odd multiple of 0.5, then we'll assume
2023
- -- that we're just sending the message. Otherwise, it's time to
2024
- -- actually hand out the work to another worker
2025
- local send_message = ((remaining * 2) % 2 == 1)
2026
- local invalidate = not send_message
2027
-
2028
- -- If the grace period has been disabled, then we'll do both.
2029
- if grace_period <= 0 then
2030
- remaining = tonumber(redis.call(
2031
- 'hincrbyfloat', QlessJob.ns .. jid, 'remaining', -0.5))
2032
- send_message = true
2033
- invalidate = true
2034
- end
2035
-
2036
- if send_message then
2037
- -- This is where we supply a courtesy message and give the worker
2038
- -- time to provide a failure message
2039
- if redis.call('zscore', 'ql:tracked', jid) ~= false then
2040
- Qless.publish('stalled', jid)
2041
- end
2042
- Qless.job(jid):history(now, 'timed-out')
2043
-
2044
- -- Send a message to let the worker know that its lost its lock on
2045
- -- the job
2046
- local encoded = cjson.encode({
2047
- jid = jid,
2048
- event = 'lock_lost',
2049
- worker = worker
2050
- })
2051
- Qless.publish('w:' .. worker, encoded)
2052
- Qless.publish('log', encoded)
2053
- self.locks.add(now + grace_period, jid)
2054
-
2055
- -- If we got any expired locks, then we should increment the
2056
- -- number of retries for this stage for this bin. The bin is
2057
- -- midnight of the provided day
2058
- local bin = now - (now % 86400)
2059
- redis.call('hincrby',
2060
- 'ql:s:stats:' .. bin .. ':' .. self.name, 'retries', 1)
2061
- end
2062
-
2063
- if invalidate then
2064
- -- This is where we actually have to time out the work
2065
- if remaining < 0 then
2066
- -- Now remove the instance from the schedule, and work queues
2067
- -- for the queue it's in
2068
- self.work.remove(jid)
2069
- self.locks.remove(jid)
2070
- self.scheduled.remove(jid)
2071
-
2072
- local group = 'failed-retries-' .. Qless.job(jid):data()['queue']
2073
- local job = Qless.job(jid)
2074
- job:history(now, 'failed', {group = group})
2075
- redis.call('hmset', QlessJob.ns .. jid, 'state', 'failed',
2076
- 'worker', '',
2077
- 'expires', '')
2078
- -- If the failure has not already been set, then set it
2079
- if failure == {} then
2080
- redis.call('hset', QlessJob.ns .. jid,
2081
- 'failure', cjson.encode({
2082
- ['group'] = group,
2083
- ['message'] =
2084
- 'Job exhausted retries in queue "' .. self.name .. '"',
2085
- ['when'] = now,
2086
- ['worker'] = unpack(job:data('worker'))
2087
- }))
2088
- end
2089
-
2090
- -- Add this type of failure to the list of failures
2091
- redis.call('sadd', 'ql:failures', group)
2092
- -- And add this particular instance to the failed types
2093
- redis.call('lpush', 'ql:f:' .. group, jid)
2094
-
2095
- if redis.call('zscore', 'ql:tracked', jid) ~= false then
2096
- Qless.publish('failed', jid)
2097
- end
2098
- else
2099
- table.insert(jids, jid)
2100
- end
2101
- end
2102
- end
2103
-
2104
- return jids
2105
- end
2106
-
2107
- -- Forget the provided queues. As in, remove them from the list of known queues
2108
- function QlessQueue.deregister(...)
2109
- redis.call('zrem', Qless.ns .. 'queues', unpack(arg))
2110
- end
2111
-
2112
- -- Return information about a particular queue, or all queues
2113
- -- [
2114
- -- {
2115
- -- 'name': 'testing',
2116
- -- 'stalled': 2,
2117
- -- 'waiting': 5,
2118
- -- 'running': 5,
2119
- -- 'scheduled': 10,
2120
- -- 'depends': 5,
2121
- -- 'recurring': 0
2122
- -- }, {
2123
- -- ...
2124
- -- }
2125
- -- ]
2126
- function QlessQueue.counts(now, name)
2127
- if name then
2128
- local queue = Qless.queue(name)
2129
- local stalled = queue.locks.length(now)
2130
- return {
2131
- name = name,
2132
- waiting = queue.work.length(),
2133
- stalled = stalled,
2134
- running = queue.locks.length() - stalled,
2135
- scheduled = queue.scheduled.length(),
2136
- depends = queue.depends.length(),
2137
- recurring = queue.recurring.length(),
2138
- paused = queue:paused()
2139
- }
2140
- else
2141
- local queues = redis.call('zrange', 'ql:queues', 0, -1)
2142
- local response = {}
2143
- for index, qname in ipairs(queues) do
2144
- table.insert(response, QlessQueue.counts(now, qname))
2145
- end
2146
- return response
2147
- end
2148
- end
2149
- -- Recur(0, 'on', queue, jid, klass, data, now, 'interval', second, offset, [priority p], [tags t], [retries r])
2150
- -- Recur(0, 'off', jid)
2151
- -- Recur(0, 'get', jid)
2152
- -- Recur(0, 'update', jid, ['priority', priority], ['interval', interval], ['retries', retries], ['data', data], ['klass', klass], ['queue', queue])
2153
- -- Recur(0, 'tag', jid, tag, [tag, [...]])
2154
- -- Recur(0, 'untag', jid, tag, [tag, [...]])
2155
- -- -------------------------------------------------------------------------------------------------------
2156
- -- This script takes the name of a queue, and then the info
2157
- -- info about the work item, and makes sure that jobs matching
2158
- -- its criteria are regularly made available.
2159
- function QlessRecurringJob:data()
2160
- local job = redis.call(
2161
- 'hmget', 'ql:r:' .. self.jid, 'jid', 'klass', 'state', 'queue',
2162
- 'priority', 'interval', 'retries', 'count', 'data', 'tags', 'backlog')
2163
-
2164
- if not job[1] then
2165
- return nil
2166
- end
2167
-
2168
- return {
2169
- jid = job[1],
2170
- klass = job[2],
2171
- state = job[3],
2172
- queue = job[4],
2173
- priority = tonumber(job[5]),
2174
- interval = tonumber(job[6]),
2175
- retries = tonumber(job[7]),
2176
- count = tonumber(job[8]),
2177
- data = cjson.decode(job[9]),
2178
- tags = cjson.decode(job[10]),
2179
- backlog = tonumber(job[11] or 0)
2180
- }
2181
- end
2182
-
2183
- -- Update the recurring job data
2184
- function QlessRecurringJob:update(...)
2185
- local options = {}
2186
- -- Make sure that the job exists
2187
- if redis.call('exists', 'ql:r:' .. self.jid) ~= 0 then
2188
- for i = 1, #arg, 2 do
2189
- local key = arg[i]
2190
- local value = arg[i+1]
2191
- if key == 'priority' or key == 'interval' or key == 'retries' then
2192
- value = assert(tonumber(value), 'Recur(): Arg "' .. key .. '" must be a number: ' .. tostring(value))
2193
- -- If the command is 'interval', then we need to update the
2194
- -- time when it should next be scheduled
2195
- if key == 'interval' then
2196
- local queue, interval = unpack(redis.call('hmget', 'ql:r:' .. self.jid, 'queue', 'interval'))
2197
- Qless.queue(queue).recurring.update(
2198
- value - tonumber(interval), self.jid)
2199
- end
2200
- redis.call('hset', 'ql:r:' .. self.jid, key, value)
2201
- elseif key == 'data' then
2202
- value = assert(cjson.decode(value), 'Recur(): Arg "data" is not JSON-encoded: ' .. tostring(value))
2203
- redis.call('hset', 'ql:r:' .. self.jid, 'data', cjson.encode(value))
2204
- elseif key == 'klass' then
2205
- redis.call('hset', 'ql:r:' .. self.jid, 'klass', value)
2206
- elseif key == 'queue' then
2207
- local queue_obj = Qless.queue(
2208
- redis.call('hget', 'ql:r:' .. self.jid, 'queue'))
2209
- local score = queue_obj.recurring.score(self.jid)
2210
- queue_obj.recurring.remove(self.jid)
2211
- Qless.queue(value).recurring.add(score, self.jid)
2212
- redis.call('hset', 'ql:r:' .. self.jid, 'queue', value)
2213
- elseif key == 'backlog' then
2214
- value = assert(tonumber(value),
2215
- 'Recur(): Arg "backlog" not a number: ' .. tostring(value))
2216
- redis.call('hset', 'ql:r:' .. self.jid, 'backlog', value)
2217
- else
2218
- error('Recur(): Unrecognized option "' .. key .. '"')
2219
- end
2220
- end
2221
- return true
2222
- else
2223
- error('Recur(): No recurring job ' .. self.jid)
2224
- end
2225
- end
2226
-
2227
- function QlessRecurringJob:tag(...)
2228
- local tags = redis.call('hget', 'ql:r:' .. self.jid, 'tags')
2229
- -- If the job has been canceled / deleted, then return false
2230
- if tags then
2231
- -- Decode the json blob, convert to dictionary
2232
- tags = cjson.decode(tags)
2233
- local _tags = {}
2234
- for i,v in ipairs(tags) do _tags[v] = true end
2235
-
2236
- -- Otherwise, add the job to the sorted set with that tags
2237
- for i=1,#arg do if _tags[arg[i]] == nil then table.insert(tags, arg[i]) end end
2238
-
2239
- tags = cjson.encode(tags)
2240
- redis.call('hset', 'ql:r:' .. self.jid, 'tags', tags)
2241
- return tags
2242
- else
2243
- return false
2244
- end
2245
- end
2246
-
2247
- function QlessRecurringJob:untag(...)
2248
- -- Get the existing tags
2249
- local tags = redis.call('hget', 'ql:r:' .. self.jid, 'tags')
2250
- -- If the job has been canceled / deleted, then return false
2251
- if tags then
2252
- -- Decode the json blob, convert to dictionary
2253
- tags = cjson.decode(tags)
2254
- local _tags = {}
2255
- -- Make a hash
2256
- for i,v in ipairs(tags) do _tags[v] = true end
2257
- -- Delete these from the hash
2258
- for i = 1,#arg do _tags[arg[i]] = nil end
2259
- -- Back into a list
2260
- local results = {}
2261
- for i, tag in ipairs(tags) do if _tags[tag] then table.insert(results, tag) end end
2262
- -- json encode them, set, and return
2263
- tags = cjson.encode(results)
2264
- redis.call('hset', 'ql:r:' .. self.jid, 'tags', tags)
2265
- return tags
2266
- else
2267
- return false
2268
- end
2269
- end
2270
-
2271
- function QlessRecurringJob:unrecur()
2272
- -- First, find out what queue it was attached to
2273
- local queue = redis.call('hget', 'ql:r:' .. self.jid, 'queue')
2274
- if queue then
2275
- -- Now, delete it from the queue it was attached to, and delete the
2276
- -- thing itself
2277
- Qless.queue(queue).recurring.remove(self.jid)
2278
- redis.call('del', 'ql:r:' .. self.jid)
2279
- return true
2280
- else
2281
- return true
2282
- end
2283
- end
2284
- -- DeregisterWorkers(0, worker)
2285
- -- This script takes the name of a worker(s) on removes it/them
2286
- -- from the ql:workers set.
2287
- --
2288
- -- Args: The list of workers to deregister.
2289
- function QlessWorker.deregister(...)
2290
- redis.call('zrem', 'ql:workers', unpack(arg))
2291
- end
2292
-
2293
- -- Workers(0, now, [worker])
2294
- ----------------------------
2295
- -- Provide data about all the workers, or if a specific worker is provided,
2296
- -- then which jobs that worker is responsible for. If no worker is provided,
2297
- -- expect a response of the form:
2298
- --
2299
- -- [
2300
- -- # This is sorted by the recency of activity from that worker
2301
- -- {
2302
- -- 'name' : 'hostname1-pid1',
2303
- -- 'jobs' : 20,
2304
- -- 'stalled': 0
2305
- -- }, {
2306
- -- ...
2307
- -- }
2308
- -- ]
2309
- --
2310
- -- If a worker id is provided, then expect a response of the form:
2311
- --
2312
- -- {
2313
- -- 'jobs': [
2314
- -- jid1,
2315
- -- jid2,
2316
- -- ...
2317
- -- ], 'stalled': [
2318
- -- jid1,
2319
- -- ...
2320
- -- ]
2321
- -- }
2322
- --
2323
- function QlessWorker.counts(now, worker)
2324
- -- Clean up all the workers' job lists if they're too old. This is
2325
- -- determined by the `max-worker-age` configuration, defaulting to the
2326
- -- last day. Seems like a 'reasonable' default
2327
- local interval = tonumber(Qless.config.get('max-worker-age', 86400))
2328
-
2329
- local workers = redis.call('zrangebyscore', 'ql:workers', 0, now - interval)
2330
- for index, worker in ipairs(workers) do
2331
- redis.call('del', 'ql:w:' .. worker .. ':jobs')
2332
- end
2333
-
2334
- -- And now remove them from the list of known workers
2335
- redis.call('zremrangebyscore', 'ql:workers', 0, now - interval)
2336
-
2337
- if worker then
2338
- return {
2339
- jobs = redis.call('zrevrangebyscore', 'ql:w:' .. worker .. ':jobs', now + 8640000, now),
2340
- stalled = redis.call('zrevrangebyscore', 'ql:w:' .. worker .. ':jobs', now, 0)
2341
- }
2342
- else
2343
- local response = {}
2344
- local workers = redis.call('zrevrange', 'ql:workers', 0, -1)
2345
- for index, worker in ipairs(workers) do
2346
- table.insert(response, {
2347
- name = worker,
2348
- jobs = redis.call('zcount', 'ql:w:' .. worker .. ':jobs', now, now + 8640000),
2349
- stalled = redis.call('zcount', 'ql:w:' .. worker .. ':jobs', 0, now)
2350
- })
2351
- end
2352
- return response
2353
- end
2354
- end