qless 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +8 -0
- data/HISTORY.md +168 -0
- data/README.md +571 -0
- data/Rakefile +28 -0
- data/bin/qless-campfire +106 -0
- data/bin/qless-growl +99 -0
- data/bin/qless-web +23 -0
- data/lib/qless.rb +185 -0
- data/lib/qless/config.rb +31 -0
- data/lib/qless/job.rb +259 -0
- data/lib/qless/job_reservers/ordered.rb +23 -0
- data/lib/qless/job_reservers/round_robin.rb +34 -0
- data/lib/qless/lua.rb +25 -0
- data/lib/qless/qless-core/cancel.lua +71 -0
- data/lib/qless/qless-core/complete.lua +218 -0
- data/lib/qless/qless-core/config.lua +44 -0
- data/lib/qless/qless-core/depends.lua +65 -0
- data/lib/qless/qless-core/fail.lua +107 -0
- data/lib/qless/qless-core/failed.lua +83 -0
- data/lib/qless/qless-core/get.lua +37 -0
- data/lib/qless/qless-core/heartbeat.lua +50 -0
- data/lib/qless/qless-core/jobs.lua +41 -0
- data/lib/qless/qless-core/peek.lua +155 -0
- data/lib/qless/qless-core/pop.lua +278 -0
- data/lib/qless/qless-core/priority.lua +32 -0
- data/lib/qless/qless-core/put.lua +156 -0
- data/lib/qless/qless-core/queues.lua +58 -0
- data/lib/qless/qless-core/recur.lua +181 -0
- data/lib/qless/qless-core/retry.lua +73 -0
- data/lib/qless/qless-core/ruby/lib/qless-core.rb +1 -0
- data/lib/qless/qless-core/ruby/lib/qless/core.rb +13 -0
- data/lib/qless/qless-core/ruby/lib/qless/core/version.rb +5 -0
- data/lib/qless/qless-core/ruby/spec/qless_core_spec.rb +13 -0
- data/lib/qless/qless-core/stats.lua +92 -0
- data/lib/qless/qless-core/tag.lua +100 -0
- data/lib/qless/qless-core/track.lua +79 -0
- data/lib/qless/qless-core/workers.lua +69 -0
- data/lib/qless/queue.rb +141 -0
- data/lib/qless/server.rb +411 -0
- data/lib/qless/tasks.rb +10 -0
- data/lib/qless/version.rb +3 -0
- data/lib/qless/worker.rb +195 -0
- metadata +239 -0
data/lib/qless/job.rb
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
require "qless"
|
2
|
+
require "qless/queue"
|
3
|
+
require "qless/lua"
|
4
|
+
require "redis"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
module Qless
|
8
|
+
class BaseJob
|
9
|
+
def initialize(client, jid)
|
10
|
+
@client = client
|
11
|
+
@jid = jid
|
12
|
+
end
|
13
|
+
|
14
|
+
def klass
|
15
|
+
@klass ||= @klass_name.split('::').inject(Kernel) { |context, name| context.const_get(name) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def queue
|
19
|
+
@queue ||= Queue.new(@queue_name, @client)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Job < BaseJob
|
24
|
+
attr_reader :jid, :expires_at, :state, :queue_name, :history, :worker_name, :failure, :klass_name, :tracked, :dependencies, :dependents
|
25
|
+
attr_reader :original_retries, :retries_left
|
26
|
+
attr_accessor :data, :priority, :tags
|
27
|
+
|
28
|
+
def perform
|
29
|
+
klass.perform(self)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.build(client, klass, attributes = {})
|
33
|
+
defaults = {
|
34
|
+
"jid" => Qless.generate_jid,
|
35
|
+
"data" => {},
|
36
|
+
"klass" => klass.to_s,
|
37
|
+
"priority" => 0,
|
38
|
+
"tags" => [],
|
39
|
+
"worker" => "mock_worker",
|
40
|
+
"expires" => Time.now + (60 * 60), # an hour from now
|
41
|
+
"state" => "running",
|
42
|
+
"tracked" => false,
|
43
|
+
"queue" => "mock_queue",
|
44
|
+
"retries" => 5,
|
45
|
+
"remaining" => 5,
|
46
|
+
"failure" => {},
|
47
|
+
"history" => [],
|
48
|
+
"dependencies" => [],
|
49
|
+
"dependents" => []
|
50
|
+
}
|
51
|
+
attributes = defaults.merge(Qless.stringify_hash_keys(attributes))
|
52
|
+
attributes["data"] = JSON.load(JSON.dump attributes["data"])
|
53
|
+
new(client, attributes)
|
54
|
+
end
|
55
|
+
|
56
|
+
def initialize(client, atts)
|
57
|
+
super(client, atts.fetch('jid'))
|
58
|
+
%w{jid data priority tags state tracked
|
59
|
+
failure history dependencies dependents}.each do |att|
|
60
|
+
self.instance_variable_set("@#{att}".to_sym, atts.fetch(att))
|
61
|
+
end
|
62
|
+
|
63
|
+
@expires_at = atts.fetch('expires')
|
64
|
+
@klass_name = atts.fetch('klass')
|
65
|
+
@queue_name = atts.fetch('queue')
|
66
|
+
@worker_name = atts.fetch('worker')
|
67
|
+
@original_retries = atts.fetch('retries')
|
68
|
+
@retries_left = atts.fetch('remaining')
|
69
|
+
|
70
|
+
# This is a silly side-effect of Lua doing JSON parsing
|
71
|
+
@tags = [] if @tags == {}
|
72
|
+
@dependents = [] if @dependents == {}
|
73
|
+
@dependencies = [] if @dependencies == {}
|
74
|
+
@state_changed = false
|
75
|
+
end
|
76
|
+
|
77
|
+
def priority=(priority)
|
78
|
+
if @client._priority.call([], [@jid, priority])
|
79
|
+
@priority = priority
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def [](key)
|
84
|
+
@data[key]
|
85
|
+
end
|
86
|
+
|
87
|
+
def []=(key, val)
|
88
|
+
@data[key] = val
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_s
|
92
|
+
inspect
|
93
|
+
end
|
94
|
+
|
95
|
+
def description
|
96
|
+
"#{@jid} (#{@klass_name} / #{@queue_name})"
|
97
|
+
end
|
98
|
+
|
99
|
+
def inspect
|
100
|
+
"<Qless::Job #{description}>"
|
101
|
+
end
|
102
|
+
|
103
|
+
def ttl
|
104
|
+
@expires_at - Time.now.to_f
|
105
|
+
end
|
106
|
+
|
107
|
+
# Move this from it's current queue into another
|
108
|
+
def move(queue)
|
109
|
+
note_state_change do
|
110
|
+
@client._put.call([queue], [
|
111
|
+
@jid, @klass_name, JSON.generate(@data), Time.now.to_f, 0
|
112
|
+
])
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Fail a job
|
117
|
+
def fail(group, message)
|
118
|
+
note_state_change do
|
119
|
+
@client._fail.call([], [
|
120
|
+
@jid,
|
121
|
+
@worker_name,
|
122
|
+
group, message,
|
123
|
+
Time.now.to_f,
|
124
|
+
JSON.generate(@data)]) || false
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Heartbeat a job
|
129
|
+
def heartbeat()
|
130
|
+
@client._heartbeat.call([], [
|
131
|
+
@jid,
|
132
|
+
@worker_name,
|
133
|
+
Time.now.to_f,
|
134
|
+
JSON.generate(@data)]) || false
|
135
|
+
end
|
136
|
+
|
137
|
+
# Complete a job
|
138
|
+
# Options include
|
139
|
+
# => next (String) the next queue
|
140
|
+
# => delay (int) how long to delay it in the next queue
|
141
|
+
def complete(nxt=nil, options={})
|
142
|
+
response = note_state_change do
|
143
|
+
if nxt.nil?
|
144
|
+
@client._complete.call([], [
|
145
|
+
@jid, @worker_name, @queue_name, Time.now.to_f, JSON.generate(@data)])
|
146
|
+
else
|
147
|
+
@client._complete.call([], [
|
148
|
+
@jid, @worker_name, @queue_name, Time.now.to_f, JSON.generate(@data), 'next', nxt, 'delay',
|
149
|
+
options.fetch(:delay, 0), 'depends', JSON.generate(options.fetch(:depends, []))])
|
150
|
+
end
|
151
|
+
end
|
152
|
+
response.nil? ? false : response
|
153
|
+
end
|
154
|
+
|
155
|
+
def state_changed?
|
156
|
+
@state_changed
|
157
|
+
end
|
158
|
+
|
159
|
+
def cancel
|
160
|
+
note_state_change do
|
161
|
+
@client._cancel.call([], [@jid])
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def track()
|
166
|
+
@client._track.call([], ['track', @jid, Time.now.to_f])
|
167
|
+
end
|
168
|
+
|
169
|
+
def untrack
|
170
|
+
@client._track.call([], ['untrack', @jid, Time.now.to_f])
|
171
|
+
end
|
172
|
+
|
173
|
+
def tag(*tags)
|
174
|
+
@client._tag.call([], ['add', @jid, Time.now.to_f] + tags)
|
175
|
+
end
|
176
|
+
|
177
|
+
def untag(*tags)
|
178
|
+
@client._tag.call([], ['remove', @jid, Time.now.to_f] + tags)
|
179
|
+
end
|
180
|
+
|
181
|
+
def retry(delay=0)
|
182
|
+
results = @client._retry.call([], [@jid, @queue_name, @worker_name, Time.now.to_f, delay])
|
183
|
+
results.nil? ? false : results
|
184
|
+
end
|
185
|
+
|
186
|
+
def depend(*jids)
|
187
|
+
!!@client._depends.call([], [@jid, 'on'] + jids)
|
188
|
+
end
|
189
|
+
|
190
|
+
def undepend(*jids)
|
191
|
+
!!@client._depends.call([], [@jid, 'off'] + jids)
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
def note_state_change
|
197
|
+
result = yield
|
198
|
+
@state_changed = true
|
199
|
+
result
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
class RecurringJob < BaseJob
|
204
|
+
attr_reader :jid, :data, :priority, :tags, :retries, :interval, :count, :queue_name, :klass_name
|
205
|
+
|
206
|
+
def initialize(client, atts)
|
207
|
+
super(client, atts.fetch('jid'))
|
208
|
+
%w{jid data priority tags retries interval count}.each do |att|
|
209
|
+
self.instance_variable_set("@#{att}".to_sym, atts.fetch(att))
|
210
|
+
end
|
211
|
+
|
212
|
+
@klass_name = atts.fetch('klass')
|
213
|
+
@queue_name = atts.fetch('queue')
|
214
|
+
@tags = [] if @tags == {}
|
215
|
+
end
|
216
|
+
|
217
|
+
def priority=(value)
|
218
|
+
@client._recur.call([], ['update', @jid, 'priority', value])
|
219
|
+
@priority = value
|
220
|
+
end
|
221
|
+
|
222
|
+
def retries=(value)
|
223
|
+
@client._recur.call([], ['update', @jid, 'retries', value])
|
224
|
+
@retries = value
|
225
|
+
end
|
226
|
+
|
227
|
+
def interval=(value)
|
228
|
+
@client._recur.call([], ['update', @jid, 'interval', value])
|
229
|
+
@interval = value
|
230
|
+
end
|
231
|
+
|
232
|
+
def data=(value)
|
233
|
+
@client._recur.call([], ['update', @jid, 'data', JSON.generate(value)])
|
234
|
+
@data = value
|
235
|
+
end
|
236
|
+
|
237
|
+
def klass=(value)
|
238
|
+
@client._recur.call([], ['update', @jid, 'klass', value.to_s])
|
239
|
+
@klass_name = value.to_s
|
240
|
+
end
|
241
|
+
|
242
|
+
def move(queue)
|
243
|
+
@client._recur.call([], ['update', @jid, 'queue', queue])
|
244
|
+
@queue_name = queue
|
245
|
+
end
|
246
|
+
|
247
|
+
def cancel
|
248
|
+
@client._recur.call([], ['off', @jid])
|
249
|
+
end
|
250
|
+
|
251
|
+
def tag(*tags)
|
252
|
+
@client._recur.call([], ['tag', @jid] + tags)
|
253
|
+
end
|
254
|
+
|
255
|
+
def untag(*tags)
|
256
|
+
@client._recur.call([], ['untag', @jid] + tags)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Qless
|
2
|
+
module JobReservers
|
3
|
+
class Ordered
|
4
|
+
attr_reader :queues
|
5
|
+
|
6
|
+
def initialize(queues)
|
7
|
+
@queues = queues
|
8
|
+
end
|
9
|
+
|
10
|
+
def reserve
|
11
|
+
@queues.each do |q|
|
12
|
+
job = q.pop
|
13
|
+
return job if job
|
14
|
+
end
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def description
|
19
|
+
@description ||= @queues.map(&:name).join(', ') + " (ordered)"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Qless
|
2
|
+
module JobReservers
|
3
|
+
class RoundRobin
|
4
|
+
attr_reader :queues
|
5
|
+
|
6
|
+
def initialize(queues)
|
7
|
+
@queues = queues
|
8
|
+
@num_queues = queues.size
|
9
|
+
@last_popped_queue_index = @num_queues - 1
|
10
|
+
end
|
11
|
+
|
12
|
+
def reserve
|
13
|
+
@num_queues.times do |i|
|
14
|
+
if job = next_queue.pop
|
15
|
+
return job
|
16
|
+
end
|
17
|
+
end
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def description
|
22
|
+
@description ||= @queues.map(&:name).join(', ') + " (round robin)"
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def next_queue
|
28
|
+
@last_popped_queue_index = (@last_popped_queue_index + 1) % @num_queues
|
29
|
+
@queues[@last_popped_queue_index]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
data/lib/qless/lua.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Qless
|
2
|
+
class Lua
|
3
|
+
LUA_SCRIPT_DIR = File.expand_path("../qless-core/", __FILE__)
|
4
|
+
|
5
|
+
def initialize(name, redis)
|
6
|
+
@sha = nil
|
7
|
+
@name = name
|
8
|
+
@redis = redis
|
9
|
+
reload()
|
10
|
+
end
|
11
|
+
|
12
|
+
def reload()
|
13
|
+
@sha = @redis.script(:load, File.read(File.join(LUA_SCRIPT_DIR, "#{@name}.lua")))
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(keys, args)
|
17
|
+
begin
|
18
|
+
return @redis.evalsha(@sha, keys.length, *(keys + args))
|
19
|
+
rescue
|
20
|
+
reload
|
21
|
+
return @redis.evalsha(@sha, keys.length, *(keys + args))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
-- Cancel(0, jid)
|
2
|
+
-- --------------
|
3
|
+
-- Cancel a job from taking place. It will be deleted from the system, and any
|
4
|
+
-- attempts to renew a heartbeat will fail, and any attempts to complete it
|
5
|
+
-- will fail. If you try to get the data on the object, you will get nothing.
|
6
|
+
--
|
7
|
+
-- Args:
|
8
|
+
-- 1) jid
|
9
|
+
|
10
|
+
if #KEYS > 0 then error('Cancel(): No Keys should be provided') end
|
11
|
+
|
12
|
+
local jid = assert(ARGV[1], 'Cancel(): Arg "jid" missing.')
|
13
|
+
|
14
|
+
-- Find any stage it's associated with and remove its from that stage
|
15
|
+
local state, queue, failure, worker = unpack(redis.call('hmget', 'ql:j:' .. jid, 'state', 'queue', 'failure', 'worker'))
|
16
|
+
|
17
|
+
if state == 'complete' then
|
18
|
+
return False
|
19
|
+
else
|
20
|
+
-- If this job has dependents, then we should probably fail
|
21
|
+
if redis.call('scard', 'ql:j:' .. jid .. '-dependents') > 0 then
|
22
|
+
error('Cancel(): ' .. jid .. ' has un-canceled jobs that depend on it')
|
23
|
+
end
|
24
|
+
|
25
|
+
-- Remove this job from whatever worker has it, if any
|
26
|
+
if worker then
|
27
|
+
redis.call('zrem', 'ql:w:' .. worker .. ':jobs', jid)
|
28
|
+
end
|
29
|
+
|
30
|
+
-- Remove it from that queue
|
31
|
+
if queue then
|
32
|
+
redis.call('zrem', 'ql:q:' .. queue .. '-work', jid)
|
33
|
+
redis.call('zrem', 'ql:q:' .. queue .. '-locks', jid)
|
34
|
+
redis.call('zrem', 'ql:q:' .. queue .. '-scheduled', jid)
|
35
|
+
redis.call('zrem', 'ql:q:' .. queue .. '-depends', jid)
|
36
|
+
end
|
37
|
+
|
38
|
+
-- We should probably go through all our dependencies and remove ourselves
|
39
|
+
-- from the list of dependents
|
40
|
+
for i, j in ipairs(redis.call('smembers', 'ql:j:' .. jid .. '-dependencies')) do
|
41
|
+
redis.call('srem', 'ql:j:' .. j .. '-dependents', jid)
|
42
|
+
end
|
43
|
+
|
44
|
+
-- Delete any notion of dependencies it has
|
45
|
+
redis.call('del', 'ql:j:' .. jid .. '-dependencies')
|
46
|
+
|
47
|
+
-- If we're in the failed state, remove all of our data
|
48
|
+
if state == 'failed' then
|
49
|
+
failure = cjson.decode(failure)
|
50
|
+
-- We need to make this remove it from the failed queues
|
51
|
+
redis.call('lrem', 'ql:f:' .. failure.group, 0, jid)
|
52
|
+
if redis.call('llen', 'ql:f:' .. failure.group) == 0 then
|
53
|
+
redis.call('srem', 'ql:failures', failure.group)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
-- Remove it as a job that's tagged with this particular tag
|
58
|
+
local tags = cjson.decode(redis.call('hget', 'ql:j:' .. jid, 'tags') or '{}')
|
59
|
+
for i, tag in ipairs(tags) do
|
60
|
+
redis.call('zrem', 'ql:t:' .. tag, jid)
|
61
|
+
redis.call('zincrby', 'ql:tags', -1, tag)
|
62
|
+
end
|
63
|
+
|
64
|
+
-- If the job was being tracked, we should notify
|
65
|
+
if redis.call('zscore', 'ql:tracked', jid) ~= false then
|
66
|
+
redis.call('publish', 'canceled', jid)
|
67
|
+
end
|
68
|
+
|
69
|
+
-- Just go ahead and delete our data
|
70
|
+
redis.call('del', 'ql:j:' .. jid)
|
71
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
-- Complete(0, jid, worker, queue, now, data, [next, q, [(delay, d) | (depends, '["jid1","jid2",...]')])
|
2
|
+
-- -----------------------------------------------------------------------------------------------------
|
3
|
+
-- Complete a job and optionally put it in another queue, either scheduled or to
|
4
|
+
-- be considered waiting immediately. It can also optionally accept other jids
|
5
|
+
-- on which this job will be considered dependent before it's considered valid.
|
6
|
+
--
|
7
|
+
-- Args:
|
8
|
+
-- 1) jid
|
9
|
+
-- 2) worker
|
10
|
+
-- 3) queue
|
11
|
+
-- 4) now
|
12
|
+
-- 5) data
|
13
|
+
-- *) [next, q, [delay, d]], [depends, '...']
|
14
|
+
|
15
|
+
if #KEYS > 0 then error('Complete(): No Keys should be provided') end
|
16
|
+
|
17
|
+
local jid = assert(ARGV[1] , 'Complete(): Arg "jid" missing.')
|
18
|
+
local worker = assert(ARGV[2] , 'Complete(): Arg "worker" missing.')
|
19
|
+
local queue = assert(ARGV[3] , 'Complete(): Arg "queue" missing.')
|
20
|
+
local now = assert(tonumber(ARGV[4]) , 'Complete(): Arg "now" not a number or missing: ' .. tostring(ARGV[4]))
|
21
|
+
local data = assert(cjson.decode(ARGV[5]) , 'Complete(): Arg "data" missing or not JSON: ' .. tostring(ARGV[5]))
|
22
|
+
|
23
|
+
-- Read in all the optional parameters
|
24
|
+
local options = {}
|
25
|
+
for i = 6, #ARGV, 2 do options[ARGV[i]] = ARGV[i + 1] end
|
26
|
+
|
27
|
+
-- Sanity check on optional args
|
28
|
+
local nextq = options['next']
|
29
|
+
local delay = assert(tonumber(options['delay'] or 0))
|
30
|
+
local depends = assert(cjson.decode(options['depends'] or '[]'), 'Complete(): Arg "depends" not JSON: ' .. tostring(options['depends']))
|
31
|
+
|
32
|
+
-- Delay and depends are not allowed together
|
33
|
+
if delay > 0 and #depends > 0 then
|
34
|
+
error('Complete(): "delay" and "depends" are not allowed to be used together')
|
35
|
+
end
|
36
|
+
|
37
|
+
-- Depends doesn't make sense without nextq
|
38
|
+
if options['delay'] and nextq == nil then
|
39
|
+
error('Complete(): "delay" cannot be used without a "next".')
|
40
|
+
end
|
41
|
+
|
42
|
+
-- Depends doesn't make sense without nextq
|
43
|
+
if options['depends'] and nextq == nil then
|
44
|
+
error('Complete(): "depends" cannot be used without a "next".')
|
45
|
+
end
|
46
|
+
|
47
|
+
-- The bin is midnight of the provided day
|
48
|
+
-- 24 * 60 * 60 = 86400
|
49
|
+
local bin = now - (now % 86400)
|
50
|
+
|
51
|
+
-- First things first, we should see if the worker still owns this job
|
52
|
+
local lastworker, history, state, priority, retries = unpack(redis.call('hmget', 'ql:j:' .. jid, 'worker', 'history', 'state', 'priority', 'retries', 'dependents'))
|
53
|
+
|
54
|
+
if (lastworker ~= worker) or (state ~= 'running') then
|
55
|
+
return false
|
56
|
+
end
|
57
|
+
|
58
|
+
-- Now we can assume that the worker does own the job. We need to
|
59
|
+
-- 1) Remove the job from the 'locks' from the old queue
|
60
|
+
-- 2) Enqueue it in the next stage if necessary
|
61
|
+
-- 3) Update the data
|
62
|
+
-- 4) Mark the job as completed, remove the worker, remove expires, and update history
|
63
|
+
|
64
|
+
-- Unpack the history, and update it
|
65
|
+
history = cjson.decode(history)
|
66
|
+
history[#history]['done'] = math.floor(now)
|
67
|
+
|
68
|
+
if data then
|
69
|
+
redis.call('hset', 'ql:j:' .. jid, 'data', cjson.encode(data))
|
70
|
+
end
|
71
|
+
|
72
|
+
-- Remove the job from the previous queue
|
73
|
+
redis.call('zrem', 'ql:q:' .. queue .. '-work', jid)
|
74
|
+
redis.call('zrem', 'ql:q:' .. queue .. '-locks', jid)
|
75
|
+
redis.call('zrem', 'ql:q:' .. queue .. '-scheduled', jid)
|
76
|
+
|
77
|
+
----------------------------------------------------------
|
78
|
+
-- This is the massive stats update that we have to do
|
79
|
+
----------------------------------------------------------
|
80
|
+
-- This is how long we've been waiting to get popped
|
81
|
+
local waiting = math.floor(now) - history[#history]['popped']
|
82
|
+
-- Now we'll go through the apparently long and arduous process of update
|
83
|
+
local count, mean, vk = unpack(redis.call('hmget', 'ql:s:run:' .. bin .. ':' .. queue, 'total', 'mean', 'vk'))
|
84
|
+
count = count or 0
|
85
|
+
if count == 0 then
|
86
|
+
mean = waiting
|
87
|
+
vk = 0
|
88
|
+
count = 1
|
89
|
+
else
|
90
|
+
count = count + 1
|
91
|
+
local oldmean = mean
|
92
|
+
mean = mean + (waiting - mean) / count
|
93
|
+
vk = vk + (waiting - mean) * (waiting - oldmean)
|
94
|
+
end
|
95
|
+
-- Now, update the histogram
|
96
|
+
-- - `s1`, `s2`, ..., -- second-resolution histogram counts
|
97
|
+
-- - `m1`, `m2`, ..., -- minute-resolution
|
98
|
+
-- - `h1`, `h2`, ..., -- hour-resolution
|
99
|
+
-- - `d1`, `d2`, ..., -- day-resolution
|
100
|
+
waiting = math.floor(waiting)
|
101
|
+
if waiting < 60 then -- seconds
|
102
|
+
redis.call('hincrby', 'ql:s:run:' .. bin .. ':' .. queue, 's' .. waiting, 1)
|
103
|
+
elseif waiting < 3600 then -- minutes
|
104
|
+
redis.call('hincrby', 'ql:s:run:' .. bin .. ':' .. queue, 'm' .. math.floor(waiting / 60), 1)
|
105
|
+
elseif waiting < 86400 then -- hours
|
106
|
+
redis.call('hincrby', 'ql:s:run:' .. bin .. ':' .. queue, 'h' .. math.floor(waiting / 3600), 1)
|
107
|
+
else -- days
|
108
|
+
redis.call('hincrby', 'ql:s:run:' .. bin .. ':' .. queue, 'd' .. math.floor(waiting / 86400), 1)
|
109
|
+
end
|
110
|
+
redis.call('hmset', 'ql:s:run:' .. bin .. ':' .. queue, 'total', count, 'mean', mean, 'vk', vk)
|
111
|
+
----------------------------------------------------------
|
112
|
+
|
113
|
+
-- Remove this job from the jobs that the worker that was running it has
|
114
|
+
redis.call('zrem', 'ql:w:' .. worker .. ':jobs', jid)
|
115
|
+
|
116
|
+
if redis.call('zscore', 'ql:tracked', jid) ~= false then
|
117
|
+
redis.call('publish', 'completed', jid)
|
118
|
+
end
|
119
|
+
|
120
|
+
if nextq then
|
121
|
+
-- Enqueue the job
|
122
|
+
table.insert(history, {
|
123
|
+
q = nextq,
|
124
|
+
put = math.floor(now)
|
125
|
+
})
|
126
|
+
|
127
|
+
-- We're going to make sure that this queue is in the
|
128
|
+
-- set of known queues
|
129
|
+
if redis.call('zscore', 'ql:queues', nextq) == false then
|
130
|
+
redis.call('zadd', 'ql:queues', now, nextq)
|
131
|
+
end
|
132
|
+
|
133
|
+
redis.call('hmset', 'ql:j:' .. jid, 'state', 'waiting', 'worker', '', 'failure', '{}',
|
134
|
+
'queue', nextq, 'expires', 0, 'history', cjson.encode(history), 'remaining', tonumber(retries))
|
135
|
+
|
136
|
+
if delay > 0 then
|
137
|
+
redis.call('zadd', 'ql:q:' .. nextq .. '-scheduled', now + delay, jid)
|
138
|
+
return 'scheduled'
|
139
|
+
else
|
140
|
+
-- These are the jids we legitimately have to wait on
|
141
|
+
local count = 0
|
142
|
+
for i, j in ipairs(depends) do
|
143
|
+
-- Make sure it's something other than 'nil' or complete.
|
144
|
+
local state = redis.call('hget', 'ql:j:' .. j, 'state')
|
145
|
+
if (state and state ~= 'complete') then
|
146
|
+
count = count + 1
|
147
|
+
redis.call('sadd', 'ql:j:' .. j .. '-dependents' , jid)
|
148
|
+
redis.call('sadd', 'ql:j:' .. jid .. '-dependencies', j)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
if count > 0 then
|
152
|
+
redis.call('zadd', 'ql:q:' .. nextq .. '-depends', now, jid)
|
153
|
+
redis.call('hset', 'ql:j:' .. jid, 'state', 'depends')
|
154
|
+
return 'depends'
|
155
|
+
else
|
156
|
+
redis.call('zadd', 'ql:q:' .. nextq .. '-work', priority - (now / 10000000000), jid)
|
157
|
+
return 'waiting'
|
158
|
+
end
|
159
|
+
end
|
160
|
+
else
|
161
|
+
redis.call('hmset', 'ql:j:' .. jid, 'state', 'complete', 'worker', '', 'failure', '{}',
|
162
|
+
'queue', '', 'expires', 0, 'history', cjson.encode(history), 'remaining', tonumber(retries))
|
163
|
+
|
164
|
+
-- Do the completion dance
|
165
|
+
local count, time = unpack(redis.call('hmget', 'ql:config', 'jobs-history-count', 'jobs-history'))
|
166
|
+
|
167
|
+
-- These are the default values
|
168
|
+
count = tonumber(count or 50000)
|
169
|
+
time = tonumber(time or 7 * 24 * 60 * 60)
|
170
|
+
|
171
|
+
-- Schedule this job for destructination eventually
|
172
|
+
redis.call('zadd', 'ql:completed', now, jid)
|
173
|
+
|
174
|
+
-- Now look at the expired job data. First, based on the current time
|
175
|
+
local jids = redis.call('zrangebyscore', 'ql:completed', 0, now - time)
|
176
|
+
-- Any jobs that need to be expired... delete
|
177
|
+
for index, jid in ipairs(jids) do
|
178
|
+
local tags = cjson.decode(redis.call('hget', 'ql:j:' .. jid, 'tags') or '{}')
|
179
|
+
for i, tag in ipairs(tags) do
|
180
|
+
redis.call('zrem', 'ql:t:' .. tag, jid)
|
181
|
+
redis.call('zincrby', 'ql:tags', -1, tag)
|
182
|
+
end
|
183
|
+
redis.call('del', 'ql:j:' .. jid)
|
184
|
+
end
|
185
|
+
-- And now remove those from the queued-for-cleanup queue
|
186
|
+
redis.call('zremrangebyscore', 'ql:completed', 0, now - time)
|
187
|
+
|
188
|
+
-- Now take the all by the most recent 'count' ids
|
189
|
+
jids = redis.call('zrange', 'ql:completed', 0, (-1-count))
|
190
|
+
for index, jid in ipairs(jids) do
|
191
|
+
local tags = cjson.decode(redis.call('hget', 'ql:j:' .. jid, 'tags') or '{}')
|
192
|
+
for i, tag in ipairs(tags) do
|
193
|
+
redis.call('zrem', 'ql:t:' .. tag, jid)
|
194
|
+
redis.call('zincrby', 'ql:tags', -1, tag)
|
195
|
+
end
|
196
|
+
redis.call('del', 'ql:j:' .. jid)
|
197
|
+
end
|
198
|
+
redis.call('zremrangebyrank', 'ql:completed', 0, (-1-count))
|
199
|
+
|
200
|
+
-- Alright, if this has any dependents, then we should go ahead
|
201
|
+
-- and unstick those guys.
|
202
|
+
for i, j in ipairs(redis.call('smembers', 'ql:j:' .. jid .. '-dependents')) do
|
203
|
+
redis.call('srem', 'ql:j:' .. j .. '-dependencies', jid)
|
204
|
+
if redis.call('scard', 'ql:j:' .. j .. '-dependencies') == 0 then
|
205
|
+
local q, p = unpack(redis.call('hmget', 'ql:j:' .. j, 'queue', 'priority'))
|
206
|
+
if q then
|
207
|
+
redis.call('zrem', 'ql:q:' .. q .. '-depends', j)
|
208
|
+
redis.call('zadd', 'ql:q:' .. q .. '-work', p, j)
|
209
|
+
redis.call('hset', 'ql:j:' .. j, 'state', 'waiting')
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
-- Delete our dependents key
|
215
|
+
redis.call('del', 'ql:j:' .. jid .. '-dependents')
|
216
|
+
|
217
|
+
return 'complete'
|
218
|
+
end
|