reqless 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +8 -0
- data/README.md +648 -0
- data/Rakefile +117 -0
- data/bin/docker-build-and-test +22 -0
- data/exe/reqless-web +11 -0
- data/lib/reqless/config.rb +31 -0
- data/lib/reqless/failure_formatter.rb +43 -0
- data/lib/reqless/job.rb +496 -0
- data/lib/reqless/job_reservers/ordered.rb +29 -0
- data/lib/reqless/job_reservers/round_robin.rb +46 -0
- data/lib/reqless/job_reservers/shuffled_round_robin.rb +21 -0
- data/lib/reqless/lua/reqless-lib.lua +2965 -0
- data/lib/reqless/lua/reqless.lua +2545 -0
- data/lib/reqless/lua_script.rb +90 -0
- data/lib/reqless/middleware/requeue_exceptions.rb +94 -0
- data/lib/reqless/middleware/retry_exceptions.rb +72 -0
- data/lib/reqless/middleware/sentry.rb +66 -0
- data/lib/reqless/middleware/timeout.rb +63 -0
- data/lib/reqless/queue.rb +189 -0
- data/lib/reqless/queue_priority_pattern.rb +16 -0
- data/lib/reqless/server/static/css/bootstrap-responsive.css +686 -0
- data/lib/reqless/server/static/css/bootstrap-responsive.min.css +12 -0
- data/lib/reqless/server/static/css/bootstrap.css +3991 -0
- data/lib/reqless/server/static/css/bootstrap.min.css +689 -0
- data/lib/reqless/server/static/css/codemirror.css +112 -0
- data/lib/reqless/server/static/css/docs.css +839 -0
- data/lib/reqless/server/static/css/jquery.noty.css +105 -0
- data/lib/reqless/server/static/css/noty_theme_twitter.css +137 -0
- data/lib/reqless/server/static/css/style.css +200 -0
- data/lib/reqless/server/static/favicon.ico +0 -0
- data/lib/reqless/server/static/img/glyphicons-halflings-white.png +0 -0
- data/lib/reqless/server/static/img/glyphicons-halflings.png +0 -0
- data/lib/reqless/server/static/js/bootstrap-alert.js +94 -0
- data/lib/reqless/server/static/js/bootstrap-scrollspy.js +125 -0
- data/lib/reqless/server/static/js/bootstrap-tab.js +130 -0
- data/lib/reqless/server/static/js/bootstrap-tooltip.js +270 -0
- data/lib/reqless/server/static/js/bootstrap-typeahead.js +285 -0
- data/lib/reqless/server/static/js/bootstrap.js +1726 -0
- data/lib/reqless/server/static/js/bootstrap.min.js +6 -0
- data/lib/reqless/server/static/js/codemirror.js +2972 -0
- data/lib/reqless/server/static/js/jquery.noty.js +220 -0
- data/lib/reqless/server/static/js/mode/javascript.js +360 -0
- data/lib/reqless/server/static/js/theme/cobalt.css +18 -0
- data/lib/reqless/server/static/js/theme/eclipse.css +25 -0
- data/lib/reqless/server/static/js/theme/elegant.css +10 -0
- data/lib/reqless/server/static/js/theme/lesser-dark.css +45 -0
- data/lib/reqless/server/static/js/theme/monokai.css +28 -0
- data/lib/reqless/server/static/js/theme/neat.css +9 -0
- data/lib/reqless/server/static/js/theme/night.css +21 -0
- data/lib/reqless/server/static/js/theme/rubyblue.css +21 -0
- data/lib/reqless/server/static/js/theme/xq-dark.css +46 -0
- data/lib/reqless/server/views/_job.erb +259 -0
- data/lib/reqless/server/views/_job_list.erb +8 -0
- data/lib/reqless/server/views/_pagination.erb +7 -0
- data/lib/reqless/server/views/about.erb +130 -0
- data/lib/reqless/server/views/completed.erb +11 -0
- data/lib/reqless/server/views/config.erb +14 -0
- data/lib/reqless/server/views/failed.erb +48 -0
- data/lib/reqless/server/views/failed_type.erb +18 -0
- data/lib/reqless/server/views/job.erb +17 -0
- data/lib/reqless/server/views/layout.erb +451 -0
- data/lib/reqless/server/views/overview.erb +137 -0
- data/lib/reqless/server/views/queue.erb +125 -0
- data/lib/reqless/server/views/queues.erb +45 -0
- data/lib/reqless/server/views/tag.erb +6 -0
- data/lib/reqless/server/views/throttles.erb +38 -0
- data/lib/reqless/server/views/track.erb +75 -0
- data/lib/reqless/server/views/worker.erb +34 -0
- data/lib/reqless/server/views/workers.erb +14 -0
- data/lib/reqless/server.rb +549 -0
- data/lib/reqless/subscriber.rb +74 -0
- data/lib/reqless/test_helpers/worker_helpers.rb +55 -0
- data/lib/reqless/throttle.rb +57 -0
- data/lib/reqless/version.rb +5 -0
- data/lib/reqless/worker/base.rb +237 -0
- data/lib/reqless/worker/forking.rb +215 -0
- data/lib/reqless/worker/serial.rb +41 -0
- data/lib/reqless/worker.rb +5 -0
- data/lib/reqless.rb +309 -0
- metadata +399 -0
@@ -0,0 +1,2965 @@
|
|
1
|
+
-- Current SHA: 8b6600adb988e7f4922f606798b6ad64c06a245d
|
2
|
+
-- This is a generated file
|
3
|
+
-- cjson can't tell an empty array from an empty object, so empty arrays end up
|
4
|
+
-- encoded as objects. This function makes empty arrays look like empty arrays.
|
5
|
+
local function cjsonArrayDegenerationWorkaround(array)
|
6
|
+
if #array == 0 then
|
7
|
+
return "[]"
|
8
|
+
end
|
9
|
+
return cjson.encode(array)
|
10
|
+
end
|
11
|
+
-------------------------------------------------------------------------------
|
12
|
+
-- Forward declarations to make everything happy
|
13
|
+
-------------------------------------------------------------------------------
|
14
|
+
local Reqless = {
|
15
|
+
ns = 'ql:'
|
16
|
+
}
|
17
|
+
|
18
|
+
-- Queue forward delcaration
|
19
|
+
local ReqlessQueue = {
|
20
|
+
ns = Reqless.ns .. 'q:'
|
21
|
+
}
|
22
|
+
ReqlessQueue.__index = ReqlessQueue
|
23
|
+
|
24
|
+
-- Worker forward declaration
|
25
|
+
local ReqlessWorker = {
|
26
|
+
ns = Reqless.ns .. 'w:'
|
27
|
+
}
|
28
|
+
ReqlessWorker.__index = ReqlessWorker
|
29
|
+
|
30
|
+
-- Job forward declaration
|
31
|
+
local ReqlessJob = {
|
32
|
+
ns = Reqless.ns .. 'j:'
|
33
|
+
}
|
34
|
+
ReqlessJob.__index = ReqlessJob
|
35
|
+
|
36
|
+
-- throttle forward declaration
|
37
|
+
local ReqlessThrottle = {
|
38
|
+
ns = Reqless.ns .. 'th:'
|
39
|
+
}
|
40
|
+
ReqlessThrottle.__index = ReqlessThrottle
|
41
|
+
|
42
|
+
-- RecurringJob forward declaration
|
43
|
+
local ReqlessRecurringJob = {}
|
44
|
+
ReqlessRecurringJob.__index = ReqlessRecurringJob
|
45
|
+
|
46
|
+
-- Config forward declaration
|
47
|
+
Reqless.config = {}
|
48
|
+
|
49
|
+
-- Extend a table. This comes up quite frequently
|
50
|
+
local function table_extend(self, other)
|
51
|
+
for _, v in ipairs(other) do
|
52
|
+
table.insert(self, v)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
-- This is essentially the same as redis' publish, but it prefixes the channel
|
57
|
+
-- with the Reqless namespace
|
58
|
+
function Reqless.publish(channel, message)
|
59
|
+
redis.call('publish', Reqless.ns .. channel, message)
|
60
|
+
end
|
61
|
+
|
62
|
+
-- Return a job object given its job id
|
63
|
+
function Reqless.job(jid)
|
64
|
+
assert(jid, 'Job(): no jid provided')
|
65
|
+
local job = {}
|
66
|
+
setmetatable(job, ReqlessJob)
|
67
|
+
job.jid = jid
|
68
|
+
return job
|
69
|
+
end
|
70
|
+
|
71
|
+
-- Return a recurring job object
|
72
|
+
function Reqless.recurring(jid)
|
73
|
+
assert(jid, 'Recurring(): no jid provided')
|
74
|
+
local job = {}
|
75
|
+
setmetatable(job, ReqlessRecurringJob)
|
76
|
+
job.jid = jid
|
77
|
+
return job
|
78
|
+
end
|
79
|
+
|
80
|
+
-- Return a throttle object
|
81
|
+
-- throttle objects are used for arbitrary throttling of jobs.
|
82
|
+
function Reqless.throttle(tid)
|
83
|
+
assert(tid, 'Throttle(): no tid provided')
|
84
|
+
local throttle = ReqlessThrottle.data({id = tid})
|
85
|
+
setmetatable(throttle, ReqlessThrottle)
|
86
|
+
|
87
|
+
-- set of jids which have acquired a lock on this throttle.
|
88
|
+
throttle.locks = {
|
89
|
+
length = function()
|
90
|
+
return (redis.call('zcard', ReqlessThrottle.ns .. tid .. '-locks') or 0)
|
91
|
+
end, members = function()
|
92
|
+
return redis.call('zrange', ReqlessThrottle.ns .. tid .. '-locks', 0, -1)
|
93
|
+
end, add = function(...)
|
94
|
+
if #arg > 0 then
|
95
|
+
redis.call('zadd', ReqlessThrottle.ns .. tid .. '-locks', unpack(arg))
|
96
|
+
end
|
97
|
+
end, remove = function(...)
|
98
|
+
if #arg > 0 then
|
99
|
+
return redis.call('zrem', ReqlessThrottle.ns .. tid .. '-locks', unpack(arg))
|
100
|
+
end
|
101
|
+
end, pop = function(min, max)
|
102
|
+
return redis.call('zremrangebyrank', ReqlessThrottle.ns .. tid .. '-locks', min, max)
|
103
|
+
end, peek = function(min, max)
|
104
|
+
return redis.call('zrange', ReqlessThrottle.ns .. tid .. '-locks', min, max)
|
105
|
+
end
|
106
|
+
}
|
107
|
+
|
108
|
+
-- set of jids which are waiting for the throttle to become available.
|
109
|
+
throttle.pending = {
|
110
|
+
length = function()
|
111
|
+
return (redis.call('zcard', ReqlessThrottle.ns .. tid .. '-pending') or 0)
|
112
|
+
end, members = function()
|
113
|
+
return redis.call('zrange', ReqlessThrottle.ns .. tid .. '-pending', 0, -1)
|
114
|
+
end, add = function(now, jid)
|
115
|
+
redis.call('zadd', ReqlessThrottle.ns .. tid .. '-pending', now, jid)
|
116
|
+
end, remove = function(...)
|
117
|
+
if #arg > 0 then
|
118
|
+
return redis.call('zrem', ReqlessThrottle.ns .. tid .. '-pending', unpack(arg))
|
119
|
+
end
|
120
|
+
end, pop = function(min, max)
|
121
|
+
return redis.call('zremrangebyrank', ReqlessThrottle.ns .. tid .. '-pending', min, max)
|
122
|
+
end, peek = function(min, max)
|
123
|
+
return redis.call('zrange', ReqlessThrottle.ns .. tid .. '-pending', min, max)
|
124
|
+
end
|
125
|
+
}
|
126
|
+
|
127
|
+
return throttle
|
128
|
+
end
|
129
|
+
|
130
|
+
-- Failed([group, [start, [limit]]])
|
131
|
+
-- ------------------------------------
|
132
|
+
-- If no group is provided, this returns a JSON blob of the counts of the
|
133
|
+
-- various groups of failures known. If a group is provided, it will report up
|
134
|
+
-- to `limit` from `start` of the jobs affected by that issue.
|
135
|
+
--
|
136
|
+
-- # If no group, then...
|
137
|
+
-- {
|
138
|
+
-- 'group1': 1,
|
139
|
+
-- 'group2': 5,
|
140
|
+
-- ...
|
141
|
+
-- }
|
142
|
+
--
|
143
|
+
-- # If a group is provided, then...
|
144
|
+
-- {
|
145
|
+
-- 'total': 20,
|
146
|
+
-- 'jobs': [
|
147
|
+
-- {
|
148
|
+
-- # All the normal keys for a job
|
149
|
+
-- 'jid': ...,
|
150
|
+
-- 'data': ...
|
151
|
+
-- # The message for this particular instance
|
152
|
+
-- 'message': ...,
|
153
|
+
-- 'group': ...,
|
154
|
+
-- }, ...
|
155
|
+
-- ]
|
156
|
+
-- }
|
157
|
+
--
|
158
|
+
function Reqless.failed(group, start, limit)
|
159
|
+
start = assert(tonumber(start or 0),
|
160
|
+
'Failed(): Arg "start" is not a number: ' .. (start or 'nil'))
|
161
|
+
limit = assert(tonumber(limit or 25),
|
162
|
+
'Failed(): Arg "limit" is not a number: ' .. (limit or 'nil'))
|
163
|
+
|
164
|
+
if group then
|
165
|
+
-- If a group was provided, then we should do paginated lookup
|
166
|
+
return {
|
167
|
+
total = redis.call('llen', 'ql:f:' .. group),
|
168
|
+
jobs = redis.call('lrange', 'ql:f:' .. group, start, start + limit - 1)
|
169
|
+
}
|
170
|
+
end
|
171
|
+
|
172
|
+
-- Otherwise, we should just list all the known failure groups we have
|
173
|
+
local response = {}
|
174
|
+
local groups = redis.call('smembers', 'ql:failures')
|
175
|
+
for _, group in ipairs(groups) do
|
176
|
+
response[group] = redis.call('llen', 'ql:f:' .. group)
|
177
|
+
end
|
178
|
+
return response
|
179
|
+
end
|
180
|
+
|
181
|
+
-- Jobs(now, 'complete', [offset, [limit]])
|
182
|
+
-- Jobs(now, (
|
183
|
+
-- 'stalled' | 'running' | 'scheduled' | 'depends', 'recurring'
|
184
|
+
-- ), queue, [offset, [limit]])
|
185
|
+
-------------------------------------------------------------------------------
|
186
|
+
-- Return all the job ids currently considered to be in the provided state
|
187
|
+
-- in a particular queue. The response is a list of job ids:
|
188
|
+
--
|
189
|
+
-- [
|
190
|
+
-- jid1,
|
191
|
+
-- jid2,
|
192
|
+
-- ...
|
193
|
+
-- ]
|
194
|
+
function Reqless.jobs(now, state, ...)
|
195
|
+
assert(state, 'Jobs(): Arg "state" missing')
|
196
|
+
if state == 'complete' then
|
197
|
+
local offset = assert(tonumber(arg[1] or 0),
|
198
|
+
'Jobs(): Arg "offset" not a number: ' .. tostring(arg[1]))
|
199
|
+
local limit = assert(tonumber(arg[2] or 25),
|
200
|
+
'Jobs(): Arg "limit" not a number: ' .. tostring(arg[2]))
|
201
|
+
return redis.call('zrevrange', 'ql:completed', offset,
|
202
|
+
offset + limit - 1)
|
203
|
+
end
|
204
|
+
|
205
|
+
local queue_name = assert(arg[1], 'Jobs(): Arg "queue" missing')
|
206
|
+
local offset = assert(tonumber(arg[2] or 0),
|
207
|
+
'Jobs(): Arg "offset" not a number: ' .. tostring(arg[2]))
|
208
|
+
local limit = assert(tonumber(arg[3] or 25),
|
209
|
+
'Jobs(): Arg "limit" not a number: ' .. tostring(arg[3]))
|
210
|
+
|
211
|
+
local queue = Reqless.queue(queue_name)
|
212
|
+
if state == 'running' then
|
213
|
+
return queue.locks.peek(now, offset, limit)
|
214
|
+
elseif state == 'stalled' then
|
215
|
+
return queue.locks.expired(now, offset, limit)
|
216
|
+
elseif state == 'throttled' then
|
217
|
+
return queue.throttled.peek(now, offset, limit)
|
218
|
+
elseif state == 'scheduled' then
|
219
|
+
queue:check_scheduled(now, queue.scheduled.length())
|
220
|
+
return queue.scheduled.peek(now, offset, limit)
|
221
|
+
elseif state == 'depends' then
|
222
|
+
return queue.depends.peek(now, offset, limit)
|
223
|
+
elseif state == 'recurring' then
|
224
|
+
return queue.recurring.peek(math.huge, offset, limit)
|
225
|
+
end
|
226
|
+
|
227
|
+
error('Jobs(): Unknown type "' .. state .. '"')
|
228
|
+
end
|
229
|
+
|
230
|
+
-- Track()
|
231
|
+
-- Track(now, ('track' | 'untrack'), jid)
|
232
|
+
-- ------------------------------------------
|
233
|
+
-- If no arguments are provided, it returns details of all currently-tracked
|
234
|
+
-- jobs. If the first argument is 'track', then it will start tracking the job
|
235
|
+
-- associated with that id, and 'untrack' stops tracking it. In this context,
|
236
|
+
-- tracking is nothing more than saving the job to a list of jobs that are
|
237
|
+
-- considered special.
|
238
|
+
--
|
239
|
+
-- {
|
240
|
+
-- 'jobs': [
|
241
|
+
-- {
|
242
|
+
-- 'jid': ...,
|
243
|
+
-- # All the other details you'd get from 'job.get'
|
244
|
+
-- }, {
|
245
|
+
-- ...
|
246
|
+
-- }
|
247
|
+
-- ], 'expired': [
|
248
|
+
-- # These are all the jids that are completed and whose data expired
|
249
|
+
-- 'deadbeef',
|
250
|
+
-- ...,
|
251
|
+
-- ...,
|
252
|
+
-- ]
|
253
|
+
-- }
|
254
|
+
--
|
255
|
+
function Reqless.track(now, command, jid)
|
256
|
+
if command ~= nil then
|
257
|
+
assert(jid, 'Track(): Arg "jid" missing')
|
258
|
+
-- Verify that job exists
|
259
|
+
assert(Reqless.job(jid):exists(), 'Track(): Job does not exist')
|
260
|
+
if string.lower(command) == 'track' then
|
261
|
+
Reqless.publish('track', jid)
|
262
|
+
return redis.call('zadd', 'ql:tracked', now, jid)
|
263
|
+
elseif string.lower(command) == 'untrack' then
|
264
|
+
Reqless.publish('untrack', jid)
|
265
|
+
return redis.call('zrem', 'ql:tracked', jid)
|
266
|
+
end
|
267
|
+
error('Track(): Unknown action "' .. command .. '"')
|
268
|
+
end
|
269
|
+
|
270
|
+
local response = {
|
271
|
+
jobs = {},
|
272
|
+
expired = {},
|
273
|
+
}
|
274
|
+
local jids = redis.call('zrange', 'ql:tracked', 0, -1)
|
275
|
+
for _, jid in ipairs(jids) do
|
276
|
+
local data = Reqless.job(jid):data()
|
277
|
+
if data then
|
278
|
+
table.insert(response.jobs, data)
|
279
|
+
else
|
280
|
+
table.insert(response.expired, jid)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
return response
|
284
|
+
end
|
285
|
+
|
286
|
+
-- tag(now, ('add' | 'remove'), jid, tag, [tag, ...])
|
287
|
+
-- tag(now, 'get', tag, [offset, [limit]])
|
288
|
+
-- tag(now, 'top', [offset, [limit]])
|
289
|
+
-- -----------------------------------------------------------------------------
|
290
|
+
-- Accepts a jid, 'add' or 'remove', and then a list of tags
|
291
|
+
-- to either add or remove from the job. Alternatively, 'get',
|
292
|
+
-- a tag to get jobs associated with that tag, and offset and
|
293
|
+
-- limit
|
294
|
+
--
|
295
|
+
-- If 'add' or 'remove', the response is a list of the jobs
|
296
|
+
-- current tags, or False if the job doesn't exist. If 'get',
|
297
|
+
-- the response is of the form:
|
298
|
+
--
|
299
|
+
-- {
|
300
|
+
-- total: ...,
|
301
|
+
-- jobs: [
|
302
|
+
-- jid,
|
303
|
+
-- ...
|
304
|
+
-- ]
|
305
|
+
-- }
|
306
|
+
--
|
307
|
+
-- If 'top' is supplied, it returns the most commonly-used tags
|
308
|
+
-- in a paginated fashion.
|
309
|
+
function Reqless.tag(now, command, ...)
|
310
|
+
assert(command,
|
311
|
+
'Tag(): Arg "command" must be "add", "remove", "get" or "top"')
|
312
|
+
|
313
|
+
if command == 'get' then
|
314
|
+
local tag = assert(arg[1], 'Tag(): Arg "tag" missing')
|
315
|
+
local offset = assert(tonumber(arg[2] or 0),
|
316
|
+
'Tag(): Arg "offset" not a number: ' .. tostring(arg[2]))
|
317
|
+
local limit = assert(tonumber(arg[3] or 25),
|
318
|
+
'Tag(): Arg "limit" not a number: ' .. tostring(arg[3]))
|
319
|
+
return {
|
320
|
+
total = redis.call('zcard', 'ql:t:' .. tag),
|
321
|
+
jobs = redis.call('zrange', 'ql:t:' .. tag, offset, offset + limit - 1)
|
322
|
+
}
|
323
|
+
elseif command == 'top' then
|
324
|
+
local offset = assert(tonumber(arg[1] or 0) , 'Tag(): Arg "offset" not a number: ' .. tostring(arg[1]))
|
325
|
+
local limit = assert(tonumber(arg[2] or 25), 'Tag(): Arg "limit" not a number: ' .. tostring(arg[2]))
|
326
|
+
return redis.call('zrevrangebyscore', 'ql:tags', '+inf', 2, 'limit', offset, limit)
|
327
|
+
elseif command ~= 'add' and command ~= 'remove' then
|
328
|
+
error('Tag(): First argument must be "add", "remove", "get", or "top"')
|
329
|
+
end
|
330
|
+
|
331
|
+
local jid = assert(arg[1], 'Tag(): Arg "jid" missing')
|
332
|
+
local tags = redis.call('hget', ReqlessJob.ns .. jid, 'tags')
|
333
|
+
-- If the job has been canceled / deleted, raise an error
|
334
|
+
if not tags then
|
335
|
+
error('Tag(): Job ' .. jid .. ' does not exist')
|
336
|
+
end
|
337
|
+
|
338
|
+
-- Decode the json blob, convert to dictionary
|
339
|
+
tags = cjson.decode(tags)
|
340
|
+
local _tags = {}
|
341
|
+
for _, v in ipairs(tags) do _tags[v] = true end
|
342
|
+
|
343
|
+
if command == 'add' then
|
344
|
+
-- Add the job to the sorted set with that tags
|
345
|
+
for i=2, #arg do
|
346
|
+
local tag = arg[i]
|
347
|
+
if _tags[tag] == nil then
|
348
|
+
_tags[tag] = true
|
349
|
+
table.insert(tags, tag)
|
350
|
+
end
|
351
|
+
Reqless.job(jid):insert_tag(now, tag)
|
352
|
+
end
|
353
|
+
|
354
|
+
redis.call('hset', ReqlessJob.ns .. jid, 'tags', cjson.encode(tags))
|
355
|
+
return tags
|
356
|
+
end
|
357
|
+
|
358
|
+
-- Remove the job from the sorted set with that tags
|
359
|
+
for i=2, #arg do
|
360
|
+
local tag = arg[i]
|
361
|
+
_tags[tag] = nil
|
362
|
+
Reqless.job(jid):remove_tag(tag)
|
363
|
+
end
|
364
|
+
|
365
|
+
local results = {}
|
366
|
+
for _, tag in ipairs(tags) do
|
367
|
+
if _tags[tag] then
|
368
|
+
table.insert(results, tag)
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
redis.call('hset', ReqlessJob.ns .. jid, 'tags', cjson.encode(results))
|
373
|
+
return results
|
374
|
+
end
|
375
|
+
|
376
|
+
-- Cancel(...)
|
377
|
+
-- --------------
|
378
|
+
-- Cancel a job from taking place. It will be deleted from the system, and any
|
379
|
+
-- attempts to renew a heartbeat will fail, and any attempts to complete it
|
380
|
+
-- will fail. If you try to get the data on the object, you will get nothing.
|
381
|
+
function Reqless.cancel(now, ...)
|
382
|
+
-- Dependents is a mapping of a job to its dependent jids
|
383
|
+
local dependents = {}
|
384
|
+
for _, jid in ipairs(arg) do
|
385
|
+
dependents[jid] = redis.call(
|
386
|
+
'smembers', ReqlessJob.ns .. jid .. '-dependents') or {}
|
387
|
+
end
|
388
|
+
|
389
|
+
-- Now, we'll loop through every jid we intend to cancel, and we'll go
|
390
|
+
-- make sure that this operation will be ok
|
391
|
+
for _, jid in ipairs(arg) do
|
392
|
+
for j, dep in ipairs(dependents[jid]) do
|
393
|
+
if dependents[dep] == nil then
|
394
|
+
error('Cancel(): ' .. jid .. ' is a dependency of ' .. dep ..
|
395
|
+
' but is not mentioned to be canceled')
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
-- If we've made it this far, then we are good to go. We can now just
|
401
|
+
-- remove any trace of all these jobs, as they form a dependent clique
|
402
|
+
for _, jid in ipairs(arg) do
|
403
|
+
-- Find any stage it's associated with and remove its from that stage
|
404
|
+
local state, queue, failure, worker = unpack(redis.call(
|
405
|
+
'hmget', ReqlessJob.ns .. jid, 'state', 'queue', 'failure', 'worker'))
|
406
|
+
|
407
|
+
if state ~= 'complete' then
|
408
|
+
-- Send a message out on the appropriate channels
|
409
|
+
local encoded = cjson.encode({
|
410
|
+
jid = jid,
|
411
|
+
worker = worker,
|
412
|
+
event = 'canceled',
|
413
|
+
queue = queue
|
414
|
+
})
|
415
|
+
Reqless.publish('log', encoded)
|
416
|
+
|
417
|
+
-- Remove this job from whatever worker has it, if any
|
418
|
+
if worker and (worker ~= '') then
|
419
|
+
redis.call('zrem', 'ql:w:' .. worker .. ':jobs', jid)
|
420
|
+
-- If necessary, send a message to the appropriate worker, too
|
421
|
+
Reqless.publish('w:' .. worker, encoded)
|
422
|
+
end
|
423
|
+
|
424
|
+
-- Remove it from that queue
|
425
|
+
if queue then
|
426
|
+
local queue = Reqless.queue(queue)
|
427
|
+
queue:remove_job(jid)
|
428
|
+
end
|
429
|
+
|
430
|
+
local job = Reqless.job(jid)
|
431
|
+
|
432
|
+
job:throttles_release(now)
|
433
|
+
|
434
|
+
-- We should probably go through all our dependencies and remove
|
435
|
+
-- ourselves from the list of dependents
|
436
|
+
for _, j in ipairs(redis.call(
|
437
|
+
'smembers', ReqlessJob.ns .. jid .. '-dependencies')) do
|
438
|
+
redis.call('srem', ReqlessJob.ns .. j .. '-dependents', jid)
|
439
|
+
end
|
440
|
+
|
441
|
+
-- If we're in the failed state, remove all of our data
|
442
|
+
if state == 'failed' then
|
443
|
+
failure = cjson.decode(failure)
|
444
|
+
-- We need to make this remove it from the failed queues
|
445
|
+
redis.call('lrem', 'ql:f:' .. failure.group, 0, jid)
|
446
|
+
if redis.call('llen', 'ql:f:' .. failure.group) == 0 then
|
447
|
+
redis.call('srem', 'ql:failures', failure.group)
|
448
|
+
end
|
449
|
+
-- Remove one count from the failed count of the particular
|
450
|
+
-- queue
|
451
|
+
local bin = failure.when - (failure.when % 86400)
|
452
|
+
local failed = redis.call(
|
453
|
+
'hget', 'ql:s:stats:' .. bin .. ':' .. queue, 'failed')
|
454
|
+
redis.call('hset',
|
455
|
+
'ql:s:stats:' .. bin .. ':' .. queue, 'failed', failed - 1)
|
456
|
+
end
|
457
|
+
|
458
|
+
job:delete()
|
459
|
+
|
460
|
+
-- If the job was being tracked, we should notify
|
461
|
+
if redis.call('zscore', 'ql:tracked', jid) ~= false then
|
462
|
+
Reqless.publish('canceled', jid)
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
return arg
|
468
|
+
end
|
469
|
+
-------------------------------------------------------------------------------
|
470
|
+
-- Configuration interactions
|
471
|
+
-------------------------------------------------------------------------------
|
472
|
+
|
473
|
+
-- This represents our default configuration settings. Redis hash values are
|
474
|
+
-- strings, so use strings for the defaults for more consistent typing.
|
475
|
+
Reqless.config.defaults = {
|
476
|
+
['application'] = 'reqless',
|
477
|
+
['grace-period'] = '10',
|
478
|
+
['heartbeat'] = '60',
|
479
|
+
['jobs-history'] = '604800',
|
480
|
+
['jobs-history-count'] = '50000',
|
481
|
+
['max-job-history'] = '100',
|
482
|
+
['max-pop-retry'] = '1',
|
483
|
+
['max-worker-age'] = '86400',
|
484
|
+
}
|
485
|
+
|
486
|
+
-- Get one or more of the keys
|
487
|
+
Reqless.config.get = function(key, default)
|
488
|
+
if key then
|
489
|
+
return redis.call('hget', 'ql:config', key) or
|
490
|
+
Reqless.config.defaults[key] or default
|
491
|
+
end
|
492
|
+
|
493
|
+
-- Inspired by redis-lua https://github.com/nrk/redis-lua/blob/version-2.0/src/redis.lua
|
494
|
+
local reply = redis.call('hgetall', 'ql:config')
|
495
|
+
for i = 1, #reply, 2 do
|
496
|
+
Reqless.config.defaults[reply[i]] = reply[i + 1]
|
497
|
+
end
|
498
|
+
return Reqless.config.defaults
|
499
|
+
end
|
500
|
+
|
501
|
+
-- Set a configuration variable
|
502
|
+
Reqless.config.set = function(option, value)
|
503
|
+
assert(option, 'config.set(): Arg "option" missing')
|
504
|
+
assert(value , 'config.set(): Arg "value" missing')
|
505
|
+
-- Send out a log message
|
506
|
+
Reqless.publish('log', cjson.encode({
|
507
|
+
event = 'config_set',
|
508
|
+
option = option,
|
509
|
+
value = value
|
510
|
+
}))
|
511
|
+
|
512
|
+
redis.call('hset', 'ql:config', option, value)
|
513
|
+
end
|
514
|
+
|
515
|
+
-- Unset a configuration option
|
516
|
+
Reqless.config.unset = function(option)
|
517
|
+
assert(option, 'config.unset(): Arg "option" missing')
|
518
|
+
-- Send out a log message
|
519
|
+
Reqless.publish('log', cjson.encode({
|
520
|
+
event = 'config_unset',
|
521
|
+
option = option
|
522
|
+
}))
|
523
|
+
|
524
|
+
redis.call('hdel', 'ql:config', option)
|
525
|
+
end
|
526
|
+
-------------------------------------------------------------------------------
|
527
|
+
-- Job Class
|
528
|
+
--
|
529
|
+
-- It returns an object that represents the job with the provided JID
|
530
|
+
-------------------------------------------------------------------------------
|
531
|
+
|
532
|
+
-- This gets all the data associated with the job with the provided id. If the
|
533
|
+
-- job is not found, it returns nil. If found, it returns an object with the
|
534
|
+
-- appropriate properties
|
535
|
+
function ReqlessJob:data(...)
|
536
|
+
local job = redis.call(
|
537
|
+
'hmget', ReqlessJob.ns .. self.jid, 'jid', 'klass', 'state', 'queue',
|
538
|
+
'worker', 'priority', 'expires', 'retries', 'remaining', 'data',
|
539
|
+
'tags', 'failure', 'throttles', 'spawned_from_jid')
|
540
|
+
|
541
|
+
-- Return nil if we haven't found it
|
542
|
+
if not job[1] then
|
543
|
+
return nil
|
544
|
+
end
|
545
|
+
|
546
|
+
local data = {
|
547
|
+
jid = job[1],
|
548
|
+
klass = job[2],
|
549
|
+
state = job[3],
|
550
|
+
queue = job[4],
|
551
|
+
worker = job[5] or '',
|
552
|
+
tracked = redis.call('zscore', 'ql:tracked', self.jid) ~= false,
|
553
|
+
priority = tonumber(job[6]),
|
554
|
+
expires = tonumber(job[7]) or 0,
|
555
|
+
retries = tonumber(job[8]),
|
556
|
+
remaining = math.floor(tonumber(job[9])),
|
557
|
+
data = job[10],
|
558
|
+
tags = cjson.decode(job[11]),
|
559
|
+
history = self:history(),
|
560
|
+
failure = cjson.decode(job[12] or '{}'),
|
561
|
+
throttles = cjson.decode(job[13] or '[]'),
|
562
|
+
spawned_from_jid = job[14],
|
563
|
+
dependents = redis.call('smembers', ReqlessJob.ns .. self.jid .. '-dependents'),
|
564
|
+
dependencies = redis.call('smembers', ReqlessJob.ns .. self.jid .. '-dependencies'),
|
565
|
+
}
|
566
|
+
|
567
|
+
if #arg > 0 then
|
568
|
+
-- This section could probably be optimized, but I wanted the interface
|
569
|
+
-- in place first
|
570
|
+
local response = {}
|
571
|
+
for _, key in ipairs(arg) do
|
572
|
+
table.insert(response, data[key])
|
573
|
+
end
|
574
|
+
return response
|
575
|
+
end
|
576
|
+
|
577
|
+
return data
|
578
|
+
end
|
579
|
+
|
580
|
+
-- Complete a job and optionally put it in another queue, either scheduled or
|
581
|
+
-- to be considered waiting immediately. It can also optionally accept other
|
582
|
+
-- jids on which this job will be considered dependent before it's considered
|
583
|
+
-- valid.
|
584
|
+
--
|
585
|
+
-- The variable-length arguments may be pairs of the form:
|
586
|
+
--
|
587
|
+
-- ('next' , queue) : The queue to advance it to next
|
588
|
+
-- ('delay' , delay) : The delay for the next queue
|
589
|
+
-- ('depends', : Json of jobs it depends on in the new queue
|
590
|
+
-- '["jid1", "jid2", ...]')
|
591
|
+
---
|
592
|
+
function ReqlessJob:complete(now, worker, queue_name, raw_data, ...)
|
593
|
+
assert(worker, 'Complete(): Arg "worker" missing')
|
594
|
+
assert(queue_name , 'Complete(): Arg "queue_name" missing')
|
595
|
+
local data = assert(cjson.decode(raw_data),
|
596
|
+
'Complete(): Arg "data" missing or not JSON: ' .. tostring(raw_data))
|
597
|
+
|
598
|
+
-- Read in all the optional parameters
|
599
|
+
local options = {}
|
600
|
+
for i = 1, #arg, 2 do options[arg[i]] = arg[i + 1] end
|
601
|
+
|
602
|
+
-- Sanity check on optional args
|
603
|
+
local next_queue_name = options['next']
|
604
|
+
local delay = assert(tonumber(options['delay'] or 0))
|
605
|
+
local depends = assert(cjson.decode(options['depends'] or '[]'),
|
606
|
+
'Complete(): Arg "depends" not JSON: ' .. tostring(options['depends']))
|
607
|
+
|
608
|
+
-- Delay doesn't make sense without next_queue_name
|
609
|
+
if options['delay'] and next_queue_name == nil then
|
610
|
+
error('Complete(): "delay" cannot be used without a "next".')
|
611
|
+
end
|
612
|
+
|
613
|
+
-- Depends doesn't make sense without next_queue_name
|
614
|
+
if options['depends'] and next_queue_name == nil then
|
615
|
+
error('Complete(): "depends" cannot be used without a "next".')
|
616
|
+
end
|
617
|
+
|
618
|
+
-- The bin is midnight of the provided day
|
619
|
+
-- 24 * 60 * 60 = 86400
|
620
|
+
local bin = now - (now % 86400)
|
621
|
+
|
622
|
+
-- First things first, we should see if the worker still owns this job
|
623
|
+
local lastworker, state, priority, retries, current_queue = unpack(
|
624
|
+
redis.call('hmget', ReqlessJob.ns .. self.jid, 'worker', 'state',
|
625
|
+
'priority', 'retries', 'queue'))
|
626
|
+
|
627
|
+
if lastworker == false then
|
628
|
+
error('Complete(): Job does not exist')
|
629
|
+
elseif (state ~= 'running') then
|
630
|
+
error('Complete(): Job is not currently running: ' .. state)
|
631
|
+
elseif lastworker ~= worker then
|
632
|
+
error('Complete(): Job has been handed out to another worker: ' ..
|
633
|
+
tostring(lastworker))
|
634
|
+
elseif queue_name ~= current_queue then
|
635
|
+
error('Complete(): Job running in another queue: ' ..
|
636
|
+
tostring(current_queue))
|
637
|
+
end
|
638
|
+
|
639
|
+
-- Now we can assume that the worker does own the job. We need to
|
640
|
+
-- 1) Remove the job from the 'locks' from the old queue
|
641
|
+
-- 2) Enqueue it in the next stage if necessary
|
642
|
+
-- 3) Update the data
|
643
|
+
-- 4) Mark the job as completed, remove the worker, remove expires, and
|
644
|
+
-- update history
|
645
|
+
self:history(now, 'done')
|
646
|
+
|
647
|
+
redis.call('hset', ReqlessJob.ns .. self.jid, 'data', raw_data)
|
648
|
+
|
649
|
+
-- Remove the job from the previous queue
|
650
|
+
local queue = Reqless.queue(queue_name)
|
651
|
+
queue:remove_job(self.jid)
|
652
|
+
|
653
|
+
self:throttles_release(now)
|
654
|
+
|
655
|
+
-- Calculate how long the job has been running.
|
656
|
+
local popped_time = tonumber(
|
657
|
+
redis.call('hget', ReqlessJob.ns .. self.jid, 'time') or now)
|
658
|
+
local run_time = now - popped_time
|
659
|
+
queue:stat(now, 'run', run_time)
|
660
|
+
redis.call('hset', ReqlessJob.ns .. self.jid,
|
661
|
+
'time', string.format("%.20f", now))
|
662
|
+
|
663
|
+
-- Remove this job from the jobs that the worker that was running it has
|
664
|
+
redis.call('zrem', 'ql:w:' .. worker .. ':jobs', self.jid)
|
665
|
+
|
666
|
+
if redis.call('zscore', 'ql:tracked', self.jid) ~= false then
|
667
|
+
Reqless.publish('completed', self.jid)
|
668
|
+
end
|
669
|
+
|
670
|
+
if next_queue_name then
|
671
|
+
local next_queue = Reqless.queue(next_queue_name)
|
672
|
+
-- Send a message out to log
|
673
|
+
Reqless.publish('log', cjson.encode({
|
674
|
+
jid = self.jid,
|
675
|
+
event = 'advanced',
|
676
|
+
queue = queue_name,
|
677
|
+
to = next_queue_name,
|
678
|
+
}))
|
679
|
+
|
680
|
+
-- Enqueue the job
|
681
|
+
self:history(now, 'put', {queue = next_queue_name})
|
682
|
+
|
683
|
+
-- We're going to make sure that this queue is in the
|
684
|
+
-- set of known queues
|
685
|
+
if redis.call('zscore', 'ql:queues', next_queue_name) == false then
|
686
|
+
redis.call('zadd', 'ql:queues', now, next_queue_name)
|
687
|
+
end
|
688
|
+
|
689
|
+
redis.call('hmset', ReqlessJob.ns .. self.jid,
|
690
|
+
'state', 'waiting',
|
691
|
+
'worker', '',
|
692
|
+
'failure', '{}',
|
693
|
+
'queue', next_queue_name,
|
694
|
+
'expires', 0,
|
695
|
+
'remaining', tonumber(retries))
|
696
|
+
|
697
|
+
if (delay > 0) and (#depends == 0) then
|
698
|
+
next_queue.scheduled.add(now + delay, self.jid)
|
699
|
+
return 'scheduled'
|
700
|
+
end
|
701
|
+
|
702
|
+
-- These are the jids we legitimately have to wait on
|
703
|
+
local count = 0
|
704
|
+
for _, j in ipairs(depends) do
|
705
|
+
-- Make sure it's something other than 'nil' or complete.
|
706
|
+
local state = redis.call('hget', ReqlessJob.ns .. j, 'state')
|
707
|
+
if (state and state ~= 'complete') then
|
708
|
+
count = count + 1
|
709
|
+
redis.call(
|
710
|
+
'sadd', ReqlessJob.ns .. j .. '-dependents',self.jid)
|
711
|
+
redis.call(
|
712
|
+
'sadd', ReqlessJob.ns .. self.jid .. '-dependencies', j)
|
713
|
+
end
|
714
|
+
end
|
715
|
+
if count > 0 then
|
716
|
+
next_queue.depends.add(now, self.jid)
|
717
|
+
redis.call('hset', ReqlessJob.ns .. self.jid, 'state', 'depends')
|
718
|
+
if delay > 0 then
|
719
|
+
-- We've already put it in 'depends'. Now, we must just save the data
|
720
|
+
-- for when it's scheduled
|
721
|
+
next_queue.depends.add(now, self.jid)
|
722
|
+
redis.call('hset', ReqlessJob.ns .. self.jid, 'scheduled', now + delay)
|
723
|
+
end
|
724
|
+
return 'depends'
|
725
|
+
end
|
726
|
+
|
727
|
+
next_queue.work.add(now, priority, self.jid)
|
728
|
+
return 'waiting'
|
729
|
+
end
|
730
|
+
-- Send a message out to log
|
731
|
+
Reqless.publish('log', cjson.encode({
|
732
|
+
jid = self.jid,
|
733
|
+
event = 'completed',
|
734
|
+
queue = queue_name,
|
735
|
+
}))
|
736
|
+
|
737
|
+
redis.call('hmset', ReqlessJob.ns .. self.jid,
|
738
|
+
'state', 'complete',
|
739
|
+
'worker', '',
|
740
|
+
'failure', '{}',
|
741
|
+
'queue', '',
|
742
|
+
'expires', 0,
|
743
|
+
'remaining', tonumber(retries))
|
744
|
+
|
745
|
+
-- Do the completion dance
|
746
|
+
local count = Reqless.config.get('jobs-history-count')
|
747
|
+
local time = Reqless.config.get('jobs-history')
|
748
|
+
|
749
|
+
-- These are the default values
|
750
|
+
count = tonumber(count or 50000)
|
751
|
+
time = tonumber(time or 7 * 24 * 60 * 60)
|
752
|
+
|
753
|
+
-- Schedule this job for destructination eventually
|
754
|
+
redis.call('zadd', 'ql:completed', now, self.jid)
|
755
|
+
|
756
|
+
-- Now look at the expired job data. First, based on the current time
|
757
|
+
local jids = redis.call('zrangebyscore', 'ql:completed', 0, now - time)
|
758
|
+
-- Any jobs that need to be expired... delete
|
759
|
+
for _, jid in ipairs(jids) do
|
760
|
+
Reqless.job(jid):delete()
|
761
|
+
end
|
762
|
+
|
763
|
+
-- And now remove those from the queued-for-cleanup queue
|
764
|
+
redis.call('zremrangebyscore', 'ql:completed', 0, now - time)
|
765
|
+
|
766
|
+
-- Now take the all by the most recent 'count' ids
|
767
|
+
jids = redis.call('zrange', 'ql:completed', 0, (-1-count))
|
768
|
+
for _, jid in ipairs(jids) do
|
769
|
+
Reqless.job(jid):delete()
|
770
|
+
end
|
771
|
+
redis.call('zremrangebyrank', 'ql:completed', 0, (-1-count))
|
772
|
+
|
773
|
+
-- Alright, if this has any dependents, then we should go ahead
|
774
|
+
-- and unstick those guys.
|
775
|
+
for _, j in ipairs(redis.call(
|
776
|
+
'smembers', ReqlessJob.ns .. self.jid .. '-dependents')) do
|
777
|
+
redis.call('srem', ReqlessJob.ns .. j .. '-dependencies', self.jid)
|
778
|
+
if redis.call(
|
779
|
+
'scard', ReqlessJob.ns .. j .. '-dependencies') == 0 then
|
780
|
+
local other_queue_name, priority, scheduled = unpack(
|
781
|
+
redis.call('hmget', ReqlessJob.ns .. j, 'queue', 'priority', 'scheduled'))
|
782
|
+
if other_queue_name then
|
783
|
+
local other_queue = Reqless.queue(other_queue_name)
|
784
|
+
other_queue.depends.remove(j)
|
785
|
+
if scheduled then
|
786
|
+
other_queue.scheduled.add(scheduled, j)
|
787
|
+
redis.call('hset', ReqlessJob.ns .. j, 'state', 'scheduled')
|
788
|
+
redis.call('hdel', ReqlessJob.ns .. j, 'scheduled')
|
789
|
+
else
|
790
|
+
other_queue.work.add(now, priority, j)
|
791
|
+
redis.call('hset', ReqlessJob.ns .. j, 'state', 'waiting')
|
792
|
+
end
|
793
|
+
end
|
794
|
+
end
|
795
|
+
end
|
796
|
+
|
797
|
+
-- Delete our dependents key
|
798
|
+
redis.call('del', ReqlessJob.ns .. self.jid .. '-dependents')
|
799
|
+
|
800
|
+
return 'complete'
|
801
|
+
end
|
802
|
+
|
803
|
+
-- Fail(now, worker, group, message, [data])
|
804
|
+
-- -------------------------------------------------
|
805
|
+
-- Mark the particular job as failed, with the provided group, and a more
|
806
|
+
-- specific message. By `group`, we mean some phrase that might be one of
|
807
|
+
-- several categorical modes of failure. The `message` is something more
|
808
|
+
-- job-specific, like perhaps a traceback.
|
809
|
+
--
|
810
|
+
-- This method should __not__ be used to note that a job has been dropped or
|
811
|
+
-- has failed in a transient way. This method __should__ be used to note that
|
812
|
+
-- a job has something really wrong with it that must be remedied.
|
813
|
+
--
|
814
|
+
-- The motivation behind the `group` is so that similar errors can be grouped
|
815
|
+
-- together. Optionally, updated data can be provided for the job. A job in
|
816
|
+
-- any state can be marked as failed. If it has been given to a worker as a
|
817
|
+
-- job, then its subsequent requests to heartbeat or complete that job will
|
818
|
+
-- fail. Failed jobs are kept until they are canceled or completed.
|
819
|
+
--
|
820
|
+
-- __Returns__ the id of the failed job if successful, or `False` on failure.
|
821
|
+
--
|
822
|
+
-- Args:
|
823
|
+
-- 1) jid
|
824
|
+
-- 2) worker
|
825
|
+
-- 3) group
|
826
|
+
-- 4) message
|
827
|
+
-- 5) the current time
|
828
|
+
-- 6) [data]
|
829
|
+
function ReqlessJob:fail(now, worker, group, message, data)
|
830
|
+
local worker = assert(worker , 'Fail(): Arg "worker" missing')
|
831
|
+
local group = assert(group , 'Fail(): Arg "group" missing')
|
832
|
+
local message = assert(message , 'Fail(): Arg "message" missing')
|
833
|
+
|
834
|
+
-- The bin is midnight of the provided day
|
835
|
+
-- 24 * 60 * 60 = 86400
|
836
|
+
local bin = now - (now % 86400)
|
837
|
+
|
838
|
+
if data then
|
839
|
+
data = cjson.decode(data)
|
840
|
+
end
|
841
|
+
|
842
|
+
-- First things first, we should get the history
|
843
|
+
local queue_name, state, oldworker = unpack(redis.call(
|
844
|
+
'hmget', ReqlessJob.ns .. self.jid, 'queue', 'state', 'worker'))
|
845
|
+
|
846
|
+
-- If the job has been completed, we cannot fail it
|
847
|
+
if not state then
|
848
|
+
error('Fail(): Job does not exist')
|
849
|
+
elseif state ~= 'running' then
|
850
|
+
error('Fail(): Job not currently running: ' .. state)
|
851
|
+
elseif worker ~= oldworker then
|
852
|
+
error('Fail(): Job running with another worker: ' .. oldworker)
|
853
|
+
end
|
854
|
+
|
855
|
+
-- Send out a log message
|
856
|
+
Reqless.publish('log', cjson.encode({
|
857
|
+
jid = self.jid,
|
858
|
+
event = 'failed',
|
859
|
+
worker = worker,
|
860
|
+
group = group,
|
861
|
+
message = message,
|
862
|
+
}))
|
863
|
+
|
864
|
+
if redis.call('zscore', 'ql:tracked', self.jid) ~= false then
|
865
|
+
Reqless.publish('failed', self.jid)
|
866
|
+
end
|
867
|
+
|
868
|
+
-- Remove this job from the jobs that the worker that was running it has
|
869
|
+
redis.call('zrem', 'ql:w:' .. worker .. ':jobs', self.jid)
|
870
|
+
|
871
|
+
-- Now, take the element of the history for which our provided worker is
|
872
|
+
-- the worker, and update 'failed'
|
873
|
+
self:history(now, 'failed', {worker = worker, group = group})
|
874
|
+
|
875
|
+
-- Increment the number of failures for that queue for the
|
876
|
+
-- given day.
|
877
|
+
redis.call('hincrby', 'ql:s:stats:' .. bin .. ':' .. queue_name, 'failures', 1)
|
878
|
+
redis.call('hincrby', 'ql:s:stats:' .. bin .. ':' .. queue_name, 'failed' , 1)
|
879
|
+
|
880
|
+
-- Now remove the instance from the schedule, and work queues for the
|
881
|
+
-- queue it's in
|
882
|
+
local queue = Reqless.queue(queue_name)
|
883
|
+
queue:remove_job(self.jid)
|
884
|
+
|
885
|
+
-- The reason that this appears here is that the above will fail if the
|
886
|
+
-- job doesn't exist
|
887
|
+
if data then
|
888
|
+
redis.call('hset', ReqlessJob.ns .. self.jid, 'data', cjson.encode(data))
|
889
|
+
end
|
890
|
+
|
891
|
+
redis.call('hmset', ReqlessJob.ns .. self.jid,
|
892
|
+
'state', 'failed',
|
893
|
+
'worker', '',
|
894
|
+
'expires', '',
|
895
|
+
'failure', cjson.encode({
|
896
|
+
group = group,
|
897
|
+
message = message,
|
898
|
+
when = math.floor(now),
|
899
|
+
worker = worker
|
900
|
+
}))
|
901
|
+
|
902
|
+
self:throttles_release(now)
|
903
|
+
|
904
|
+
-- Add this group of failure to the list of failures
|
905
|
+
redis.call('sadd', 'ql:failures', group)
|
906
|
+
-- And add this particular instance to the failed groups
|
907
|
+
redis.call('lpush', 'ql:f:' .. group, self.jid)
|
908
|
+
|
909
|
+
-- Here is where we'd increment stats about the particular stage
|
910
|
+
-- and possibly the workers
|
911
|
+
|
912
|
+
return self.jid
|
913
|
+
end
|
914
|
+
|
915
|
+
-- retry(now, queue_name, worker, [delay, [group, [message]]])
|
916
|
+
-- ------------------------------------------
|
917
|
+
-- This script accepts jid, queue, worker and delay for retrying a job. This
|
918
|
+
-- is similar in functionality to `put`, except that this counts against the
|
919
|
+
-- retries a job has for a stage.
|
920
|
+
--
|
921
|
+
-- Throws an exception if:
|
922
|
+
-- - the worker is not the worker with a lock on the job
|
923
|
+
-- - the job is not actually running
|
924
|
+
--
|
925
|
+
-- Otherwise, it returns the number of retries remaining. If the allowed
|
926
|
+
-- retries have been exhausted, then it is automatically failed, and a negative
|
927
|
+
-- number is returned.
|
928
|
+
--
|
929
|
+
-- If a group and message is provided, then if the retries are exhausted, then
|
930
|
+
-- the provided group and message will be used in place of the default
|
931
|
+
-- messaging about retries in the particular queue being exhausted
|
932
|
+
function ReqlessJob:retry(now, queue_name, worker, delay, group, message)
|
933
|
+
assert(queue_name , 'Retry(): Arg "queue_name" missing')
|
934
|
+
assert(worker, 'Retry(): Arg "worker" missing')
|
935
|
+
delay = assert(tonumber(delay or 0),
|
936
|
+
'Retry(): Arg "delay" not a number: ' .. tostring(delay))
|
937
|
+
|
938
|
+
-- Let's see what the old priority, and tags were
|
939
|
+
local old_queue_name, state, retries, oldworker, priority, failure = unpack(
|
940
|
+
redis.call('hmget', ReqlessJob.ns .. self.jid, 'queue', 'state',
|
941
|
+
'retries', 'worker', 'priority', 'failure'))
|
942
|
+
|
943
|
+
-- If this isn't the worker that owns
|
944
|
+
if oldworker == false then
|
945
|
+
error('Retry(): Job does not exist')
|
946
|
+
elseif state ~= 'running' then
|
947
|
+
error('Retry(): Job is not currently running: ' .. state)
|
948
|
+
elseif oldworker ~= worker then
|
949
|
+
error('Retry(): Job has been given to another worker: ' .. oldworker)
|
950
|
+
end
|
951
|
+
|
952
|
+
-- For each of these, decrement their retries. If any of them
|
953
|
+
-- have exhausted their retries, then we should mark them as
|
954
|
+
-- failed.
|
955
|
+
local remaining = tonumber(redis.call(
|
956
|
+
'hincrby', ReqlessJob.ns .. self.jid, 'remaining', -1))
|
957
|
+
redis.call('hdel', ReqlessJob.ns .. self.jid, 'grace')
|
958
|
+
|
959
|
+
-- Remove it from the locks key of the old queue
|
960
|
+
Reqless.queue(old_queue_name).locks.remove(self.jid)
|
961
|
+
|
962
|
+
-- Release the throttle for the job
|
963
|
+
self:throttles_release(now)
|
964
|
+
|
965
|
+
-- Remove this job from the worker that was previously working it
|
966
|
+
redis.call('zrem', 'ql:w:' .. worker .. ':jobs', self.jid)
|
967
|
+
|
968
|
+
if remaining < 0 then
|
969
|
+
-- Now remove the instance from the schedule, and work queues for the
|
970
|
+
-- queue it's in
|
971
|
+
local group = group or 'failed-retries-' .. queue_name
|
972
|
+
self:history(now, 'failed-retries', {group = group})
|
973
|
+
|
974
|
+
redis.call('hmset', ReqlessJob.ns .. self.jid, 'state', 'failed',
|
975
|
+
'worker', '',
|
976
|
+
'expires', '')
|
977
|
+
-- If the failure has not already been set, then set it
|
978
|
+
if group ~= nil and message ~= nil then
|
979
|
+
redis.call('hset', ReqlessJob.ns .. self.jid,
|
980
|
+
'failure', cjson.encode({
|
981
|
+
group = group,
|
982
|
+
message = message,
|
983
|
+
when = math.floor(now),
|
984
|
+
worker = worker
|
985
|
+
})
|
986
|
+
)
|
987
|
+
else
|
988
|
+
redis.call('hset', ReqlessJob.ns .. self.jid,
|
989
|
+
'failure', cjson.encode({
|
990
|
+
group = group,
|
991
|
+
message = 'Job exhausted retries in queue "' .. old_queue_name .. '"',
|
992
|
+
when = now,
|
993
|
+
worker = unpack(self:data('worker'))
|
994
|
+
}))
|
995
|
+
end
|
996
|
+
|
997
|
+
-- Add this type of failure to the list of failures
|
998
|
+
redis.call('sadd', 'ql:failures', group)
|
999
|
+
-- And add this particular instance to the failed types
|
1000
|
+
redis.call('lpush', 'ql:f:' .. group, self.jid)
|
1001
|
+
-- Increment the count of the failed jobs
|
1002
|
+
local bin = now - (now % 86400)
|
1003
|
+
redis.call('hincrby', 'ql:s:stats:' .. bin .. ':' .. queue_name, 'failures', 1)
|
1004
|
+
redis.call('hincrby', 'ql:s:stats:' .. bin .. ':' .. queue_name, 'failed' , 1)
|
1005
|
+
else
|
1006
|
+
-- Put it in the queue again with a delay. Like put()
|
1007
|
+
local queue = Reqless.queue(queue_name)
|
1008
|
+
if delay > 0 then
|
1009
|
+
queue.scheduled.add(now + delay, self.jid)
|
1010
|
+
redis.call('hset', ReqlessJob.ns .. self.jid, 'state', 'scheduled')
|
1011
|
+
else
|
1012
|
+
queue.work.add(now, priority, self.jid)
|
1013
|
+
redis.call('hset', ReqlessJob.ns .. self.jid, 'state', 'waiting')
|
1014
|
+
end
|
1015
|
+
|
1016
|
+
-- If a group and a message was provided, then we should save it
|
1017
|
+
if group ~= nil and message ~= nil then
|
1018
|
+
redis.call('hset', ReqlessJob.ns .. self.jid,
|
1019
|
+
'failure', cjson.encode({
|
1020
|
+
group = group,
|
1021
|
+
message = message,
|
1022
|
+
when = math.floor(now),
|
1023
|
+
worker = worker
|
1024
|
+
})
|
1025
|
+
)
|
1026
|
+
end
|
1027
|
+
end
|
1028
|
+
|
1029
|
+
return math.floor(remaining)
|
1030
|
+
end
|
1031
|
+
|
1032
|
+
-- Depends(jid, 'on', [jid, [jid, [...]]]
|
1033
|
+
-- Depends(jid, 'off', [jid, [jid, [...]]])
|
1034
|
+
-- Depends(jid, 'off', 'all')
|
1035
|
+
-------------------------------------------------------------------------------
|
1036
|
+
-- Add or remove dependencies a job has. If 'on' is provided, the provided
|
1037
|
+
-- jids are added as dependencies. If 'off' and 'all' are provided, then all
|
1038
|
+
-- the current dependencies are removed. If 'off' is provided and the next
|
1039
|
+
-- argument is not 'all', then those jids are removed as dependencies.
|
1040
|
+
--
|
1041
|
+
-- If a job is not already in the 'depends' state, then this call will raise an
|
1042
|
+
-- error. Otherwise, it will return true.
|
1043
|
+
function ReqlessJob:depends(now, command, ...)
|
1044
|
+
assert(command, 'Depends(): Arg "command" missing')
|
1045
|
+
if command ~= 'on' and command ~= 'off' then
|
1046
|
+
error('Depends(): Argument "command" must be "on" or "off"')
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
local state = redis.call('hget', ReqlessJob.ns .. self.jid, 'state')
|
1050
|
+
if state ~= 'depends' then
|
1051
|
+
error('Depends(): Job ' .. self.jid ..
|
1052
|
+
' not in the depends state: ' .. tostring(state))
|
1053
|
+
end
|
1054
|
+
|
1055
|
+
if command == 'on' then
|
1056
|
+
-- These are the jids we legitimately have to wait on
|
1057
|
+
for _, j in ipairs(arg) do
|
1058
|
+
-- Make sure it's something other than 'nil' or complete.
|
1059
|
+
local state = redis.call('hget', ReqlessJob.ns .. j, 'state')
|
1060
|
+
if (state and state ~= 'complete') then
|
1061
|
+
redis.call(
|
1062
|
+
'sadd', ReqlessJob.ns .. j .. '-dependents' , self.jid)
|
1063
|
+
redis.call(
|
1064
|
+
'sadd', ReqlessJob.ns .. self.jid .. '-dependencies', j)
|
1065
|
+
end
|
1066
|
+
end
|
1067
|
+
return true
|
1068
|
+
end
|
1069
|
+
|
1070
|
+
if arg[1] == 'all' then
|
1071
|
+
for _, j in ipairs(redis.call(
|
1072
|
+
'smembers', ReqlessJob.ns .. self.jid .. '-dependencies')) do
|
1073
|
+
redis.call('srem', ReqlessJob.ns .. j .. '-dependents', self.jid)
|
1074
|
+
end
|
1075
|
+
redis.call('del', ReqlessJob.ns .. self.jid .. '-dependencies')
|
1076
|
+
local queue_name, priority = unpack(redis.call(
|
1077
|
+
'hmget', ReqlessJob.ns .. self.jid, 'queue', 'priority'))
|
1078
|
+
if queue_name then
|
1079
|
+
local queue = Reqless.queue(queue_name)
|
1080
|
+
queue.depends.remove(self.jid)
|
1081
|
+
queue.work.add(now, priority, self.jid)
|
1082
|
+
redis.call('hset', ReqlessJob.ns .. self.jid, 'state', 'waiting')
|
1083
|
+
end
|
1084
|
+
else
|
1085
|
+
for _, j in ipairs(arg) do
|
1086
|
+
redis.call('srem', ReqlessJob.ns .. j .. '-dependents', self.jid)
|
1087
|
+
redis.call(
|
1088
|
+
'srem', ReqlessJob.ns .. self.jid .. '-dependencies', j)
|
1089
|
+
if redis.call('scard',
|
1090
|
+
ReqlessJob.ns .. self.jid .. '-dependencies') == 0 then
|
1091
|
+
local queue_name, priority = unpack(redis.call(
|
1092
|
+
'hmget', ReqlessJob.ns .. self.jid, 'queue', 'priority'))
|
1093
|
+
if queue_name then
|
1094
|
+
local queue = Reqless.queue(queue_name)
|
1095
|
+
queue.depends.remove(self.jid)
|
1096
|
+
queue.work.add(now, priority, self.jid)
|
1097
|
+
redis.call('hset',
|
1098
|
+
ReqlessJob.ns .. self.jid, 'state', 'waiting')
|
1099
|
+
end
|
1100
|
+
end
|
1101
|
+
end
|
1102
|
+
end
|
1103
|
+
return true
|
1104
|
+
end
|
1105
|
+
|
1106
|
+
-- Heartbeat
|
1107
|
+
------------
|
1108
|
+
-- Renew this worker's lock on this job. Throws an exception if:
|
1109
|
+
-- - the job's been given to another worker
|
1110
|
+
-- - the job's been completed
|
1111
|
+
-- - the job's been canceled
|
1112
|
+
-- - the job's not running
|
1113
|
+
function ReqlessJob:heartbeat(now, worker, data)
|
1114
|
+
assert(worker, 'Heatbeat(): Arg "worker" missing')
|
1115
|
+
|
1116
|
+
-- We should find the heartbeat interval for this queue
|
1117
|
+
-- heartbeat. First, though, we need to find the queue
|
1118
|
+
-- this particular job is in
|
1119
|
+
local queue_name = redis.call('hget', ReqlessJob.ns .. self.jid, 'queue') or ''
|
1120
|
+
local expires = now + tonumber(
|
1121
|
+
Reqless.config.get(queue_name .. '-heartbeat') or
|
1122
|
+
Reqless.config.get('heartbeat', 60))
|
1123
|
+
|
1124
|
+
if data then
|
1125
|
+
data = cjson.decode(data)
|
1126
|
+
end
|
1127
|
+
|
1128
|
+
-- First, let's see if the worker still owns this job, and there is a
|
1129
|
+
-- worker
|
1130
|
+
local job_worker, state = unpack(
|
1131
|
+
redis.call('hmget', ReqlessJob.ns .. self.jid, 'worker', 'state'))
|
1132
|
+
if job_worker == false then
|
1133
|
+
-- This means the job doesn't exist
|
1134
|
+
error('Heartbeat(): Job does not exist')
|
1135
|
+
elseif state ~= 'running' then
|
1136
|
+
error('Heartbeat(): Job not currently running: ' .. state)
|
1137
|
+
elseif job_worker ~= worker or #job_worker == 0 then
|
1138
|
+
error('Heartbeat(): Job given out to another worker: ' .. job_worker)
|
1139
|
+
end
|
1140
|
+
|
1141
|
+
-- Otherwise, optionally update the user data, and the heartbeat
|
1142
|
+
if data then
|
1143
|
+
-- I don't know if this is wise, but I'm decoding and encoding
|
1144
|
+
-- the user data to hopefully ensure its sanity
|
1145
|
+
redis.call('hmset', ReqlessJob.ns .. self.jid, 'expires',
|
1146
|
+
expires, 'worker', worker, 'data', cjson.encode(data))
|
1147
|
+
else
|
1148
|
+
redis.call('hmset', ReqlessJob.ns .. self.jid,
|
1149
|
+
'expires', expires, 'worker', worker)
|
1150
|
+
end
|
1151
|
+
|
1152
|
+
-- Update when this job was last updated on that worker
|
1153
|
+
-- Add this job to the list of jobs handled by this worker
|
1154
|
+
redis.call('zadd', 'ql:w:' .. worker .. ':jobs', expires, self.jid)
|
1155
|
+
|
1156
|
+
-- And now we should just update the locks
|
1157
|
+
local queue = Reqless.queue(
|
1158
|
+
redis.call('hget', ReqlessJob.ns .. self.jid, 'queue'))
|
1159
|
+
queue.locks.add(expires, self.jid)
|
1160
|
+
return expires
|
1161
|
+
end
|
1162
|
+
|
1163
|
+
-- Priority
|
1164
|
+
-- --------
|
1165
|
+
-- Update the priority of this job. If the job doesn't exist, throws an
|
1166
|
+
-- exception
|
1167
|
+
function ReqlessJob:priority(priority)
|
1168
|
+
priority = assert(tonumber(priority),
|
1169
|
+
'Priority(): Arg "priority" missing or not a number: ' ..
|
1170
|
+
tostring(priority))
|
1171
|
+
|
1172
|
+
-- Get the queue the job is currently in, if any
|
1173
|
+
local queue_name = redis.call('hget', ReqlessJob.ns .. self.jid, 'queue')
|
1174
|
+
|
1175
|
+
if queue_name == nil then
|
1176
|
+
-- If the job doesn't exist, throw an error
|
1177
|
+
error('Priority(): Job ' .. self.jid .. ' does not exist')
|
1178
|
+
end
|
1179
|
+
|
1180
|
+
-- See if the job is a candidate for updating its priority in the queue it's
|
1181
|
+
-- currently in
|
1182
|
+
if queue_name ~= '' then
|
1183
|
+
local queue = Reqless.queue(queue_name)
|
1184
|
+
if queue.work.score(self.jid) then
|
1185
|
+
queue.work.add(0, priority, self.jid)
|
1186
|
+
end
|
1187
|
+
end
|
1188
|
+
|
1189
|
+
redis.call('hset', ReqlessJob.ns .. self.jid, 'priority', priority)
|
1190
|
+
return priority
|
1191
|
+
end
|
1192
|
+
|
1193
|
+
-- Update the jobs' attributes with the provided dictionary
|
1194
|
+
function ReqlessJob:update(data)
|
1195
|
+
local tmp = {}
|
1196
|
+
for k, v in pairs(data) do
|
1197
|
+
table.insert(tmp, k)
|
1198
|
+
table.insert(tmp, v)
|
1199
|
+
end
|
1200
|
+
redis.call('hmset', ReqlessJob.ns .. self.jid, unpack(tmp))
|
1201
|
+
end
|
1202
|
+
|
1203
|
+
-- Times out the job now rather than when its lock is normally set to expire
|
1204
|
+
function ReqlessJob:timeout(now)
|
1205
|
+
local queue_name, state, worker = unpack(redis.call('hmget',
|
1206
|
+
ReqlessJob.ns .. self.jid, 'queue', 'state', 'worker'))
|
1207
|
+
if queue_name == nil then
|
1208
|
+
error('Timeout(): Job does not exist')
|
1209
|
+
elseif state ~= 'running' then
|
1210
|
+
error('Timeout(): Job ' .. self.jid .. ' not running')
|
1211
|
+
end
|
1212
|
+
-- Time out the job
|
1213
|
+
self:history(now, 'timed-out')
|
1214
|
+
local queue = Reqless.queue(queue_name)
|
1215
|
+
queue.locks.remove(self.jid)
|
1216
|
+
|
1217
|
+
-- Release acquired throttles
|
1218
|
+
self:throttles_release(now)
|
1219
|
+
|
1220
|
+
queue.work.add(now, math.huge, self.jid)
|
1221
|
+
redis.call('hmset', ReqlessJob.ns .. self.jid,
|
1222
|
+
'state', 'stalled', 'expires', 0, 'worker', '')
|
1223
|
+
local encoded = cjson.encode({
|
1224
|
+
jid = self.jid,
|
1225
|
+
event = 'lock_lost',
|
1226
|
+
worker = worker,
|
1227
|
+
})
|
1228
|
+
Reqless.publish('w:' .. worker, encoded)
|
1229
|
+
Reqless.publish('log', encoded)
|
1230
|
+
return queue_name
|
1231
|
+
end
|
1232
|
+
|
1233
|
+
-- Return whether or not this job exists
|
1234
|
+
function ReqlessJob:exists()
|
1235
|
+
return redis.call('exists', ReqlessJob.ns .. self.jid) == 1
|
1236
|
+
end
|
1237
|
+
|
1238
|
+
-- Get or append to history
|
1239
|
+
function ReqlessJob:history(now, what, item)
|
1240
|
+
-- First, check if there's an old-style history, and update it if there is
|
1241
|
+
local history = redis.call('hget', ReqlessJob.ns .. self.jid, 'history')
|
1242
|
+
if history then
|
1243
|
+
history = cjson.decode(history)
|
1244
|
+
for _, value in ipairs(history) do
|
1245
|
+
redis.call('rpush', ReqlessJob.ns .. self.jid .. '-history',
|
1246
|
+
cjson.encode({math.floor(value.put), 'put', {queue = value.queue}}))
|
1247
|
+
|
1248
|
+
-- If there's any popped time
|
1249
|
+
if value.popped then
|
1250
|
+
redis.call('rpush', ReqlessJob.ns .. self.jid .. '-history',
|
1251
|
+
cjson.encode({math.floor(value.popped), 'popped',
|
1252
|
+
{worker = value.worker}}))
|
1253
|
+
end
|
1254
|
+
|
1255
|
+
-- If there's any failure
|
1256
|
+
if value.failed then
|
1257
|
+
redis.call('rpush', ReqlessJob.ns .. self.jid .. '-history',
|
1258
|
+
cjson.encode(
|
1259
|
+
{math.floor(value.failed), 'failed', nil}))
|
1260
|
+
end
|
1261
|
+
|
1262
|
+
-- If it was completed
|
1263
|
+
if value.done then
|
1264
|
+
redis.call('rpush', ReqlessJob.ns .. self.jid .. '-history',
|
1265
|
+
cjson.encode(
|
1266
|
+
{math.floor(value.done), 'done', nil}))
|
1267
|
+
end
|
1268
|
+
end
|
1269
|
+
-- With all this ported forward, delete the old-style history
|
1270
|
+
redis.call('hdel', ReqlessJob.ns .. self.jid, 'history')
|
1271
|
+
end
|
1272
|
+
|
1273
|
+
-- Now to the meat of the function
|
1274
|
+
if what == nil then
|
1275
|
+
-- Get the history
|
1276
|
+
local response = {}
|
1277
|
+
for _, value in ipairs(redis.call('lrange',
|
1278
|
+
ReqlessJob.ns .. self.jid .. '-history', 0, -1)) do
|
1279
|
+
value = cjson.decode(value)
|
1280
|
+
local dict = value[3] or {}
|
1281
|
+
dict['when'] = value[1]
|
1282
|
+
dict['what'] = value[2]
|
1283
|
+
table.insert(response, dict)
|
1284
|
+
end
|
1285
|
+
return response
|
1286
|
+
end
|
1287
|
+
|
1288
|
+
-- Append to the history. If the length of the history should be limited,
|
1289
|
+
-- then we'll truncate it.
|
1290
|
+
local count = tonumber(Reqless.config.get('max-job-history', 100))
|
1291
|
+
if count > 0 then
|
1292
|
+
-- We'll always keep the first item around
|
1293
|
+
local obj = redis.call('lpop', ReqlessJob.ns .. self.jid .. '-history')
|
1294
|
+
redis.call('ltrim', ReqlessJob.ns .. self.jid .. '-history', -count + 2, -1)
|
1295
|
+
if obj ~= nil and obj ~= false then
|
1296
|
+
redis.call('lpush', ReqlessJob.ns .. self.jid .. '-history', obj)
|
1297
|
+
end
|
1298
|
+
end
|
1299
|
+
return redis.call('rpush', ReqlessJob.ns .. self.jid .. '-history',
|
1300
|
+
cjson.encode({math.floor(now), what, item}))
|
1301
|
+
end
|
1302
|
+
|
1303
|
+
function ReqlessJob:throttles_release(now)
|
1304
|
+
local throttles = redis.call('hget', ReqlessJob.ns .. self.jid, 'throttles')
|
1305
|
+
throttles = cjson.decode(throttles or '[]')
|
1306
|
+
|
1307
|
+
for _, tid in ipairs(throttles) do
|
1308
|
+
Reqless.throttle(tid):release(now, self.jid)
|
1309
|
+
end
|
1310
|
+
end
|
1311
|
+
|
1312
|
+
function ReqlessJob:throttles_available()
|
1313
|
+
for _, tid in ipairs(self:throttles()) do
|
1314
|
+
if not Reqless.throttle(tid):available() then
|
1315
|
+
return false
|
1316
|
+
end
|
1317
|
+
end
|
1318
|
+
|
1319
|
+
return true
|
1320
|
+
end
|
1321
|
+
|
1322
|
+
function ReqlessJob:throttles_acquire(now)
|
1323
|
+
if not self:throttles_available() then
|
1324
|
+
return false
|
1325
|
+
end
|
1326
|
+
|
1327
|
+
for _, tid in ipairs(self:throttles()) do
|
1328
|
+
Reqless.throttle(tid):acquire(self.jid)
|
1329
|
+
end
|
1330
|
+
|
1331
|
+
return true
|
1332
|
+
end
|
1333
|
+
|
1334
|
+
-- Finds the first unavailable throttle and adds the job to its pending job set.
|
1335
|
+
function ReqlessJob:throttle(now)
|
1336
|
+
for _, tid in ipairs(self:throttles()) do
|
1337
|
+
local throttle = Reqless.throttle(tid)
|
1338
|
+
if not throttle:available() then
|
1339
|
+
throttle:pend(now, self.jid)
|
1340
|
+
return
|
1341
|
+
end
|
1342
|
+
end
|
1343
|
+
end
|
1344
|
+
|
1345
|
+
function ReqlessJob:throttles()
|
1346
|
+
-- memoize throttles for the job.
|
1347
|
+
if not self._throttles then
|
1348
|
+
self._throttles = cjson.decode(redis.call('hget', ReqlessJob.ns .. self.jid, 'throttles') or '[]')
|
1349
|
+
end
|
1350
|
+
|
1351
|
+
return self._throttles
|
1352
|
+
end
|
1353
|
+
|
1354
|
+
-- Completely removes all the data
|
1355
|
+
-- associated with this job, use
|
1356
|
+
-- with care.
|
1357
|
+
function ReqlessJob:delete()
|
1358
|
+
local tags = redis.call('hget', ReqlessJob.ns .. self.jid, 'tags') or '[]'
|
1359
|
+
tags = cjson.decode(tags)
|
1360
|
+
-- remove the jid from each tag
|
1361
|
+
for _, tag in ipairs(tags) do
|
1362
|
+
self:remove_tag(tag)
|
1363
|
+
end
|
1364
|
+
-- Delete the job's data
|
1365
|
+
redis.call('del', ReqlessJob.ns .. self.jid)
|
1366
|
+
-- Delete the job's history
|
1367
|
+
redis.call('del', ReqlessJob.ns .. self.jid .. '-history')
|
1368
|
+
-- Delete any notion of dependencies it has
|
1369
|
+
redis.call('del', ReqlessJob.ns .. self.jid .. '-dependencies')
|
1370
|
+
end
|
1371
|
+
|
1372
|
+
-- Inserts the jid into the specified tag.
|
1373
|
+
-- This should probably be moved to its own tag
|
1374
|
+
-- object.
|
1375
|
+
function ReqlessJob:insert_tag(now, tag)
|
1376
|
+
redis.call('zadd', 'ql:t:' .. tag, now, self.jid)
|
1377
|
+
redis.call('zincrby', 'ql:tags', 1, tag)
|
1378
|
+
end
|
1379
|
+
|
1380
|
+
-- Removes the jid from the specified tag.
|
1381
|
+
-- this should probably be moved to its own tag
|
1382
|
+
-- object.
|
1383
|
+
function ReqlessJob:remove_tag(tag)
|
1384
|
+
-- namespace the tag
|
1385
|
+
local namespaced_tag = 'ql:t:' .. tag
|
1386
|
+
|
1387
|
+
-- Remove the job from the specified tag
|
1388
|
+
redis.call('zrem', namespaced_tag, self.jid)
|
1389
|
+
|
1390
|
+
-- Check if any tags jids remain in the tag set.
|
1391
|
+
local remaining = redis.call('zcard', namespaced_tag)
|
1392
|
+
|
1393
|
+
-- If the number of jids in the tagged set
|
1394
|
+
-- is 0 it means we have no jobs with this tag
|
1395
|
+
-- and we should remove it from the set of all tags
|
1396
|
+
-- to prevent memory leaks.
|
1397
|
+
if tonumber(remaining) == 0 then
|
1398
|
+
redis.call('zrem', 'ql:tags', tag)
|
1399
|
+
else
|
1400
|
+
-- Decrement the tag in the set of all tags.
|
1401
|
+
redis.call('zincrby', 'ql:tags', -1, tag)
|
1402
|
+
end
|
1403
|
+
end
|
1404
|
+
-------------------------------------------------------------------------------
|
1405
|
+
-- Queue class
|
1406
|
+
-------------------------------------------------------------------------------
|
1407
|
+
-- Return a queue object
|
1408
|
+
function Reqless.queue(name)
|
1409
|
+
assert(name, 'Queue(): no queue name provided')
|
1410
|
+
local queue = {}
|
1411
|
+
setmetatable(queue, ReqlessQueue)
|
1412
|
+
queue.name = name
|
1413
|
+
|
1414
|
+
-- Access to our work
|
1415
|
+
queue.work = {
|
1416
|
+
peek = function(offset, limit)
|
1417
|
+
if limit <= 0 then
|
1418
|
+
return {}
|
1419
|
+
end
|
1420
|
+
return redis.call('zrevrange', queue:prefix('work'), offset, offset + limit - 1)
|
1421
|
+
end, remove = function(...)
|
1422
|
+
if #arg > 0 then
|
1423
|
+
return redis.call('zrem', queue:prefix('work'), unpack(arg))
|
1424
|
+
end
|
1425
|
+
end, add = function(now, priority, jid)
|
1426
|
+
return redis.call('zadd',
|
1427
|
+
queue:prefix('work'), priority - (now / 10000000000), jid)
|
1428
|
+
end, score = function(jid)
|
1429
|
+
return redis.call('zscore', queue:prefix('work'), jid)
|
1430
|
+
end, length = function()
|
1431
|
+
return redis.call('zcard', queue:prefix('work'))
|
1432
|
+
end
|
1433
|
+
}
|
1434
|
+
|
1435
|
+
-- Access to our locks
|
1436
|
+
queue.locks = {
|
1437
|
+
expired = function(now, offset, limit)
|
1438
|
+
return redis.call('zrangebyscore',
|
1439
|
+
queue:prefix('locks'), -math.huge, now, 'LIMIT', offset, limit)
|
1440
|
+
end, peek = function(now, offset, limit)
|
1441
|
+
return redis.call('zrangebyscore', queue:prefix('locks'),
|
1442
|
+
now, math.huge, 'LIMIT', offset, limit)
|
1443
|
+
end, add = function(expires, jid)
|
1444
|
+
redis.call('zadd', queue:prefix('locks'), expires, jid)
|
1445
|
+
end, remove = function(...)
|
1446
|
+
if #arg > 0 then
|
1447
|
+
return redis.call('zrem', queue:prefix('locks'), unpack(arg))
|
1448
|
+
end
|
1449
|
+
end, running = function(now)
|
1450
|
+
return redis.call('zcount', queue:prefix('locks'), now, math.huge)
|
1451
|
+
end, length = function(now)
|
1452
|
+
-- If a 'now' is provided, we're interested in how many are before
|
1453
|
+
-- that time
|
1454
|
+
if now then
|
1455
|
+
return redis.call('zcount', queue:prefix('locks'), 0, now)
|
1456
|
+
else
|
1457
|
+
return redis.call('zcard', queue:prefix('locks'))
|
1458
|
+
end
|
1459
|
+
end
|
1460
|
+
}
|
1461
|
+
|
1462
|
+
-- Access to our dependent jobs
|
1463
|
+
queue.depends = {
|
1464
|
+
peek = function(now, offset, limit)
|
1465
|
+
return redis.call('zrange',
|
1466
|
+
queue:prefix('depends'), offset, offset + limit - 1)
|
1467
|
+
end, add = function(now, jid)
|
1468
|
+
redis.call('zadd', queue:prefix('depends'), now, jid)
|
1469
|
+
end, remove = function(...)
|
1470
|
+
if #arg > 0 then
|
1471
|
+
return redis.call('zrem', queue:prefix('depends'), unpack(arg))
|
1472
|
+
end
|
1473
|
+
end, length = function()
|
1474
|
+
return redis.call('zcard', queue:prefix('depends'))
|
1475
|
+
end
|
1476
|
+
}
|
1477
|
+
|
1478
|
+
|
1479
|
+
-- Access to the queue level throttled jobs.
|
1480
|
+
queue.throttled = {
|
1481
|
+
length = function()
|
1482
|
+
return (redis.call('zcard', queue:prefix('throttled')) or 0)
|
1483
|
+
end, peek = function(now, offset, limit)
|
1484
|
+
return redis.call('zrange', queue:prefix('throttled'), offset, offset + limit - 1)
|
1485
|
+
end, add = function(...)
|
1486
|
+
if #arg > 0 then
|
1487
|
+
redis.call('zadd', queue:prefix('throttled'), unpack(arg))
|
1488
|
+
end
|
1489
|
+
end, remove = function(...)
|
1490
|
+
if #arg > 0 then
|
1491
|
+
return redis.call('zrem', queue:prefix('throttled'), unpack(arg))
|
1492
|
+
end
|
1493
|
+
end, pop = function(min, max)
|
1494
|
+
return redis.call('zremrangebyrank', queue:prefix('throttled'), min, max)
|
1495
|
+
end
|
1496
|
+
}
|
1497
|
+
|
1498
|
+
-- Access to our scheduled jobs
|
1499
|
+
queue.scheduled = {
|
1500
|
+
peek = function(now, offset, limit)
|
1501
|
+
return redis.call('zrange',
|
1502
|
+
queue:prefix('scheduled'), offset, offset + limit - 1)
|
1503
|
+
end, ready = function(now, offset, limit)
|
1504
|
+
return redis.call('zrangebyscore',
|
1505
|
+
queue:prefix('scheduled'), 0, now, 'LIMIT', offset, limit)
|
1506
|
+
end, add = function(when, jid)
|
1507
|
+
redis.call('zadd', queue:prefix('scheduled'), when, jid)
|
1508
|
+
end, remove = function(...)
|
1509
|
+
if #arg > 0 then
|
1510
|
+
return redis.call('zrem', queue:prefix('scheduled'), unpack(arg))
|
1511
|
+
end
|
1512
|
+
end, length = function()
|
1513
|
+
return redis.call('zcard', queue:prefix('scheduled'))
|
1514
|
+
end
|
1515
|
+
}
|
1516
|
+
|
1517
|
+
-- Access to our recurring jobs
|
1518
|
+
queue.recurring = {
|
1519
|
+
peek = function(now, offset, limit)
|
1520
|
+
return redis.call('zrangebyscore', queue:prefix('recur'),
|
1521
|
+
0, now, 'LIMIT', offset, limit)
|
1522
|
+
end, ready = function(now, offset, limit)
|
1523
|
+
end, add = function(when, jid)
|
1524
|
+
redis.call('zadd', queue:prefix('recur'), when, jid)
|
1525
|
+
end, remove = function(...)
|
1526
|
+
if #arg > 0 then
|
1527
|
+
return redis.call('zrem', queue:prefix('recur'), unpack(arg))
|
1528
|
+
end
|
1529
|
+
end, update = function(increment, jid)
|
1530
|
+
redis.call('zincrby', queue:prefix('recur'), increment, jid)
|
1531
|
+
end, score = function(jid)
|
1532
|
+
return redis.call('zscore', queue:prefix('recur'), jid)
|
1533
|
+
end, length = function()
|
1534
|
+
return redis.call('zcard', queue:prefix('recur'))
|
1535
|
+
end
|
1536
|
+
}
|
1537
|
+
return queue
|
1538
|
+
end
|
1539
|
+
|
1540
|
+
-- Return the prefix for this particular queue
|
1541
|
+
function ReqlessQueue:prefix(group)
|
1542
|
+
if group then
|
1543
|
+
return ReqlessQueue.ns .. self.name .. '-' .. group
|
1544
|
+
end
|
1545
|
+
|
1546
|
+
return ReqlessQueue.ns .. self.name
|
1547
|
+
end
|
1548
|
+
|
1549
|
+
-- Stats(now, date)
|
1550
|
+
-- ---------------------
|
1551
|
+
-- Return the current statistics for a given queue on a given date. The
|
1552
|
+
-- results are returned are a JSON blob:
|
1553
|
+
--
|
1554
|
+
--
|
1555
|
+
-- {
|
1556
|
+
-- # These are unimplemented as of yet
|
1557
|
+
-- 'failed': 3,
|
1558
|
+
-- 'retries': 5,
|
1559
|
+
-- 'wait' : {
|
1560
|
+
-- 'total' : ...,
|
1561
|
+
-- 'mean' : ...,
|
1562
|
+
-- 'variance' : ...,
|
1563
|
+
-- 'histogram': [
|
1564
|
+
-- ...
|
1565
|
+
-- ]
|
1566
|
+
-- }, 'run': {
|
1567
|
+
-- 'total' : ...,
|
1568
|
+
-- 'mean' : ...,
|
1569
|
+
-- 'variance' : ...,
|
1570
|
+
-- 'histogram': [
|
1571
|
+
-- ...
|
1572
|
+
-- ]
|
1573
|
+
-- }
|
1574
|
+
-- }
|
1575
|
+
--
|
1576
|
+
-- The histogram's data points are at the second resolution for the first
|
1577
|
+
-- minute, the minute resolution for the first hour, the 15-minute resolution
|
1578
|
+
-- for the first day, the hour resolution for the first 3 days, and then at
|
1579
|
+
-- the day resolution from there on out. The `histogram` key is a list of
|
1580
|
+
-- those values.
|
1581
|
+
function ReqlessQueue:stats(now, date)
|
1582
|
+
date = assert(tonumber(date),
|
1583
|
+
'Stats(): Arg "date" missing or not a number: ' .. (date or 'nil'))
|
1584
|
+
|
1585
|
+
-- The bin is midnight of the provided day
|
1586
|
+
-- 24 * 60 * 60 = 86400
|
1587
|
+
local bin = date - (date % 86400)
|
1588
|
+
|
1589
|
+
-- This a table of all the keys we want to use in order to produce a histogram
|
1590
|
+
local histokeys = {
|
1591
|
+
'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',
|
1592
|
+
'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',
|
1593
|
+
'h1','h2','h3','h4','h5','h6','h7','h8','h9','h10','h11','h12','h13','h14','h15','h16','h17','h18','h19','h20','h21','h22','h23',
|
1594
|
+
'd1','d2','d3','d4','d5','d6'
|
1595
|
+
}
|
1596
|
+
|
1597
|
+
local mkstats = function(name, bin, queue)
|
1598
|
+
-- The results we'll be sending back
|
1599
|
+
local results = {}
|
1600
|
+
|
1601
|
+
local key = 'ql:s:' .. name .. ':' .. bin .. ':' .. queue
|
1602
|
+
local count, mean, vk = unpack(redis.call('hmget', key, 'total', 'mean', 'vk'))
|
1603
|
+
|
1604
|
+
count = tonumber(count) or 0
|
1605
|
+
mean = tonumber(mean) or 0
|
1606
|
+
vk = tonumber(vk)
|
1607
|
+
|
1608
|
+
results.count = count or 0
|
1609
|
+
results.mean = mean or 0
|
1610
|
+
results.histogram = {}
|
1611
|
+
|
1612
|
+
if not count then
|
1613
|
+
results.std = 0
|
1614
|
+
else
|
1615
|
+
if count > 1 then
|
1616
|
+
results.std = math.sqrt(vk / (count - 1))
|
1617
|
+
else
|
1618
|
+
results.std = 0
|
1619
|
+
end
|
1620
|
+
end
|
1621
|
+
|
1622
|
+
local histogram = redis.call('hmget', key, unpack(histokeys))
|
1623
|
+
for i=1, #histokeys do
|
1624
|
+
table.insert(results.histogram, tonumber(histogram[i]) or 0)
|
1625
|
+
end
|
1626
|
+
return results
|
1627
|
+
end
|
1628
|
+
|
1629
|
+
local retries, failed, failures = unpack(redis.call('hmget', 'ql:s:stats:' .. bin .. ':' .. self.name, 'retries', 'failed', 'failures'))
|
1630
|
+
return {
|
1631
|
+
retries = tonumber(retries or 0),
|
1632
|
+
failed = tonumber(failed or 0),
|
1633
|
+
failures = tonumber(failures or 0),
|
1634
|
+
wait = mkstats('wait', bin, self.name),
|
1635
|
+
run = mkstats('run' , bin, self.name)
|
1636
|
+
}
|
1637
|
+
end
|
1638
|
+
|
1639
|
+
-- Peek
|
1640
|
+
-------
|
1641
|
+
-- Examine the next jobs that would be popped from the queue without actually
|
1642
|
+
-- popping them.
|
1643
|
+
function ReqlessQueue:peek(now, offset, limit)
|
1644
|
+
offset = assert(tonumber(offset),
|
1645
|
+
'Peek(): Arg "offset" missing or not a number: ' .. tostring(offset))
|
1646
|
+
|
1647
|
+
limit = assert(tonumber(limit),
|
1648
|
+
'Peek(): Arg "limit" missing or not a number: ' .. tostring(limit))
|
1649
|
+
|
1650
|
+
if limit <= 0 then
|
1651
|
+
return {}
|
1652
|
+
end
|
1653
|
+
|
1654
|
+
local offset_with_limit = offset + limit
|
1655
|
+
|
1656
|
+
-- These are the ids that we're going to return. We'll begin with any jobs
|
1657
|
+
-- that have lost their locks
|
1658
|
+
local jids = self.locks.expired(now, 0, offset_with_limit)
|
1659
|
+
|
1660
|
+
-- Since we can't just peek the range we want, we have to consider all offset
|
1661
|
+
-- + limit jobs before we can take the relevant range.
|
1662
|
+
local remaining_capacity = offset_with_limit - #jids
|
1663
|
+
|
1664
|
+
-- If we still need jobs in order to meet demand, then we should
|
1665
|
+
-- look for all the recurring jobs that need jobs run
|
1666
|
+
self:check_recurring(now, remaining_capacity)
|
1667
|
+
|
1668
|
+
-- Now we've checked __all__ the locks for this queue the could
|
1669
|
+
-- have expired, and are no more than the number requested. If
|
1670
|
+
-- we still need values in order to meet the demand, then we
|
1671
|
+
-- should check if any scheduled items, and if so, we should
|
1672
|
+
-- insert them to ensure correctness when pulling off the next
|
1673
|
+
-- unit of work.
|
1674
|
+
self:check_scheduled(now, remaining_capacity)
|
1675
|
+
|
1676
|
+
if offset > #jids then
|
1677
|
+
-- Offset takes us past the expired jids, so just return straight from the
|
1678
|
+
-- work queue
|
1679
|
+
return self.work.peek(offset - #jids, limit)
|
1680
|
+
end
|
1681
|
+
|
1682
|
+
-- Return a mix of expired jids and prioritized items from the work queue
|
1683
|
+
table_extend(jids, self.work.peek(0, remaining_capacity))
|
1684
|
+
|
1685
|
+
if #jids < offset then
|
1686
|
+
return {}
|
1687
|
+
end
|
1688
|
+
|
1689
|
+
return {unpack(jids, offset + 1, offset_with_limit)}
|
1690
|
+
end
|
1691
|
+
|
1692
|
+
-- Return true if this queue is paused
|
1693
|
+
function ReqlessQueue:paused()
|
1694
|
+
return redis.call('sismember', 'ql:paused_queues', self.name) == 1
|
1695
|
+
end
|
1696
|
+
|
1697
|
+
-- Pause this queue
|
1698
|
+
--
|
1699
|
+
-- Note: long term, we have discussed adding a rate-limiting
|
1700
|
+
-- feature to reqless-core, which would be more flexible and
|
1701
|
+
-- could be used for pausing (i.e. pause = set the rate to 0).
|
1702
|
+
-- For now, this is far simpler, but we should rewrite this
|
1703
|
+
-- in terms of the rate limiting feature if/when that is added.
|
1704
|
+
function ReqlessQueue.pause(now, ...)
|
1705
|
+
redis.call('sadd', 'ql:paused_queues', unpack(arg))
|
1706
|
+
end
|
1707
|
+
|
1708
|
+
-- Unpause this queue
|
1709
|
+
function ReqlessQueue.unpause(...)
|
1710
|
+
redis.call('srem', 'ql:paused_queues', unpack(arg))
|
1711
|
+
end
|
1712
|
+
|
1713
|
+
-- Checks for expired locks, scheduled and recurring jobs, returning any
|
1714
|
+
-- jobs that are ready to be processes
|
1715
|
+
function ReqlessQueue:pop(now, worker, limit)
|
1716
|
+
assert(worker, 'Pop(): Arg "worker" missing')
|
1717
|
+
limit = assert(tonumber(limit),
|
1718
|
+
'Pop(): Arg "limit" missing or not a number: ' .. tostring(limit))
|
1719
|
+
|
1720
|
+
-- If this queue is paused, then return no jobs
|
1721
|
+
if self:paused() then
|
1722
|
+
return {}
|
1723
|
+
end
|
1724
|
+
|
1725
|
+
-- Make sure we this worker to the list of seen workers
|
1726
|
+
redis.call('zadd', 'ql:workers', now, worker)
|
1727
|
+
|
1728
|
+
local dead_jids = self:invalidate_locks(now, limit) or {}
|
1729
|
+
local popped = {}
|
1730
|
+
|
1731
|
+
for _, jid in ipairs(dead_jids) do
|
1732
|
+
local success = self:pop_job(now, worker, Reqless.job(jid))
|
1733
|
+
-- only track jid if a job was popped and it's not a phantom jid
|
1734
|
+
if success then
|
1735
|
+
table.insert(popped, jid)
|
1736
|
+
end
|
1737
|
+
end
|
1738
|
+
|
1739
|
+
-- if queue is at max capacity don't pop any further jobs.
|
1740
|
+
if not Reqless.throttle(ReqlessQueue.ns .. self.name):available() then
|
1741
|
+
return popped
|
1742
|
+
end
|
1743
|
+
|
1744
|
+
-- Now we've checked __all__ the locks for this queue the could
|
1745
|
+
-- have expired, and are no more than the number requested.
|
1746
|
+
|
1747
|
+
-- If we still need jobs in order to meet demand, then we should
|
1748
|
+
-- look for all the recurring jobs that need jobs run
|
1749
|
+
self:check_recurring(now, limit - #dead_jids)
|
1750
|
+
|
1751
|
+
-- If we still need values in order to meet the demand, then we
|
1752
|
+
-- should check if any scheduled items, and if so, we should
|
1753
|
+
-- insert them to ensure correctness when pulling off the next
|
1754
|
+
-- unit of work.
|
1755
|
+
self:check_scheduled(now, limit - #dead_jids)
|
1756
|
+
|
1757
|
+
-- With these in place, we can expand this list of jids based on the work
|
1758
|
+
-- queue itself and the priorities therein
|
1759
|
+
|
1760
|
+
-- Since throttles could prevent work queue items from being popped, we can
|
1761
|
+
-- retry a number of times till we find work items that are not throttled
|
1762
|
+
local pop_retry_limit = tonumber(
|
1763
|
+
Reqless.config.get(self.name .. '-max-pop-retry') or
|
1764
|
+
Reqless.config.get('max-pop-retry', 1)
|
1765
|
+
)
|
1766
|
+
|
1767
|
+
-- Keep trying to fulfill fulfill jobs from the work queue until we reach
|
1768
|
+
-- the desired limit or exhaust our retry limit
|
1769
|
+
while #popped < limit and pop_retry_limit > 0 do
|
1770
|
+
|
1771
|
+
local jids = self.work.peek(0, limit - #popped) or {}
|
1772
|
+
|
1773
|
+
-- If there is nothing in the work queue, then no need to keep looping
|
1774
|
+
if #jids == 0 then
|
1775
|
+
break
|
1776
|
+
end
|
1777
|
+
|
1778
|
+
|
1779
|
+
for _, jid in ipairs(jids) do
|
1780
|
+
local job = Reqless.job(jid)
|
1781
|
+
if job:throttles_acquire(now) then
|
1782
|
+
local success = self:pop_job(now, worker, job)
|
1783
|
+
-- only track jid if a job was popped and it's not a phantom jid
|
1784
|
+
if success then
|
1785
|
+
table.insert(popped, jid)
|
1786
|
+
end
|
1787
|
+
else
|
1788
|
+
self:throttle(now, job)
|
1789
|
+
end
|
1790
|
+
end
|
1791
|
+
|
1792
|
+
-- All jobs should have acquired locks or be throttled,
|
1793
|
+
-- ergo, remove all jids from work queue
|
1794
|
+
self.work.remove(unpack(jids))
|
1795
|
+
|
1796
|
+
pop_retry_limit = pop_retry_limit - 1
|
1797
|
+
end
|
1798
|
+
|
1799
|
+
return popped
|
1800
|
+
end
|
1801
|
+
|
1802
|
+
-- Throttle a job
|
1803
|
+
function ReqlessQueue:throttle(now, job)
|
1804
|
+
job:throttle(now)
|
1805
|
+
self.throttled.add(now, job.jid)
|
1806
|
+
local state = unpack(job:data('state'))
|
1807
|
+
if state ~= 'throttled' then
|
1808
|
+
job:update({state = 'throttled'})
|
1809
|
+
job:history(now, 'throttled', {queue = self.name})
|
1810
|
+
end
|
1811
|
+
end
|
1812
|
+
|
1813
|
+
function ReqlessQueue:pop_job(now, worker, job)
|
1814
|
+
local state
|
1815
|
+
local jid = job.jid
|
1816
|
+
local job_state = job:data('state')
|
1817
|
+
-- if the job doesn't exist, short circuit
|
1818
|
+
if not job_state then
|
1819
|
+
return false
|
1820
|
+
end
|
1821
|
+
|
1822
|
+
state = unpack(job_state)
|
1823
|
+
job:history(now, 'popped', {worker = worker})
|
1824
|
+
|
1825
|
+
-- We should find the heartbeat interval for this queue heartbeat
|
1826
|
+
local expires = now + tonumber(
|
1827
|
+
Reqless.config.get(self.name .. '-heartbeat') or
|
1828
|
+
Reqless.config.get('heartbeat', 60))
|
1829
|
+
|
1830
|
+
-- Update the wait time statistics
|
1831
|
+
-- Just does job:data('time') do the same as this?
|
1832
|
+
local time = tonumber(redis.call('hget', ReqlessJob.ns .. jid, 'time') or now)
|
1833
|
+
local waiting = now - time
|
1834
|
+
self:stat(now, 'wait', waiting)
|
1835
|
+
redis.call('hset', ReqlessJob.ns .. jid,
|
1836
|
+
'time', string.format("%.20f", now))
|
1837
|
+
|
1838
|
+
-- Add this job to the list of jobs handled by this worker
|
1839
|
+
redis.call('zadd', 'ql:w:' .. worker .. ':jobs', expires, jid)
|
1840
|
+
|
1841
|
+
-- Update the jobs data, and add its locks, and return the job
|
1842
|
+
job:update({
|
1843
|
+
worker = worker,
|
1844
|
+
expires = expires,
|
1845
|
+
state = 'running'
|
1846
|
+
})
|
1847
|
+
|
1848
|
+
self.locks.add(expires, jid)
|
1849
|
+
|
1850
|
+
local tracked = redis.call('zscore', 'ql:tracked', jid) ~= false
|
1851
|
+
if tracked then
|
1852
|
+
Reqless.publish('popped', jid)
|
1853
|
+
end
|
1854
|
+
return true
|
1855
|
+
end
|
1856
|
+
|
1857
|
+
-- Update the stats for this queue
|
1858
|
+
function ReqlessQueue:stat(now, stat, val)
|
1859
|
+
-- The bin is midnight of the provided day
|
1860
|
+
local bin = now - (now % 86400)
|
1861
|
+
local key = 'ql:s:' .. stat .. ':' .. bin .. ':' .. self.name
|
1862
|
+
|
1863
|
+
-- Get the current data
|
1864
|
+
local count, mean, vk = unpack(
|
1865
|
+
redis.call('hmget', key, 'total', 'mean', 'vk'))
|
1866
|
+
|
1867
|
+
-- If there isn't any data there presently, then we must initialize it
|
1868
|
+
count = count or 0
|
1869
|
+
if count == 0 then
|
1870
|
+
mean = val
|
1871
|
+
vk = 0
|
1872
|
+
count = 1
|
1873
|
+
else
|
1874
|
+
count = count + 1
|
1875
|
+
local oldmean = mean
|
1876
|
+
mean = mean + (val - mean) / count
|
1877
|
+
vk = vk + (val - mean) * (val - oldmean)
|
1878
|
+
end
|
1879
|
+
|
1880
|
+
-- Now, update the histogram
|
1881
|
+
-- - `s1`, `s2`, ..., -- second-resolution histogram counts
|
1882
|
+
-- - `m1`, `m2`, ..., -- minute-resolution
|
1883
|
+
-- - `h1`, `h2`, ..., -- hour-resolution
|
1884
|
+
-- - `d1`, `d2`, ..., -- day-resolution
|
1885
|
+
val = math.floor(val)
|
1886
|
+
if val < 60 then -- seconds
|
1887
|
+
redis.call('hincrby', key, 's' .. val, 1)
|
1888
|
+
elseif val < 3600 then -- minutes
|
1889
|
+
redis.call('hincrby', key, 'm' .. math.floor(val / 60), 1)
|
1890
|
+
elseif val < 86400 then -- hours
|
1891
|
+
redis.call('hincrby', key, 'h' .. math.floor(val / 3600), 1)
|
1892
|
+
else -- days
|
1893
|
+
redis.call('hincrby', key, 'd' .. math.floor(val / 86400), 1)
|
1894
|
+
end
|
1895
|
+
redis.call('hmset', key, 'total', count, 'mean', mean, 'vk', vk)
|
1896
|
+
end
|
1897
|
+
|
1898
|
+
-- Put(now, jid, klass, data, delay,
|
1899
|
+
-- [priority, p],
|
1900
|
+
-- [tags, t],
|
1901
|
+
-- [retries, r],
|
1902
|
+
-- [depends, '[...]'])
|
1903
|
+
-- -----------------------
|
1904
|
+
-- Insert a job into the queue with the given priority, tags, delay, klass and
|
1905
|
+
-- data.
|
1906
|
+
function ReqlessQueue:put(now, worker, jid, klass, raw_data, delay, ...)
|
1907
|
+
assert(jid , 'Put(): Arg "jid" missing')
|
1908
|
+
assert(klass, 'Put(): Arg "klass" missing')
|
1909
|
+
local data = assert(cjson.decode(raw_data),
|
1910
|
+
'Put(): Arg "data" missing or not JSON: ' .. tostring(raw_data))
|
1911
|
+
delay = assert(tonumber(delay),
|
1912
|
+
'Put(): Arg "delay" not a number: ' .. tostring(delay))
|
1913
|
+
|
1914
|
+
-- Read in all the optional parameters. All of these must come in pairs, so
|
1915
|
+
-- if we have an odd number of extra args, raise an error
|
1916
|
+
if #arg % 2 == 1 then
|
1917
|
+
error('Odd number of additional args: ' .. tostring(arg))
|
1918
|
+
end
|
1919
|
+
local options = {}
|
1920
|
+
for i = 1, #arg, 2 do options[arg[i]] = arg[i + 1] end
|
1921
|
+
|
1922
|
+
-- Let's see what the old priority and tags were
|
1923
|
+
local job = Reqless.job(jid)
|
1924
|
+
local priority, tags, oldqueue, state, failure, retries, oldworker =
|
1925
|
+
unpack(redis.call('hmget', ReqlessJob.ns .. jid, 'priority', 'tags',
|
1926
|
+
'queue', 'state', 'failure', 'retries', 'worker'))
|
1927
|
+
|
1928
|
+
-- If there are old tags, then we should remove the tags this job has
|
1929
|
+
if tags then
|
1930
|
+
Reqless.tag(now, 'remove', jid, unpack(cjson.decode(tags)))
|
1931
|
+
end
|
1932
|
+
|
1933
|
+
-- Sanity check on optional args
|
1934
|
+
local retries = assert(tonumber(options['retries'] or retries or 5) ,
|
1935
|
+
'Put(): Arg "retries" not a number: ' .. tostring(options['retries']))
|
1936
|
+
local tags = assert(cjson.decode(options['tags'] or tags or '[]' ),
|
1937
|
+
'Put(): Arg "tags" not JSON' .. tostring(options['tags']))
|
1938
|
+
local priority = assert(tonumber(options['priority'] or priority or 0),
|
1939
|
+
'Put(): Arg "priority" not a number' .. tostring(options['priority']))
|
1940
|
+
local depends = assert(cjson.decode(options['depends'] or '[]') ,
|
1941
|
+
'Put(): Arg "depends" not JSON: ' .. tostring(options['depends']))
|
1942
|
+
local throttles = assert(cjson.decode(options['throttles'] or '[]'),
|
1943
|
+
'Put(): Arg "throttles" not JSON array: ' .. tostring(options['throttles']))
|
1944
|
+
|
1945
|
+
-- If the job has old dependencies, determine which dependencies are
|
1946
|
+
-- in the new dependencies but not in the old ones, and which are in the
|
1947
|
+
-- old ones but not in the new
|
1948
|
+
if #depends > 0 then
|
1949
|
+
-- This makes it easier to check if it's in the new list
|
1950
|
+
local new = {}
|
1951
|
+
for _, d in ipairs(depends) do new[d] = 1 end
|
1952
|
+
|
1953
|
+
-- Now find what's in the original, but not the new
|
1954
|
+
local original = redis.call(
|
1955
|
+
'smembers', ReqlessJob.ns .. jid .. '-dependencies')
|
1956
|
+
for _, dep in pairs(original) do
|
1957
|
+
if new[dep] == nil then
|
1958
|
+
-- Remove k as a dependency
|
1959
|
+
redis.call('srem', ReqlessJob.ns .. dep .. '-dependents' , jid)
|
1960
|
+
redis.call('srem', ReqlessJob.ns .. jid .. '-dependencies', dep)
|
1961
|
+
end
|
1962
|
+
end
|
1963
|
+
end
|
1964
|
+
|
1965
|
+
-- Send out a log message
|
1966
|
+
Reqless.publish('log', cjson.encode({
|
1967
|
+
jid = jid,
|
1968
|
+
event = 'put',
|
1969
|
+
queue = self.name
|
1970
|
+
}))
|
1971
|
+
|
1972
|
+
-- Update the history to include this new change
|
1973
|
+
job:history(now, 'put', {queue = self.name})
|
1974
|
+
|
1975
|
+
-- If this item was previously in another queue, then we should remove it from there
|
1976
|
+
-- and remove the associated throttle
|
1977
|
+
if oldqueue then
|
1978
|
+
local queue_obj = Reqless.queue(oldqueue)
|
1979
|
+
queue_obj:remove_job(jid)
|
1980
|
+
local old_qid = ReqlessQueue.ns .. oldqueue
|
1981
|
+
for index, throttle_name in ipairs(throttles) do
|
1982
|
+
if throttle_name == old_qid then
|
1983
|
+
table.remove(throttles, index)
|
1984
|
+
end
|
1985
|
+
end
|
1986
|
+
end
|
1987
|
+
|
1988
|
+
-- If this had previously been given out to a worker, make sure to remove it
|
1989
|
+
-- from that worker's jobs
|
1990
|
+
if oldworker and oldworker ~= '' then
|
1991
|
+
redis.call('zrem', 'ql:w:' .. oldworker .. ':jobs', jid)
|
1992
|
+
-- If it's a different worker that's putting this job, send a notification
|
1993
|
+
-- to the last owner of the job
|
1994
|
+
if oldworker ~= worker then
|
1995
|
+
-- We need to inform whatever worker had that job
|
1996
|
+
local encoded = cjson.encode({
|
1997
|
+
jid = jid,
|
1998
|
+
event = 'lock_lost',
|
1999
|
+
worker = oldworker
|
2000
|
+
})
|
2001
|
+
Reqless.publish('w:' .. oldworker, encoded)
|
2002
|
+
Reqless.publish('log', encoded)
|
2003
|
+
end
|
2004
|
+
end
|
2005
|
+
|
2006
|
+
-- If the job was previously in the 'completed' state, then we should
|
2007
|
+
-- remove it from being enqueued for destructination
|
2008
|
+
if state == 'complete' then
|
2009
|
+
redis.call('zrem', 'ql:completed', jid)
|
2010
|
+
end
|
2011
|
+
|
2012
|
+
-- Add this job to the list of jobs tagged with whatever tags were supplied
|
2013
|
+
for _, tag in ipairs(tags) do
|
2014
|
+
Reqless.job(jid):insert_tag(now, tag)
|
2015
|
+
end
|
2016
|
+
|
2017
|
+
-- If we're in the failed state, remove all of our data
|
2018
|
+
if state == 'failed' then
|
2019
|
+
failure = cjson.decode(failure)
|
2020
|
+
-- We need to make this remove it from the failed queues
|
2021
|
+
redis.call('lrem', 'ql:f:' .. failure.group, 0, jid)
|
2022
|
+
if redis.call('llen', 'ql:f:' .. failure.group) == 0 then
|
2023
|
+
redis.call('srem', 'ql:failures', failure.group)
|
2024
|
+
end
|
2025
|
+
-- The bin is midnight of the provided day
|
2026
|
+
-- 24 * 60 * 60 = 86400
|
2027
|
+
local bin = failure.when - (failure.when % 86400)
|
2028
|
+
-- We also need to decrement the stats about the queue on
|
2029
|
+
-- the day that this failure actually happened.
|
2030
|
+
redis.call('hincrby', 'ql:s:stats:' .. bin .. ':' .. self.name, 'failed' , -1)
|
2031
|
+
end
|
2032
|
+
|
2033
|
+
-- insert default queue throttle
|
2034
|
+
table.insert(throttles, ReqlessQueue.ns .. self.name)
|
2035
|
+
|
2036
|
+
data = {
|
2037
|
+
'jid' , jid,
|
2038
|
+
'klass' , klass,
|
2039
|
+
'data' , raw_data,
|
2040
|
+
'priority' , priority,
|
2041
|
+
'tags' , cjson.encode(tags),
|
2042
|
+
'state' , ((delay > 0) and 'scheduled') or 'waiting',
|
2043
|
+
'worker' , '',
|
2044
|
+
'expires' , 0,
|
2045
|
+
'queue' , self.name,
|
2046
|
+
'retries' , retries,
|
2047
|
+
'remaining', retries,
|
2048
|
+
'time' , string.format("%.20f", now),
|
2049
|
+
'throttles', cjson.encode(throttles)
|
2050
|
+
}
|
2051
|
+
|
2052
|
+
-- First, let's save its data
|
2053
|
+
redis.call('hmset', ReqlessJob.ns .. jid, unpack(data))
|
2054
|
+
|
2055
|
+
-- These are the jids we legitimately have to wait on
|
2056
|
+
for _, j in ipairs(depends) do
|
2057
|
+
-- Make sure it's something other than 'nil' or complete.
|
2058
|
+
local state = redis.call('hget', ReqlessJob.ns .. j, 'state')
|
2059
|
+
if (state and state ~= 'complete') then
|
2060
|
+
redis.call('sadd', ReqlessJob.ns .. j .. '-dependents' , jid)
|
2061
|
+
redis.call('sadd', ReqlessJob.ns .. jid .. '-dependencies', j)
|
2062
|
+
end
|
2063
|
+
end
|
2064
|
+
|
2065
|
+
-- Now, if a delay was provided, and if it's in the future,
|
2066
|
+
-- then we'll have to schedule it. Otherwise, we're just
|
2067
|
+
-- going to add it to the work queue.
|
2068
|
+
if delay > 0 then
|
2069
|
+
if redis.call('scard', ReqlessJob.ns .. jid .. '-dependencies') > 0 then
|
2070
|
+
-- We've already put it in 'depends'. Now, we must just save the data
|
2071
|
+
-- for when it's scheduled
|
2072
|
+
self.depends.add(now, jid)
|
2073
|
+
redis.call('hmset', ReqlessJob.ns .. jid,
|
2074
|
+
'state', 'depends',
|
2075
|
+
'scheduled', now + delay)
|
2076
|
+
else
|
2077
|
+
self.scheduled.add(now + delay, jid)
|
2078
|
+
end
|
2079
|
+
else
|
2080
|
+
-- to avoid false negatives when popping jobs check if the job should be
|
2081
|
+
-- throttled immediately.
|
2082
|
+
local job = Reqless.job(jid)
|
2083
|
+
if redis.call('scard', ReqlessJob.ns .. jid .. '-dependencies') > 0 then
|
2084
|
+
self.depends.add(now, jid)
|
2085
|
+
redis.call('hset', ReqlessJob.ns .. jid, 'state', 'depends')
|
2086
|
+
elseif not job:throttles_available() then
|
2087
|
+
self:throttle(now, job)
|
2088
|
+
else
|
2089
|
+
self.work.add(now, priority, jid)
|
2090
|
+
end
|
2091
|
+
end
|
2092
|
+
|
2093
|
+
-- Lastly, we're going to make sure that this item is in the
|
2094
|
+
-- set of known queues. We should keep this sorted by the
|
2095
|
+
-- order in which we saw each of these queues
|
2096
|
+
if redis.call('zscore', 'ql:queues', self.name) == false then
|
2097
|
+
redis.call('zadd', 'ql:queues', now, self.name)
|
2098
|
+
end
|
2099
|
+
|
2100
|
+
if redis.call('zscore', 'ql:tracked', jid) ~= false then
|
2101
|
+
Reqless.publish('put', jid)
|
2102
|
+
end
|
2103
|
+
|
2104
|
+
return jid
|
2105
|
+
end
|
2106
|
+
|
2107
|
+
-- Move `count` jobs out of the failed state and into this queue
|
2108
|
+
function ReqlessQueue:unfail(now, group, count)
|
2109
|
+
assert(group, 'Unfail(): Arg "group" missing')
|
2110
|
+
count = assert(tonumber(count or 25),
|
2111
|
+
'Unfail(): Arg "count" not a number: ' .. tostring(count))
|
2112
|
+
assert(count > 0, 'Unfail(): Arg "count" must be greater than zero')
|
2113
|
+
|
2114
|
+
-- Get up to that many jobs, and we'll put them in the appropriate queue
|
2115
|
+
local jids = redis.call('lrange', 'ql:f:' .. group, -count, -1)
|
2116
|
+
|
2117
|
+
-- And now set each job's state, and put it into the appropriate queue
|
2118
|
+
local toinsert = {}
|
2119
|
+
for _, jid in ipairs(jids) do
|
2120
|
+
local job = Reqless.job(jid)
|
2121
|
+
local data = job:data()
|
2122
|
+
job:history(now, 'put', {queue = self.name})
|
2123
|
+
redis.call('hmset', ReqlessJob.ns .. data.jid,
|
2124
|
+
'state' , 'waiting',
|
2125
|
+
'worker' , '',
|
2126
|
+
'expires' , 0,
|
2127
|
+
'queue' , self.name,
|
2128
|
+
'remaining', data.retries or 5)
|
2129
|
+
self.work.add(now, data.priority, data.jid)
|
2130
|
+
end
|
2131
|
+
|
2132
|
+
-- Remove these jobs from the failed state
|
2133
|
+
redis.call('ltrim', 'ql:f:' .. group, 0, -count - 1)
|
2134
|
+
if (redis.call('llen', 'ql:f:' .. group) == 0) then
|
2135
|
+
redis.call('srem', 'ql:failures', group)
|
2136
|
+
end
|
2137
|
+
|
2138
|
+
return #jids
|
2139
|
+
end
|
2140
|
+
|
2141
|
+
-- Recur a job of type klass in this queue
|
2142
|
+
function ReqlessQueue:recurAtInterval(now, jid, klass, raw_data, interval, offset, ...)
|
2143
|
+
assert(jid , 'Recur(): Arg "jid" missing')
|
2144
|
+
assert(klass, 'Recur(): Arg "klass" missing')
|
2145
|
+
local data = assert(cjson.decode(raw_data),
|
2146
|
+
'Recur(): Arg "data" not JSON: ' .. tostring(raw_data))
|
2147
|
+
|
2148
|
+
local interval = assert(tonumber(interval),
|
2149
|
+
'Recur(): Arg "interval" not a number: ' .. tostring(interval))
|
2150
|
+
local offset = assert(tonumber(offset),
|
2151
|
+
'Recur(): Arg "offset" not a number: ' .. tostring(offset))
|
2152
|
+
if interval <= 0 then
|
2153
|
+
error('Recur(): Arg "interval" must be greater than 0')
|
2154
|
+
end
|
2155
|
+
|
2156
|
+
-- Read in all the optional parameters. All of these must come in
|
2157
|
+
-- pairs, so if we have an odd number of extra args, raise an error
|
2158
|
+
if #arg % 2 == 1 then
|
2159
|
+
error('Recur(): Odd number of additional args: ' .. tostring(arg))
|
2160
|
+
end
|
2161
|
+
|
2162
|
+
-- Read in all the optional parameters
|
2163
|
+
local options = {}
|
2164
|
+
for i = 1, #arg, 2 do options[arg[i]] = arg[i + 1] end
|
2165
|
+
options.tags = assert(cjson.decode(options.tags or '{}'),
|
2166
|
+
'Recur(): Arg "tags" must be JSON string array: ' .. tostring(
|
2167
|
+
options.tags))
|
2168
|
+
options.priority = assert(tonumber(options.priority or 0),
|
2169
|
+
'Recur(): Arg "priority" not a number: ' .. tostring(
|
2170
|
+
options.priority))
|
2171
|
+
options.retries = assert(tonumber(options.retries or 0),
|
2172
|
+
'Recur(): Arg "retries" not a number: ' .. tostring(
|
2173
|
+
options.retries))
|
2174
|
+
options.backlog = assert(tonumber(options.backlog or 0),
|
2175
|
+
'Recur(): Arg "backlog" not a number: ' .. tostring(
|
2176
|
+
options.backlog))
|
2177
|
+
options.throttles = assert(cjson.decode(options['throttles'] or '{}'),
|
2178
|
+
'Recur(): Arg "throttles" not JSON array: ' .. tostring(options['throttles']))
|
2179
|
+
|
2180
|
+
local count, old_queue = unpack(redis.call('hmget', 'ql:r:' .. jid, 'count', 'queue'))
|
2181
|
+
count = count or 0
|
2182
|
+
|
2183
|
+
local throttles = options['throttles'] or {}
|
2184
|
+
|
2185
|
+
-- If it has previously been in another queue, then we should remove
|
2186
|
+
-- some information about it
|
2187
|
+
if old_queue then
|
2188
|
+
Reqless.queue(old_queue).recurring.remove(jid)
|
2189
|
+
|
2190
|
+
for index, throttle_name in ipairs(throttles) do
|
2191
|
+
if throttle_name == old_queue then
|
2192
|
+
table.remove(throttles, index)
|
2193
|
+
end
|
2194
|
+
end
|
2195
|
+
end
|
2196
|
+
|
2197
|
+
-- insert default queue throttle
|
2198
|
+
table.insert(throttles, ReqlessQueue.ns .. self.name)
|
2199
|
+
|
2200
|
+
-- Do some insertions
|
2201
|
+
redis.call('hmset', 'ql:r:' .. jid,
|
2202
|
+
'jid' , jid,
|
2203
|
+
'klass' , klass,
|
2204
|
+
'data' , raw_data,
|
2205
|
+
'priority' , options.priority,
|
2206
|
+
'tags' , cjson.encode(options.tags or {}),
|
2207
|
+
'state' , 'recur',
|
2208
|
+
'queue' , self.name,
|
2209
|
+
'type' , 'interval',
|
2210
|
+
-- How many jobs we've spawned from this
|
2211
|
+
'count' , count,
|
2212
|
+
'interval' , interval,
|
2213
|
+
'retries' , options.retries,
|
2214
|
+
'backlog' , options.backlog,
|
2215
|
+
'throttles', cjson.encode(throttles))
|
2216
|
+
-- Now, we should schedule the next run of the job
|
2217
|
+
self.recurring.add(now + offset, jid)
|
2218
|
+
|
2219
|
+
-- Lastly, we're going to make sure that this item is in the
|
2220
|
+
-- set of known queues. We should keep this sorted by the
|
2221
|
+
-- order in which we saw each of these queues
|
2222
|
+
if redis.call('zscore', 'ql:queues', self.name) == false then
|
2223
|
+
redis.call('zadd', 'ql:queues', now, self.name)
|
2224
|
+
end
|
2225
|
+
|
2226
|
+
return jid
|
2227
|
+
end
|
2228
|
+
|
2229
|
+
-- Return the length of the queue
|
2230
|
+
function ReqlessQueue:length()
|
2231
|
+
return self.locks.length() + self.work.length() + self.scheduled.length()
|
2232
|
+
end
|
2233
|
+
|
2234
|
+
-------------------------------------------------------------------------------
|
2235
|
+
-- Housekeeping methods
|
2236
|
+
-------------------------------------------------------------------------------
|
2237
|
+
function ReqlessQueue:remove_job(jid)
|
2238
|
+
self.work.remove(jid)
|
2239
|
+
self.locks.remove(jid)
|
2240
|
+
self.throttled.remove(jid)
|
2241
|
+
self.depends.remove(jid)
|
2242
|
+
self.scheduled.remove(jid)
|
2243
|
+
end
|
2244
|
+
|
2245
|
+
-- Instantiate any recurring jobs that are ready
|
2246
|
+
function ReqlessQueue:check_recurring(now, count)
|
2247
|
+
if count <= 0 then
|
2248
|
+
return
|
2249
|
+
end
|
2250
|
+
-- This is how many jobs we've moved so far
|
2251
|
+
local moved = 0
|
2252
|
+
-- These are the recurring jobs that need work
|
2253
|
+
local r = self.recurring.peek(now, 0, count)
|
2254
|
+
for _, jid in ipairs(r) do
|
2255
|
+
-- For each of the jids that need jobs scheduled, first
|
2256
|
+
-- get the last time each of them was run, and then increment
|
2257
|
+
-- it by its interval. While this time is less than now,
|
2258
|
+
-- we need to keep putting jobs on the queue
|
2259
|
+
local r = redis.call('hmget', 'ql:r:' .. jid, 'klass', 'data', 'priority',
|
2260
|
+
'tags', 'retries', 'interval', 'backlog', 'throttles')
|
2261
|
+
local klass, data, priority, tags, retries, interval, backlog, throttles = unpack(
|
2262
|
+
redis.call('hmget', 'ql:r:' .. jid, 'klass', 'data', 'priority',
|
2263
|
+
'tags', 'retries', 'interval', 'backlog', 'throttles'))
|
2264
|
+
local _tags = cjson.decode(tags)
|
2265
|
+
local score = math.floor(tonumber(self.recurring.score(jid)))
|
2266
|
+
interval = tonumber(interval)
|
2267
|
+
|
2268
|
+
-- If the backlog is set for this job, then see if it's been a long
|
2269
|
+
-- time since the last pop
|
2270
|
+
backlog = tonumber(backlog or 0)
|
2271
|
+
if backlog ~= 0 then
|
2272
|
+
-- Check how many jobs we could concievably generate
|
2273
|
+
local num = ((now - score) / interval)
|
2274
|
+
if num > backlog then
|
2275
|
+
-- Update the score
|
2276
|
+
score = score + (
|
2277
|
+
math.ceil(num - backlog) * interval
|
2278
|
+
)
|
2279
|
+
end
|
2280
|
+
end
|
2281
|
+
|
2282
|
+
-- We're saving this value so that in the history, we can accurately
|
2283
|
+
-- reflect when the job would normally have been scheduled
|
2284
|
+
while (score <= now) and (moved < count) do
|
2285
|
+
local count = redis.call('hincrby', 'ql:r:' .. jid, 'count', 1)
|
2286
|
+
moved = moved + 1
|
2287
|
+
|
2288
|
+
local child_jid = jid .. '-' .. count
|
2289
|
+
|
2290
|
+
-- Add this job to the list of jobs tagged with whatever tags were
|
2291
|
+
-- supplied
|
2292
|
+
for _, tag in ipairs(_tags) do
|
2293
|
+
Reqless.job(child_jid):insert_tag(now, tag)
|
2294
|
+
end
|
2295
|
+
|
2296
|
+
-- First, let's save its data
|
2297
|
+
redis.call('hmset', ReqlessJob.ns .. child_jid,
|
2298
|
+
'jid' , child_jid,
|
2299
|
+
'klass' , klass,
|
2300
|
+
'data' , data,
|
2301
|
+
'priority' , priority,
|
2302
|
+
'tags' , tags,
|
2303
|
+
'state' , 'waiting',
|
2304
|
+
'worker' , '',
|
2305
|
+
'expires' , 0,
|
2306
|
+
'queue' , self.name,
|
2307
|
+
'retries' , retries,
|
2308
|
+
'remaining', retries,
|
2309
|
+
'time' , string.format("%.20f", score),
|
2310
|
+
'throttles', throttles,
|
2311
|
+
'spawned_from_jid', jid)
|
2312
|
+
|
2313
|
+
Reqless.job(child_jid):history(score, 'put', {queue = self.name})
|
2314
|
+
|
2315
|
+
-- Now, if a delay was provided, and if it's in the future,
|
2316
|
+
-- then we'll have to schedule it. Otherwise, we're just
|
2317
|
+
-- going to add it to the work queue.
|
2318
|
+
self.work.add(score, priority, child_jid)
|
2319
|
+
|
2320
|
+
score = score + interval
|
2321
|
+
self.recurring.add(score, jid)
|
2322
|
+
end
|
2323
|
+
end
|
2324
|
+
end
|
2325
|
+
|
2326
|
+
-- Check for any jobs that have been scheduled, and shovel them onto
|
2327
|
+
-- the work queue. Returns nothing, but afterwards, up to `count`
|
2328
|
+
-- scheduled jobs will be moved into the work queue
|
2329
|
+
function ReqlessQueue:check_scheduled(now, count)
|
2330
|
+
if count <= 0 then
|
2331
|
+
return
|
2332
|
+
end
|
2333
|
+
-- zadd is a list of arguments that we'll be able to use to
|
2334
|
+
-- insert into the work queue
|
2335
|
+
local scheduled = self.scheduled.ready(now, 0, count)
|
2336
|
+
for _, jid in ipairs(scheduled) do
|
2337
|
+
-- With these in hand, we'll have to go out and find the
|
2338
|
+
-- priorities of these jobs, and then we'll insert them
|
2339
|
+
-- into the work queue and then when that's complete, we'll
|
2340
|
+
-- remove them from the scheduled queue
|
2341
|
+
local priority = tonumber(
|
2342
|
+
redis.call('hget', ReqlessJob.ns .. jid, 'priority') or 0)
|
2343
|
+
self.work.add(now, priority, jid)
|
2344
|
+
self.scheduled.remove(jid)
|
2345
|
+
|
2346
|
+
-- We should also update them to have the state 'waiting'
|
2347
|
+
-- instead of 'scheduled'
|
2348
|
+
redis.call('hset', ReqlessJob.ns .. jid, 'state', 'waiting')
|
2349
|
+
end
|
2350
|
+
end
|
2351
|
+
|
2352
|
+
-- Check for and invalidate any locks that have been lost. Returns the
|
2353
|
+
-- list of jids that have been invalidated
|
2354
|
+
function ReqlessQueue:invalidate_locks(now, count)
|
2355
|
+
local jids = {}
|
2356
|
+
-- Iterate through all the expired locks and add them to the list
|
2357
|
+
-- of keys that we'll return
|
2358
|
+
for _, jid in ipairs(self.locks.expired(now, 0, count)) do
|
2359
|
+
-- Remove this job from the jobs that the worker that was running it
|
2360
|
+
-- has
|
2361
|
+
local worker, failure = unpack(
|
2362
|
+
redis.call('hmget', ReqlessJob.ns .. jid, 'worker', 'failure'))
|
2363
|
+
redis.call('zrem', 'ql:w:' .. worker .. ':jobs', jid)
|
2364
|
+
|
2365
|
+
-- We'll provide a grace period after jobs time out for them to give
|
2366
|
+
-- some indication of the failure mode. After that time, however, we'll
|
2367
|
+
-- consider the worker dust in the wind
|
2368
|
+
local grace_period = tonumber(Reqless.config.get('grace-period'))
|
2369
|
+
|
2370
|
+
-- Whether or not we've already sent a coutesy message
|
2371
|
+
local courtesy_sent = tonumber(
|
2372
|
+
redis.call('hget', ReqlessJob.ns .. jid, 'grace') or 0)
|
2373
|
+
|
2374
|
+
-- If the remaining value is an odd multiple of 0.5, then we'll assume
|
2375
|
+
-- that we're just sending the message. Otherwise, it's time to
|
2376
|
+
-- actually hand out the work to another worker
|
2377
|
+
local send_message = (courtesy_sent ~= 1)
|
2378
|
+
local invalidate = not send_message
|
2379
|
+
|
2380
|
+
-- If the grace period has been disabled, then we'll do both.
|
2381
|
+
if grace_period <= 0 then
|
2382
|
+
send_message = true
|
2383
|
+
invalidate = true
|
2384
|
+
end
|
2385
|
+
|
2386
|
+
if send_message then
|
2387
|
+
-- This is where we supply a courtesy message and give the worker
|
2388
|
+
-- time to provide a failure message
|
2389
|
+
if redis.call('zscore', 'ql:tracked', jid) ~= false then
|
2390
|
+
Reqless.publish('stalled', jid)
|
2391
|
+
end
|
2392
|
+
Reqless.job(jid):history(now, 'timed-out')
|
2393
|
+
redis.call('hset', ReqlessJob.ns .. jid, 'grace', 1)
|
2394
|
+
|
2395
|
+
-- Send a message to let the worker know that its lost its lock on
|
2396
|
+
-- the job
|
2397
|
+
local encoded = cjson.encode({
|
2398
|
+
jid = jid,
|
2399
|
+
event = 'lock_lost',
|
2400
|
+
worker = worker,
|
2401
|
+
})
|
2402
|
+
Reqless.publish('w:' .. worker, encoded)
|
2403
|
+
Reqless.publish('log', encoded)
|
2404
|
+
self.locks.add(now + grace_period, jid)
|
2405
|
+
|
2406
|
+
-- If we got any expired locks, then we should increment the
|
2407
|
+
-- number of retries for this stage for this bin. The bin is
|
2408
|
+
-- midnight of the provided day
|
2409
|
+
local bin = now - (now % 86400)
|
2410
|
+
redis.call('hincrby',
|
2411
|
+
'ql:s:stats:' .. bin .. ':' .. self.name, 'retries', 1)
|
2412
|
+
end
|
2413
|
+
|
2414
|
+
if invalidate then
|
2415
|
+
-- Unset the grace period attribute so that next time we'll send
|
2416
|
+
-- the grace period
|
2417
|
+
redis.call('hdel', ReqlessJob.ns .. jid, 'grace', 0)
|
2418
|
+
|
2419
|
+
-- See how many remaining retries the job has
|
2420
|
+
local remaining = tonumber(redis.call(
|
2421
|
+
'hincrby', ReqlessJob.ns .. jid, 'remaining', -1))
|
2422
|
+
|
2423
|
+
-- This is where we actually have to time out the work
|
2424
|
+
if remaining < 0 then
|
2425
|
+
-- Now remove the instance from the schedule, and work queues
|
2426
|
+
-- for the queue it's in
|
2427
|
+
self.work.remove(jid)
|
2428
|
+
self.locks.remove(jid)
|
2429
|
+
self.scheduled.remove(jid)
|
2430
|
+
|
2431
|
+
local job = Reqless.job(jid)
|
2432
|
+
local job_data = Reqless.job(jid):data()
|
2433
|
+
local queue = job_data['queue']
|
2434
|
+
local group = 'failed-retries-' .. queue
|
2435
|
+
|
2436
|
+
job:throttles_release(now)
|
2437
|
+
|
2438
|
+
job:history(now, 'failed', {group = group})
|
2439
|
+
redis.call('hmset', ReqlessJob.ns .. jid, 'state', 'failed',
|
2440
|
+
'worker', '',
|
2441
|
+
'expires', '')
|
2442
|
+
-- If the failure has not already been set, then set it
|
2443
|
+
redis.call('hset', ReqlessJob.ns .. jid,
|
2444
|
+
'failure', cjson.encode({
|
2445
|
+
group = group,
|
2446
|
+
message = 'Job exhausted retries in queue "' .. self.name .. '"',
|
2447
|
+
when = now,
|
2448
|
+
worker = unpack(job:data('worker'))
|
2449
|
+
}))
|
2450
|
+
|
2451
|
+
-- Add this type of failure to the list of failures
|
2452
|
+
redis.call('sadd', 'ql:failures', group)
|
2453
|
+
-- And add this particular instance to the failed types
|
2454
|
+
redis.call('lpush', 'ql:f:' .. group, jid)
|
2455
|
+
|
2456
|
+
if redis.call('zscore', 'ql:tracked', jid) ~= false then
|
2457
|
+
Reqless.publish('failed', jid)
|
2458
|
+
end
|
2459
|
+
Reqless.publish('log', cjson.encode({
|
2460
|
+
jid = jid,
|
2461
|
+
event = 'failed',
|
2462
|
+
group = group,
|
2463
|
+
worker = worker,
|
2464
|
+
message =
|
2465
|
+
'Job exhausted retries in queue "' .. self.name .. '"'
|
2466
|
+
}))
|
2467
|
+
|
2468
|
+
-- Increment the count of the failed jobs
|
2469
|
+
local bin = now - (now % 86400)
|
2470
|
+
redis.call('hincrby',
|
2471
|
+
'ql:s:stats:' .. bin .. ':' .. self.name, 'failures', 1)
|
2472
|
+
redis.call('hincrby',
|
2473
|
+
'ql:s:stats:' .. bin .. ':' .. self.name, 'failed' , 1)
|
2474
|
+
else
|
2475
|
+
table.insert(jids, jid)
|
2476
|
+
end
|
2477
|
+
end
|
2478
|
+
end
|
2479
|
+
|
2480
|
+
return jids
|
2481
|
+
end
|
2482
|
+
|
2483
|
+
-- Forget the provided queues. As in, remove them from the list of known queues
|
2484
|
+
function ReqlessQueue.deregister(...)
|
2485
|
+
redis.call('zrem', Reqless.ns .. 'queues', unpack(arg))
|
2486
|
+
end
|
2487
|
+
|
2488
|
+
-- Return information about a particular queue, or all queues
|
2489
|
+
-- [
|
2490
|
+
-- {
|
2491
|
+
-- 'name': 'testing',
|
2492
|
+
-- 'stalled': 2,
|
2493
|
+
-- 'waiting': 5,
|
2494
|
+
-- 'running': 5,
|
2495
|
+
-- 'scheduled': 10,
|
2496
|
+
-- 'depends': 5,
|
2497
|
+
-- 'recurring': 0
|
2498
|
+
-- }, {
|
2499
|
+
-- ...
|
2500
|
+
-- }
|
2501
|
+
-- ]
|
2502
|
+
function ReqlessQueue.counts(now, name)
|
2503
|
+
if name then
|
2504
|
+
local queue = Reqless.queue(name)
|
2505
|
+
local stalled = queue.locks.length(now)
|
2506
|
+
-- Check for any scheduled jobs that need to be moved
|
2507
|
+
queue:check_scheduled(now, queue.scheduled.length())
|
2508
|
+
return {
|
2509
|
+
name = name,
|
2510
|
+
waiting = queue.work.length(),
|
2511
|
+
stalled = stalled,
|
2512
|
+
running = queue.locks.length() - stalled,
|
2513
|
+
throttled = queue.throttled.length(),
|
2514
|
+
scheduled = queue.scheduled.length(),
|
2515
|
+
depends = queue.depends.length(),
|
2516
|
+
recurring = queue.recurring.length(),
|
2517
|
+
paused = queue:paused()
|
2518
|
+
}
|
2519
|
+
end
|
2520
|
+
|
2521
|
+
local queues = redis.call('zrange', 'ql:queues', 0, -1)
|
2522
|
+
local response = {}
|
2523
|
+
for _, qname in ipairs(queues) do
|
2524
|
+
table.insert(response, ReqlessQueue.counts(now, qname))
|
2525
|
+
end
|
2526
|
+
return response
|
2527
|
+
end
|
2528
|
+
local ReqlessQueuePatterns = {
|
2529
|
+
default_identifiers_default_pattern = '["*"]',
|
2530
|
+
ns = Reqless.ns .. "qp:",
|
2531
|
+
}
|
2532
|
+
ReqlessQueuePatterns.__index = ReqlessQueuePatterns
|
2533
|
+
|
2534
|
+
ReqlessQueuePatterns['getIdentifierPatterns'] = function(now)
|
2535
|
+
local reply = redis.call('hgetall', ReqlessQueuePatterns.ns .. 'identifiers')
|
2536
|
+
|
2537
|
+
if #reply == 0 then
|
2538
|
+
-- Check legacy key
|
2539
|
+
reply = redis.call('hgetall', 'qmore:dynamic')
|
2540
|
+
end
|
2541
|
+
|
2542
|
+
-- Include default pattern in case identifier patterns have never been set.
|
2543
|
+
local identifierPatterns = {
|
2544
|
+
['default'] = ReqlessQueuePatterns.default_identifiers_default_pattern,
|
2545
|
+
}
|
2546
|
+
for i = 1, #reply, 2 do
|
2547
|
+
identifierPatterns[reply[i]] = reply[i + 1]
|
2548
|
+
end
|
2549
|
+
|
2550
|
+
return identifierPatterns
|
2551
|
+
end
|
2552
|
+
|
2553
|
+
-- Each key is a string and each value is string containing a JSON list of
|
2554
|
+
-- patterns.
|
2555
|
+
ReqlessQueuePatterns['setIdentifierPatterns'] = function(now, ...)
|
2556
|
+
if #arg % 2 == 1 then
|
2557
|
+
error('Odd number of identifier patterns: ' .. tostring(arg))
|
2558
|
+
end
|
2559
|
+
local key = ReqlessQueuePatterns.ns .. 'identifiers'
|
2560
|
+
|
2561
|
+
local goodDefault = false;
|
2562
|
+
local identifierPatterns = {}
|
2563
|
+
for i = 1, #arg, 2 do
|
2564
|
+
local key = arg[i]
|
2565
|
+
local serializedValues = arg[i + 1]
|
2566
|
+
|
2567
|
+
-- Ensure that the value is valid JSON.
|
2568
|
+
local values = cjson.decode(serializedValues)
|
2569
|
+
|
2570
|
+
-- Only write the value if there are items in the list.
|
2571
|
+
if #values > 0 then
|
2572
|
+
if key == 'default' then
|
2573
|
+
goodDefault = true
|
2574
|
+
end
|
2575
|
+
table.insert(identifierPatterns, key)
|
2576
|
+
table.insert(identifierPatterns, serializedValues)
|
2577
|
+
end
|
2578
|
+
end
|
2579
|
+
|
2580
|
+
-- Ensure some kind of default value is persisted.
|
2581
|
+
if not goodDefault then
|
2582
|
+
table.insert(identifierPatterns, "default")
|
2583
|
+
table.insert(
|
2584
|
+
identifierPatterns,
|
2585
|
+
ReqlessQueuePatterns.default_identifiers_default_pattern
|
2586
|
+
)
|
2587
|
+
end
|
2588
|
+
|
2589
|
+
-- Clear out the legacy key too
|
2590
|
+
redis.call('del', key, 'qmore:dynamic')
|
2591
|
+
redis.call('hset', key, unpack(identifierPatterns))
|
2592
|
+
end
|
2593
|
+
|
2594
|
+
ReqlessQueuePatterns['getPriorityPatterns'] = function(now)
|
2595
|
+
local reply = redis.call('lrange', ReqlessQueuePatterns.ns .. 'priorities', 0, -1)
|
2596
|
+
|
2597
|
+
if #reply == 0 then
|
2598
|
+
-- Check legacy key
|
2599
|
+
reply = redis.call('lrange', 'qmore:priority', 0, -1)
|
2600
|
+
end
|
2601
|
+
|
2602
|
+
return reply
|
2603
|
+
end
|
2604
|
+
|
2605
|
+
-- Each key is a string and each value is a string containing a JSON object
|
2606
|
+
-- where the JSON object has a shape like:
|
2607
|
+
-- {"fairly": true, "pattern": ["string", "string", "string"]}
|
2608
|
+
ReqlessQueuePatterns['setPriorityPatterns'] = function(now, ...)
|
2609
|
+
local key = ReqlessQueuePatterns.ns .. 'priorities'
|
2610
|
+
redis.call('del', key)
|
2611
|
+
-- Clear out the legacy key
|
2612
|
+
redis.call('del', 'qmore:priority')
|
2613
|
+
if #arg > 0 then
|
2614
|
+
redis.call('rpush', key, unpack(arg))
|
2615
|
+
end
|
2616
|
+
end
|
2617
|
+
-- Get all the attributes of this particular job
|
2618
|
+
function ReqlessRecurringJob:data()
|
2619
|
+
local job = redis.call(
|
2620
|
+
'hmget', 'ql:r:' .. self.jid, 'jid', 'klass', 'state', 'queue',
|
2621
|
+
'priority', 'interval', 'retries', 'count', 'data', 'tags', 'backlog', 'throttles')
|
2622
|
+
|
2623
|
+
if not job[1] then
|
2624
|
+
return nil
|
2625
|
+
end
|
2626
|
+
|
2627
|
+
return {
|
2628
|
+
jid = job[1],
|
2629
|
+
klass = job[2],
|
2630
|
+
state = job[3],
|
2631
|
+
queue = job[4],
|
2632
|
+
priority = tonumber(job[5]),
|
2633
|
+
interval = tonumber(job[6]),
|
2634
|
+
retries = tonumber(job[7]),
|
2635
|
+
count = tonumber(job[8]),
|
2636
|
+
data = job[9],
|
2637
|
+
tags = cjson.decode(job[10]),
|
2638
|
+
backlog = tonumber(job[11] or 0),
|
2639
|
+
throttles = cjson.decode(job[12] or '[]'),
|
2640
|
+
}
|
2641
|
+
end
|
2642
|
+
|
2643
|
+
-- Update the recurring job data. Key can be:
|
2644
|
+
-- - priority
|
2645
|
+
-- - interval
|
2646
|
+
-- - retries
|
2647
|
+
-- - data
|
2648
|
+
-- - klass
|
2649
|
+
-- - queue
|
2650
|
+
-- - backlog
|
2651
|
+
function ReqlessRecurringJob:update(now, ...)
|
2652
|
+
local options = {}
|
2653
|
+
-- Make sure that the job exists
|
2654
|
+
if redis.call('exists', 'ql:r:' .. self.jid) == 0 then
|
2655
|
+
error('Recur(): No recurring job ' .. self.jid)
|
2656
|
+
end
|
2657
|
+
|
2658
|
+
for i = 1, #arg, 2 do
|
2659
|
+
local key = arg[i]
|
2660
|
+
local value = arg[i+1]
|
2661
|
+
assert(value, 'No value provided for ' .. tostring(key))
|
2662
|
+
if key == 'priority' or key == 'interval' or key == 'retries' then
|
2663
|
+
value = assert(tonumber(value), 'Recur(): Arg "' .. key .. '" must be a number: ' .. tostring(value))
|
2664
|
+
-- If the command is 'interval', then we need to update the
|
2665
|
+
-- time when it should next be scheduled
|
2666
|
+
if key == 'interval' then
|
2667
|
+
local queue, interval = unpack(redis.call('hmget', 'ql:r:' .. self.jid, 'queue', 'interval'))
|
2668
|
+
Reqless.queue(queue).recurring.update(
|
2669
|
+
value - tonumber(interval), self.jid)
|
2670
|
+
end
|
2671
|
+
redis.call('hset', 'ql:r:' .. self.jid, key, value)
|
2672
|
+
elseif key == 'data' then
|
2673
|
+
assert(cjson.decode(value), 'Recur(): Arg "data" is not JSON-encoded: ' .. tostring(value))
|
2674
|
+
redis.call('hset', 'ql:r:' .. self.jid, 'data', value)
|
2675
|
+
elseif key == 'klass' then
|
2676
|
+
redis.call('hset', 'ql:r:' .. self.jid, 'klass', value)
|
2677
|
+
elseif key == 'queue' then
|
2678
|
+
local old_queue_name = redis.call('hget', 'ql:r:' .. self.jid, 'queue')
|
2679
|
+
local queue_obj = Reqless.queue(old_queue_name)
|
2680
|
+
local score = queue_obj.recurring.score(self.jid)
|
2681
|
+
|
2682
|
+
-- Detach from the old queue
|
2683
|
+
queue_obj.recurring.remove(self.jid)
|
2684
|
+
local throttles = cjson.decode(redis.call('hget', 'ql:r:' .. self.jid, 'throttles') or '{}')
|
2685
|
+
for index, throttle_name in ipairs(throttles) do
|
2686
|
+
if throttle_name == ReqlessQueue.ns .. old_queue_name then
|
2687
|
+
table.remove(throttles, index)
|
2688
|
+
end
|
2689
|
+
end
|
2690
|
+
|
2691
|
+
|
2692
|
+
-- Attach to the new queue
|
2693
|
+
table.insert(throttles, ReqlessQueue.ns .. value)
|
2694
|
+
redis.call('hset', 'ql:r:' .. self.jid, 'throttles', cjson.encode(throttles))
|
2695
|
+
|
2696
|
+
Reqless.queue(value).recurring.add(score, self.jid)
|
2697
|
+
redis.call('hset', 'ql:r:' .. self.jid, 'queue', value)
|
2698
|
+
-- If we don't already know about the queue, learn about it
|
2699
|
+
if redis.call('zscore', 'ql:queues', value) == false then
|
2700
|
+
redis.call('zadd', 'ql:queues', now, value)
|
2701
|
+
end
|
2702
|
+
elseif key == 'backlog' then
|
2703
|
+
value = assert(tonumber(value),
|
2704
|
+
'Recur(): Arg "backlog" not a number: ' .. tostring(value))
|
2705
|
+
redis.call('hset', 'ql:r:' .. self.jid, 'backlog', value)
|
2706
|
+
elseif key == 'throttles' then
|
2707
|
+
local throttles = assert(cjson.decode(value), 'Recur(): Arg "throttles" is not JSON-encoded: ' .. tostring(value))
|
2708
|
+
redis.call('hset', 'ql:r:' .. self.jid, 'throttles', cjson.encode(throttles))
|
2709
|
+
else
|
2710
|
+
error('Recur(): Unrecognized option "' .. key .. '"')
|
2711
|
+
end
|
2712
|
+
end
|
2713
|
+
|
2714
|
+
return true
|
2715
|
+
end
|
2716
|
+
|
2717
|
+
-- Tags this recurring job with the provided tags
|
2718
|
+
function ReqlessRecurringJob:tag(...)
|
2719
|
+
local tags = redis.call('hget', 'ql:r:' .. self.jid, 'tags')
|
2720
|
+
-- If the job has been canceled / deleted, then throw an error.
|
2721
|
+
if not tags then
|
2722
|
+
error('Tag(): Job ' .. self.jid .. ' does not exist')
|
2723
|
+
end
|
2724
|
+
|
2725
|
+
-- Decode the json blob, convert to dictionary
|
2726
|
+
tags = cjson.decode(tags)
|
2727
|
+
local _tags = {}
|
2728
|
+
for _, v in ipairs(tags) do
|
2729
|
+
_tags[v] = true
|
2730
|
+
end
|
2731
|
+
|
2732
|
+
-- Otherwise, add the job to the sorted set with that tags
|
2733
|
+
for i = 1, #arg do
|
2734
|
+
if _tags[arg[i]] == nil then
|
2735
|
+
table.insert(tags, arg[i])
|
2736
|
+
end
|
2737
|
+
end
|
2738
|
+
|
2739
|
+
tags = cjsonArrayDegenerationWorkaround(tags)
|
2740
|
+
redis.call('hset', 'ql:r:' .. self.jid, 'tags', tags)
|
2741
|
+
|
2742
|
+
return tags
|
2743
|
+
end
|
2744
|
+
|
2745
|
+
-- Removes a tag from the recurring job
|
2746
|
+
function ReqlessRecurringJob:untag(...)
|
2747
|
+
-- Get the existing tags
|
2748
|
+
local tags = redis.call('hget', 'ql:r:' .. self.jid, 'tags')
|
2749
|
+
|
2750
|
+
-- If the job has been canceled / deleted, then return false
|
2751
|
+
if not tags then
|
2752
|
+
error('Untag(): Job ' .. self.jid .. ' does not exist')
|
2753
|
+
end
|
2754
|
+
|
2755
|
+
-- Decode the json blob, convert to dictionary
|
2756
|
+
tags = cjson.decode(tags)
|
2757
|
+
|
2758
|
+
local _tags = {}
|
2759
|
+
-- Make a dictionary
|
2760
|
+
for _, v in ipairs(tags) do
|
2761
|
+
_tags[v] = true
|
2762
|
+
end
|
2763
|
+
|
2764
|
+
-- Delete these from the hash
|
2765
|
+
for i = 1, #arg do
|
2766
|
+
_tags[arg[i]] = nil
|
2767
|
+
end
|
2768
|
+
|
2769
|
+
-- Back into a list
|
2770
|
+
local results = {}
|
2771
|
+
for _, tag in ipairs(tags) do
|
2772
|
+
if _tags[tag] then
|
2773
|
+
table.insert(results, tag)
|
2774
|
+
end
|
2775
|
+
end
|
2776
|
+
|
2777
|
+
-- json encode them, set, and return
|
2778
|
+
tags = cjson.encode(results)
|
2779
|
+
redis.call('hset', 'ql:r:' .. self.jid, 'tags', tags)
|
2780
|
+
|
2781
|
+
return tags
|
2782
|
+
end
|
2783
|
+
|
2784
|
+
-- Stop further occurrences of this job
|
2785
|
+
function ReqlessRecurringJob:cancel()
|
2786
|
+
-- First, find out what queue it was attached to
|
2787
|
+
local queue = redis.call('hget', 'ql:r:' .. self.jid, 'queue')
|
2788
|
+
if queue then
|
2789
|
+
-- Now, delete it from the queue it was attached to, and delete the
|
2790
|
+
-- thing itself
|
2791
|
+
Reqless.queue(queue).recurring.remove(self.jid)
|
2792
|
+
redis.call('del', 'ql:r:' .. self.jid)
|
2793
|
+
end
|
2794
|
+
|
2795
|
+
return true
|
2796
|
+
end
|
2797
|
+
-- Deregisters these workers from the list of known workers
|
2798
|
+
function ReqlessWorker.deregister(...)
|
2799
|
+
redis.call('zrem', 'ql:workers', unpack(arg))
|
2800
|
+
end
|
2801
|
+
|
2802
|
+
-- Provide data about all the workers, or if a specific worker is provided,
|
2803
|
+
-- then which jobs that worker is responsible for. If no worker is provided,
|
2804
|
+
-- expect a response of the form:
|
2805
|
+
--
|
2806
|
+
-- [
|
2807
|
+
-- # This is sorted by the recency of activity from that worker
|
2808
|
+
-- {
|
2809
|
+
-- 'name' : 'hostname1-pid1',
|
2810
|
+
-- 'jobs' : 20,
|
2811
|
+
-- 'stalled': 0
|
2812
|
+
-- }, {
|
2813
|
+
-- ...
|
2814
|
+
-- }
|
2815
|
+
-- ]
|
2816
|
+
--
|
2817
|
+
-- If a worker id is provided, then expect a response of the form:
|
2818
|
+
--
|
2819
|
+
-- {
|
2820
|
+
-- 'jobs': [
|
2821
|
+
-- jid1,
|
2822
|
+
-- jid2,
|
2823
|
+
-- ...
|
2824
|
+
-- ], 'stalled': [
|
2825
|
+
-- jid1,
|
2826
|
+
-- ...
|
2827
|
+
-- ]
|
2828
|
+
-- }
|
2829
|
+
--
|
2830
|
+
function ReqlessWorker.counts(now, worker)
|
2831
|
+
-- Clean up all the workers' job lists if they're too old. This is
|
2832
|
+
-- determined by the `max-worker-age` configuration, defaulting to the
|
2833
|
+
-- last day. Seems like a 'reasonable' default
|
2834
|
+
local interval = tonumber(Reqless.config.get('max-worker-age', 86400))
|
2835
|
+
|
2836
|
+
local workers = redis.call('zrangebyscore', 'ql:workers', 0, now - interval)
|
2837
|
+
for _, worker in ipairs(workers) do
|
2838
|
+
redis.call('del', 'ql:w:' .. worker .. ':jobs')
|
2839
|
+
end
|
2840
|
+
|
2841
|
+
-- And now remove them from the list of known workers
|
2842
|
+
redis.call('zremrangebyscore', 'ql:workers', 0, now - interval)
|
2843
|
+
|
2844
|
+
if worker then
|
2845
|
+
return {
|
2846
|
+
jobs = redis.call('zrevrangebyscore', 'ql:w:' .. worker .. ':jobs', now + 8640000, now),
|
2847
|
+
stalled = redis.call('zrevrangebyscore', 'ql:w:' .. worker .. ':jobs', now, 0)
|
2848
|
+
}
|
2849
|
+
end
|
2850
|
+
|
2851
|
+
local response = {}
|
2852
|
+
local workers = redis.call('zrevrange', 'ql:workers', 0, -1)
|
2853
|
+
for _, worker in ipairs(workers) do
|
2854
|
+
table.insert(response, {
|
2855
|
+
name = worker,
|
2856
|
+
jobs = redis.call('zcount', 'ql:w:' .. worker .. ':jobs', now, now + 8640000),
|
2857
|
+
stalled = redis.call('zcount', 'ql:w:' .. worker .. ':jobs', 0, now)
|
2858
|
+
})
|
2859
|
+
end
|
2860
|
+
return response
|
2861
|
+
end
|
2862
|
+
-- Retrieve the data for a throttled resource
|
2863
|
+
function ReqlessThrottle:data()
|
2864
|
+
-- Default values for the data
|
2865
|
+
local data = {
|
2866
|
+
id = self.id,
|
2867
|
+
maximum = 0
|
2868
|
+
}
|
2869
|
+
|
2870
|
+
-- Retrieve data stored in redis
|
2871
|
+
local throttle = redis.call('hmget', ReqlessThrottle.ns .. self.id, 'id', 'maximum')
|
2872
|
+
|
2873
|
+
if throttle[2] then
|
2874
|
+
data.maximum = tonumber(throttle[2])
|
2875
|
+
end
|
2876
|
+
|
2877
|
+
return data
|
2878
|
+
end
|
2879
|
+
|
2880
|
+
-- Like data, but includes ttl.
|
2881
|
+
function ReqlessThrottle:dataWithTtl()
|
2882
|
+
local data = self:data()
|
2883
|
+
data.ttl = self:ttl()
|
2884
|
+
return data
|
2885
|
+
end
|
2886
|
+
|
2887
|
+
-- Set the data for a throttled resource
|
2888
|
+
function ReqlessThrottle:set(data, expiration)
|
2889
|
+
redis.call('hmset', ReqlessThrottle.ns .. self.id, 'id', self.id, 'maximum', data.maximum)
|
2890
|
+
if expiration > 0 then
|
2891
|
+
redis.call('expire', ReqlessThrottle.ns .. self.id, expiration)
|
2892
|
+
end
|
2893
|
+
end
|
2894
|
+
|
2895
|
+
-- Delete a throttled resource
|
2896
|
+
function ReqlessThrottle:unset()
|
2897
|
+
redis.call('del', ReqlessThrottle.ns .. self.id)
|
2898
|
+
end
|
2899
|
+
|
2900
|
+
-- Acquire a throttled resource for a job.
|
2901
|
+
-- Returns true of the job acquired the resource, false otherwise
|
2902
|
+
function ReqlessThrottle:acquire(jid)
|
2903
|
+
if not self:available() then
|
2904
|
+
return false
|
2905
|
+
end
|
2906
|
+
|
2907
|
+
self.locks.add(1, jid)
|
2908
|
+
return true
|
2909
|
+
end
|
2910
|
+
|
2911
|
+
function ReqlessThrottle:pend(now, jid)
|
2912
|
+
self.pending.add(now, jid)
|
2913
|
+
end
|
2914
|
+
|
2915
|
+
-- Releases the lock taken by the specified jid.
|
2916
|
+
-- number of jobs released back into the queues is determined by the locks_available method.
|
2917
|
+
function ReqlessThrottle:release(now, jid)
|
2918
|
+
-- Only attempt to remove from the pending set if the job wasn't found in the
|
2919
|
+
-- locks set
|
2920
|
+
if self.locks.remove(jid) == 0 then
|
2921
|
+
self.pending.remove(jid)
|
2922
|
+
end
|
2923
|
+
|
2924
|
+
local available_locks = self:locks_available()
|
2925
|
+
if self.pending.length() == 0 or available_locks < 1 then
|
2926
|
+
return
|
2927
|
+
end
|
2928
|
+
|
2929
|
+
-- subtract one to ensure we pop the correct amount. peek(0, 0) returns the first element
|
2930
|
+
-- peek(0,1) return the first two.
|
2931
|
+
for _, jid in ipairs(self.pending.peek(0, available_locks - 1)) do
|
2932
|
+
local job = Reqless.job(jid)
|
2933
|
+
local data = job:data()
|
2934
|
+
local queue = Reqless.queue(data['queue'])
|
2935
|
+
|
2936
|
+
queue.throttled.remove(jid)
|
2937
|
+
queue.work.add(now, data.priority, jid)
|
2938
|
+
end
|
2939
|
+
|
2940
|
+
-- subtract one to ensure we pop the correct amount. pop(0, 0) pops the first element
|
2941
|
+
-- pop(0,1) pops the first two.
|
2942
|
+
local popped = self.pending.pop(0, available_locks - 1)
|
2943
|
+
end
|
2944
|
+
|
2945
|
+
-- Returns true if the throttle has locks available, false otherwise.
|
2946
|
+
function ReqlessThrottle:available()
|
2947
|
+
return self.maximum == 0 or self.locks.length() < self.maximum
|
2948
|
+
end
|
2949
|
+
|
2950
|
+
-- Returns the TTL of the throttle
|
2951
|
+
function ReqlessThrottle:ttl()
|
2952
|
+
return redis.call('ttl', ReqlessThrottle.ns .. self.id)
|
2953
|
+
end
|
2954
|
+
|
2955
|
+
-- Returns the number of locks available for the throttle.
|
2956
|
+
-- calculated by maximum - locks.length(), if the throttle is unlimited
|
2957
|
+
-- then up to 10 jobs are released.
|
2958
|
+
function ReqlessThrottle:locks_available()
|
2959
|
+
if self.maximum == 0 then
|
2960
|
+
-- Arbitrarily chosen value. might want to make it configurable in the future.
|
2961
|
+
return 10
|
2962
|
+
end
|
2963
|
+
|
2964
|
+
return self.maximum - self.locks.length()
|
2965
|
+
end
|