solid-jobs 0.1.3 → 0.4.0
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 +4 -4
- data/CHANGELOG.md +73 -4
- data/README.md +149 -118
- data/docs/reliability.md +73 -91
- data/exe/solid-jobs +2 -3
- data/lib/active_job/queue_adapters/solid_jobs_adapter.rb +7 -8
- data/lib/solid_jobs/{config.rb → blueprint.rb} +51 -51
- data/lib/solid_jobs/catalog.rb +427 -0
- data/lib/solid_jobs/claim.rb +315 -0
- data/lib/solid_jobs/{server.rb → conductor.rb} +10 -10
- data/lib/solid_jobs/{cli.rb → console.rb} +12 -9
- data/lib/solid_jobs/{processor.rb → engine.rb} +33 -33
- data/lib/solid_jobs/errors.rb +1 -1
- data/lib/solid_jobs/executor.rb +34 -0
- data/lib/solid_jobs/failure_policy.rb +98 -0
- data/lib/solid_jobs/heartbeat.rb +15 -13
- data/lib/solid_jobs/integrity_check.rb +39 -36
- data/lib/solid_jobs/interceptor_registry.rb +57 -0
- data/lib/solid_jobs/keyspace.rb +42 -0
- data/lib/solid_jobs/{testing.rb → lab.rb} +19 -19
- data/lib/solid_jobs/publisher.rb +186 -0
- data/lib/solid_jobs/rails_adapter.rb +13 -0
- data/lib/solid_jobs/{rails.rb → railtie.rb} +1 -2
- data/lib/solid_jobs/recovery.rb +8 -7
- data/lib/solid_jobs/task.rb +147 -0
- data/lib/solid_jobs/{scheduler.rb → timer.rb} +8 -8
- data/lib/solid_jobs/utilities.rb +5 -11
- data/lib/solid_jobs/version.rb +1 -1
- data/lib/solid_jobs.rb +28 -27
- metadata +19 -18
- data/lib/solid_jobs/active_job.rb +0 -16
- data/lib/solid_jobs/api.rb +0 -457
- data/lib/solid_jobs/client.rb +0 -202
- data/lib/solid_jobs/fetch.rb +0 -310
- data/lib/solid_jobs/job.rb +0 -156
- data/lib/solid_jobs/middleware/chain.rb +0 -104
- data/lib/solid_jobs/retry_service.rb +0 -100
- data/lib/solid_jobs/worker.rb +0 -35
data/lib/solid_jobs/api.rb
DELETED
|
@@ -1,457 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
|
-
module SolidJobs
|
|
6
|
-
class Stats
|
|
7
|
-
attr_reader :config
|
|
8
|
-
|
|
9
|
-
def initialize(config: SolidJobs.config)
|
|
10
|
-
@config = config
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def processed
|
|
14
|
-
integer("stat:processed")
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def failed
|
|
18
|
-
integer("stat:failed")
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def enqueued
|
|
22
|
-
queues.sum(&:size)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def scheduled_size
|
|
26
|
-
integer_command("ZCARD", "schedule")
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def retry_size
|
|
30
|
-
integer_command("ZCARD", "retry")
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def dead_size
|
|
34
|
-
integer_command("ZCARD", "dead")
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def processes_size
|
|
38
|
-
integer_command("SCARD", "processes")
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def workers_size
|
|
42
|
-
ProcessSet.new(config: config).sum(&:busy)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def reset
|
|
46
|
-
config.redis_pool.call("DEL", "stat:processed", "stat:failed")
|
|
47
|
-
self
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def queues
|
|
53
|
-
Queue.all(config: config)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def integer(key)
|
|
57
|
-
Integer(config.redis_pool.call("GET", key) || 0)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def integer_command(*command)
|
|
61
|
-
Integer(config.redis_pool.call(*command))
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
class Queue
|
|
66
|
-
include Enumerable
|
|
67
|
-
|
|
68
|
-
attr_reader :name
|
|
69
|
-
|
|
70
|
-
def self.all(config: SolidJobs.config)
|
|
71
|
-
Array(config.redis_pool.call("SMEMBERS", "queues")).sort.map do |name|
|
|
72
|
-
new(name, config: config)
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def initialize(name, config: SolidJobs.config)
|
|
77
|
-
@name = String(name)
|
|
78
|
-
@config = config
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def each
|
|
82
|
-
return enum_for(:each) unless block_given?
|
|
83
|
-
|
|
84
|
-
Array(@config.redis_pool.call("LRANGE", key, 0, -1)).reverse_each do |raw|
|
|
85
|
-
yield JobRecord.new(raw, config: @config, container: self)
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def size
|
|
90
|
-
Integer(@config.redis_pool.call("LLEN", key))
|
|
91
|
-
end
|
|
92
|
-
alias_method :length, :size
|
|
93
|
-
|
|
94
|
-
def latency
|
|
95
|
-
raw = @config.redis_pool.call("LINDEX", key, -1)
|
|
96
|
-
return 0.0 unless raw
|
|
97
|
-
|
|
98
|
-
payload = JSON.parse(raw)
|
|
99
|
-
enqueued_at = payload["enqueued_at"] || payload["created_at"]
|
|
100
|
-
return 0.0 unless enqueued_at
|
|
101
|
-
|
|
102
|
-
[Time.now.to_f - Float(enqueued_at) / 1_000, 0.0].max
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def clear
|
|
106
|
-
@config.redis_pool.call("DEL", key)
|
|
107
|
-
self
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def delete
|
|
111
|
-
@config.redis_pool.pipelined do |pipeline|
|
|
112
|
-
pipeline.call("DEL", key)
|
|
113
|
-
pipeline.call("SREM", "queues", name)
|
|
114
|
-
end
|
|
115
|
-
self
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def paused?
|
|
119
|
-
Array(@config.redis_pool.call("SISMEMBER", "paused", name)).first.to_i == 1
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def pause
|
|
123
|
-
@config.redis_pool.call("SADD", "paused", name)
|
|
124
|
-
self
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def unpause
|
|
128
|
-
@config.redis_pool.call("SREM", "paused", name)
|
|
129
|
-
self
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
private
|
|
133
|
-
|
|
134
|
-
def key
|
|
135
|
-
"queue:#{name}"
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
class JobRecord
|
|
140
|
-
attr_reader :item, :score
|
|
141
|
-
|
|
142
|
-
def initialize(raw, score: nil, config: SolidJobs.config, container: nil)
|
|
143
|
-
@raw = raw
|
|
144
|
-
@item = JSON.parse(raw)
|
|
145
|
-
@score = Float(score) if score
|
|
146
|
-
@config = config
|
|
147
|
-
@container = container
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def jid
|
|
151
|
-
item["jid"]
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def queue
|
|
155
|
-
item["queue"]
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def klass
|
|
159
|
-
item["class"]
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def args
|
|
163
|
-
item["args"]
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def created_at
|
|
167
|
-
milliseconds_time(item["created_at"])
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def enqueued_at
|
|
171
|
-
milliseconds_time(item["enqueued_at"])
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def at
|
|
175
|
-
Time.at(score) if score
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def delete
|
|
179
|
-
case @container
|
|
180
|
-
when SortedSet
|
|
181
|
-
@config.redis_pool.call("ZREM", @container.name, @raw)
|
|
182
|
-
when Queue
|
|
183
|
-
@config.redis_pool.call("LREM", "queue:#{@container.name}", 1, @raw)
|
|
184
|
-
else
|
|
185
|
-
false
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def retry
|
|
190
|
-
delete
|
|
191
|
-
Client.new(config: @config).push(item)
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
private
|
|
195
|
-
|
|
196
|
-
def milliseconds_time(value)
|
|
197
|
-
Time.at(Float(value) / 1_000) if value
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
class SortedSet
|
|
202
|
-
include Enumerable
|
|
203
|
-
|
|
204
|
-
attr_reader :name
|
|
205
|
-
|
|
206
|
-
def initialize(name, config: SolidJobs.config)
|
|
207
|
-
@name = name
|
|
208
|
-
@config = config
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def each
|
|
212
|
-
return enum_for(:each) unless block_given?
|
|
213
|
-
|
|
214
|
-
values = Array(@config.redis_pool.call("ZRANGE", name, 0, -1, "WITHSCORES"))
|
|
215
|
-
values.each_slice(2) do |raw, score|
|
|
216
|
-
yield JobRecord.new(raw, score: score, config: @config, container: self)
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def size
|
|
221
|
-
Integer(@config.redis_pool.call("ZCARD", name))
|
|
222
|
-
end
|
|
223
|
-
alias_method :length, :size
|
|
224
|
-
|
|
225
|
-
def clear
|
|
226
|
-
@config.redis_pool.call("DEL", name)
|
|
227
|
-
self
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def fetch(*jids)
|
|
231
|
-
wanted = jids.flatten
|
|
232
|
-
select { |job| wanted.include?(job.jid) }
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
class ScheduledSet < SortedSet
|
|
237
|
-
def initialize(config: SolidJobs.config)
|
|
238
|
-
super("schedule", config: config)
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
class RetrySet < SortedSet
|
|
243
|
-
def initialize(config: SolidJobs.config)
|
|
244
|
-
super("retry", config: config)
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
def retry_all
|
|
248
|
-
each.to_a.each(&:retry)
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
class DeadSet < SortedSet
|
|
253
|
-
def initialize(config: SolidJobs.config)
|
|
254
|
-
super("dead", config: config)
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
def retry_all
|
|
258
|
-
each.to_a.each(&:retry)
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
class Process
|
|
263
|
-
attr_reader :identity
|
|
264
|
-
|
|
265
|
-
def initialize(identity, config: SolidJobs.config)
|
|
266
|
-
@identity = identity
|
|
267
|
-
@config = config
|
|
268
|
-
fields = Array(@config.redis_pool.call("HGETALL", identity))
|
|
269
|
-
@attributes = fields.each_slice(2).to_h
|
|
270
|
-
@info = JSON.parse(@attributes.fetch("info", "{}"))
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
def hostname
|
|
274
|
-
@info["hostname"]
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
def pid
|
|
278
|
-
Integer(@info["pid"] || 0)
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
def queues
|
|
282
|
-
@info["queues"] || []
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
def busy
|
|
286
|
-
Integer(@attributes["busy"] || 0)
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def concurrency
|
|
290
|
-
Integer(@attributes["concurrency"] || 0)
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
def quiet?
|
|
294
|
-
@attributes["quiet"] == "true"
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
def beat
|
|
298
|
-
Time.at(Float(@attributes["beat"])) if @attributes["beat"]
|
|
299
|
-
end
|
|
300
|
-
|
|
301
|
-
def quiet!
|
|
302
|
-
signal("TSTP")
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
def stop!
|
|
306
|
-
signal("TERM")
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
def dump_threads
|
|
310
|
-
signal("TTIN")
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
def workers
|
|
314
|
-
fields = Array(@config.redis_pool.call("HGETALL", "#{identity}:work"))
|
|
315
|
-
fields.each_slice(2).map do |processor_id, raw|
|
|
316
|
-
Work.new(identity, processor_id, raw)
|
|
317
|
-
end
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
private
|
|
321
|
-
|
|
322
|
-
def signal(name)
|
|
323
|
-
@config.redis_pool.call("LPUSH", "#{identity}-signals", name)
|
|
324
|
-
true
|
|
325
|
-
end
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
class ProcessSet
|
|
329
|
-
include Enumerable
|
|
330
|
-
|
|
331
|
-
def initialize(config: SolidJobs.config)
|
|
332
|
-
@config = config
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
def each
|
|
336
|
-
return enum_for(:each) unless block_given?
|
|
337
|
-
|
|
338
|
-
Array(@config.redis_pool.call("SMEMBERS", "processes")).each do |identity|
|
|
339
|
-
process = Process.new(identity, config: @config)
|
|
340
|
-
if process.beat
|
|
341
|
-
yield process
|
|
342
|
-
else
|
|
343
|
-
@config.redis_pool.call("SREM", "processes", identity)
|
|
344
|
-
end
|
|
345
|
-
end
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
def size
|
|
349
|
-
count
|
|
350
|
-
end
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
class Work
|
|
354
|
-
attr_reader :process_id, :processor_id
|
|
355
|
-
|
|
356
|
-
def initialize(process_id, processor_id, raw)
|
|
357
|
-
@process_id = process_id
|
|
358
|
-
@processor_id = processor_id
|
|
359
|
-
@attributes = JSON.parse(raw)
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
def queue
|
|
363
|
-
@attributes["queue"]
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
def payload
|
|
367
|
-
@attributes["payload"]
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
def run_at
|
|
371
|
-
Time.at(Float(@attributes["run_at"]))
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
def reservation_id
|
|
375
|
-
@attributes["reservation_id"]
|
|
376
|
-
end
|
|
377
|
-
|
|
378
|
-
def attempt
|
|
379
|
-
Integer(@attributes["attempt"] || 1)
|
|
380
|
-
end
|
|
381
|
-
end
|
|
382
|
-
|
|
383
|
-
class Reservation
|
|
384
|
-
attr_reader :attributes
|
|
385
|
-
|
|
386
|
-
def initialize(raw)
|
|
387
|
-
@attributes = JSON.parse(raw)
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
%w[job_id reservation_id process_id worker_id queue].each do |name|
|
|
391
|
-
define_method(name) { attributes[name] }
|
|
392
|
-
end
|
|
393
|
-
|
|
394
|
-
def attempt
|
|
395
|
-
Integer(attributes["attempt"])
|
|
396
|
-
end
|
|
397
|
-
|
|
398
|
-
def reserved_at
|
|
399
|
-
Time.at(Float(attributes["reserved_at"]))
|
|
400
|
-
end
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
class ReservationSet
|
|
404
|
-
include Enumerable
|
|
405
|
-
|
|
406
|
-
def initialize(config: SolidJobs.config)
|
|
407
|
-
@config = config
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
def each
|
|
411
|
-
return enum_for(:each) unless block_given?
|
|
412
|
-
|
|
413
|
-
keys.each do |key|
|
|
414
|
-
Array(@config.redis_pool.call("HVALS", key)).each do |raw|
|
|
415
|
-
yield Reservation.new(raw)
|
|
416
|
-
end
|
|
417
|
-
end
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
def find_job(job_id)
|
|
421
|
-
find { |reservation| reservation.job_id == job_id }
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
private
|
|
425
|
-
|
|
426
|
-
def keys
|
|
427
|
-
cursor = "0"
|
|
428
|
-
found = []
|
|
429
|
-
loop do
|
|
430
|
-
cursor, batch = @config.redis_pool.call(
|
|
431
|
-
"SCAN", cursor, "MATCH", "*:reservations", "COUNT", 100,
|
|
432
|
-
)
|
|
433
|
-
found.concat(batch)
|
|
434
|
-
break if cursor == "0"
|
|
435
|
-
end
|
|
436
|
-
found
|
|
437
|
-
end
|
|
438
|
-
end
|
|
439
|
-
|
|
440
|
-
class Workers
|
|
441
|
-
include Enumerable
|
|
442
|
-
|
|
443
|
-
def initialize(config: SolidJobs.config)
|
|
444
|
-
@processes = ProcessSet.new(config: config)
|
|
445
|
-
end
|
|
446
|
-
|
|
447
|
-
def each
|
|
448
|
-
return enum_for(:each) unless block_given?
|
|
449
|
-
|
|
450
|
-
@processes.each { |process| process.workers.each { |work| yield work } }
|
|
451
|
-
end
|
|
452
|
-
|
|
453
|
-
def size
|
|
454
|
-
count
|
|
455
|
-
end
|
|
456
|
-
end
|
|
457
|
-
end
|
data/lib/solid_jobs/client.rb
DELETED
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "securerandom"
|
|
5
|
-
|
|
6
|
-
module SolidJobs
|
|
7
|
-
class Client
|
|
8
|
-
DEFAULT_BATCH_SIZE = 1_000
|
|
9
|
-
SCHEDULED_BATCH_SIZE = 100
|
|
10
|
-
|
|
11
|
-
def self.push(item)
|
|
12
|
-
new.push(item)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.push_bulk(item)
|
|
16
|
-
new.push_bulk(item)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def initialize(config: nil, configuration: nil)
|
|
20
|
-
selected = config || configuration || SolidJobs.config
|
|
21
|
-
@config = selected.is_a?(SolidRedis::Config) ? Config.new(redis: selected) : selected
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def push(item)
|
|
25
|
-
payload = normalize_item(item)
|
|
26
|
-
payload = invoke_middleware(payload)
|
|
27
|
-
return unless payload
|
|
28
|
-
|
|
29
|
-
verify_json!(payload)
|
|
30
|
-
raw_push([payload])
|
|
31
|
-
payload["jid"]
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def push_bulk(item)
|
|
35
|
-
raise InvalidJobError, "Job payload must be a Hash" unless item.is_a?(Hash)
|
|
36
|
-
|
|
37
|
-
source = Utilities.stringify_hash_keys(item)
|
|
38
|
-
args = source.delete("args")
|
|
39
|
-
raise InvalidJobError, "Job 'args' must be an Array or Enumerable" unless args.respond_to?(:each)
|
|
40
|
-
|
|
41
|
-
ats = source.delete("at")
|
|
42
|
-
if ats && !ats.is_a?(Numeric) && !ats.is_a?(Array)
|
|
43
|
-
raise InvalidJobError, "Job 'at' must be a Numeric or an Array"
|
|
44
|
-
end
|
|
45
|
-
if ats.is_a?(Array) && args.respond_to?(:size) && ats.size != args.size
|
|
46
|
-
raise InvalidJobError, "Job 'at' Array must have the same size as 'args'"
|
|
47
|
-
end
|
|
48
|
-
batch_size = Integer(source.delete("batch_size") || (ats ? SCHEDULED_BATCH_SIZE : DEFAULT_BATCH_SIZE))
|
|
49
|
-
raise ArgumentError, "batch_size must be positive" unless batch_size.positive?
|
|
50
|
-
|
|
51
|
-
explicit_jid = source.delete("jid")
|
|
52
|
-
if explicit_jid && args.respond_to?(:size) && args.size > 1
|
|
53
|
-
raise InvalidJobError, "Explicit jid is only supported for one bulk job"
|
|
54
|
-
end
|
|
55
|
-
base = normalize_item(source.merge("args" => []))
|
|
56
|
-
base.delete("jid")
|
|
57
|
-
result = []
|
|
58
|
-
args.each_slice(batch_size).with_index do |batch, batch_index|
|
|
59
|
-
payloads = []
|
|
60
|
-
batch.each_with_index do |job_args, index|
|
|
61
|
-
raise InvalidJobError, "Bulk job arguments must be Arrays" unless job_args.is_a?(Array)
|
|
62
|
-
|
|
63
|
-
payload = base.merge(
|
|
64
|
-
"args" => job_args,
|
|
65
|
-
"jid" => explicit_jid || SecureRandom.hex(12),
|
|
66
|
-
)
|
|
67
|
-
payload["at"] = ats.is_a?(Array) ? ats[batch_index * batch_size + index] : ats if ats
|
|
68
|
-
normalized = invoke_middleware(payload)
|
|
69
|
-
if normalized
|
|
70
|
-
verify_json!(normalized)
|
|
71
|
-
payloads << normalized
|
|
72
|
-
result << normalized["jid"]
|
|
73
|
-
else
|
|
74
|
-
result << nil
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
raw_push(payloads)
|
|
78
|
-
end
|
|
79
|
-
result
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def raw_push(payloads)
|
|
83
|
-
return true if payloads.empty?
|
|
84
|
-
|
|
85
|
-
if defined?(Testing)
|
|
86
|
-
case Testing.mode
|
|
87
|
-
when :fake
|
|
88
|
-
payloads.each do |payload|
|
|
89
|
-
Testing.jobs_for(Utilities.constantize(payload["class"])) << payload
|
|
90
|
-
end
|
|
91
|
-
return true
|
|
92
|
-
when :inline
|
|
93
|
-
payloads.each { |payload| Testing.execute_inline(payload) }
|
|
94
|
-
return true
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
@config.redis_pool.pipelined do |pipeline|
|
|
99
|
-
if payloads.all? { |payload| payload["at"] }
|
|
100
|
-
payloads.each do |payload|
|
|
101
|
-
score = payload.delete("at")
|
|
102
|
-
pipeline.call("ZADD", "schedule", score, JSON.generate(payload))
|
|
103
|
-
end
|
|
104
|
-
elsif payloads.none? { |payload| payload["at"] }
|
|
105
|
-
push_immediate(pipeline, payloads)
|
|
106
|
-
else
|
|
107
|
-
scheduled, immediate = payloads.partition { |payload| payload["at"] }
|
|
108
|
-
scheduled.each do |payload|
|
|
109
|
-
score = payload.delete("at")
|
|
110
|
-
pipeline.call("ZADD", "schedule", score, JSON.generate(payload))
|
|
111
|
-
end
|
|
112
|
-
push_immediate(pipeline, immediate)
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
true
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
private
|
|
119
|
-
|
|
120
|
-
def normalize_item(item)
|
|
121
|
-
raise InvalidJobError, "Job payload must be a Hash" unless item.is_a?(Hash)
|
|
122
|
-
|
|
123
|
-
payload = @config.default_job_options.merge(Utilities.stringify_hash_keys(item))
|
|
124
|
-
payload["class"] ||= payload.delete("job_class")
|
|
125
|
-
payload["jid"] ||= payload.delete("job_id")
|
|
126
|
-
job_class = payload["class"]
|
|
127
|
-
args = payload["args"]
|
|
128
|
-
unless job_class.is_a?(Class) || (job_class.is_a?(String) && !job_class.empty?)
|
|
129
|
-
raise InvalidJobError, "Job must include a non-empty class"
|
|
130
|
-
end
|
|
131
|
-
raise InvalidJobError, "Job args must be an Array" unless args.is_a?(Array)
|
|
132
|
-
|
|
133
|
-
payload["class"] = job_class.name if job_class.is_a?(Class)
|
|
134
|
-
payload["queue"] = String(payload.fetch("queue"))
|
|
135
|
-
payload["jid"] ||= SecureRandom.hex(12)
|
|
136
|
-
payload["created_at"] ||= Utilities.realtime_milliseconds
|
|
137
|
-
payload
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def invoke_middleware(payload)
|
|
141
|
-
job_class = payload["class"]
|
|
142
|
-
@config.client_middleware.invoke(job_class, payload, payload["queue"], @config.redis_pool) do
|
|
143
|
-
payload
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def push_immediate(pipeline, payloads)
|
|
148
|
-
return if payloads.empty?
|
|
149
|
-
|
|
150
|
-
groups = if payloads.all? { |payload| payload["queue"] == payloads.first["queue"] }
|
|
151
|
-
[[payloads.first["queue"], payloads]]
|
|
152
|
-
else
|
|
153
|
-
payloads.group_by { |payload| payload["queue"] }
|
|
154
|
-
end
|
|
155
|
-
groups.each do |queue, jobs|
|
|
156
|
-
pipeline.call("SADD", "queues", queue)
|
|
157
|
-
now = Utilities.realtime_milliseconds
|
|
158
|
-
encoded = jobs.map do |payload|
|
|
159
|
-
payload["enqueued_at"] = now
|
|
160
|
-
JSON.generate(payload)
|
|
161
|
-
end
|
|
162
|
-
pipeline.call("LPUSH", "queue:#{queue}", *encoded)
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def verify_json!(payload)
|
|
167
|
-
invalid = find_invalid_json_value(payload["args"])
|
|
168
|
-
return unless invalid
|
|
169
|
-
|
|
170
|
-
message = "Job arguments must contain only JSON-native values with string hash keys; found #{invalid.inspect}"
|
|
171
|
-
case @config.on_complex_arguments
|
|
172
|
-
when :raise
|
|
173
|
-
raise InvalidArgumentError, message
|
|
174
|
-
when :warn
|
|
175
|
-
@config.logger.warn(message)
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
def find_invalid_json_value(value)
|
|
180
|
-
case value
|
|
181
|
-
when nil, true, false, String, Integer, Float
|
|
182
|
-
nil
|
|
183
|
-
when Array
|
|
184
|
-
value.each do |nested|
|
|
185
|
-
invalid = find_invalid_json_value(nested)
|
|
186
|
-
return invalid if invalid
|
|
187
|
-
end
|
|
188
|
-
nil
|
|
189
|
-
when Hash
|
|
190
|
-
value.each do |key, nested|
|
|
191
|
-
return key unless key.is_a?(String)
|
|
192
|
-
|
|
193
|
-
invalid = find_invalid_json_value(nested)
|
|
194
|
-
return invalid if invalid
|
|
195
|
-
end
|
|
196
|
-
nil
|
|
197
|
-
else
|
|
198
|
-
value
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
end
|