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
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module SolidJobs
|
|
6
|
+
class Counters
|
|
7
|
+
attr_reader :config
|
|
8
|
+
|
|
9
|
+
def initialize(config: SolidJobs.config)
|
|
10
|
+
@config = config
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def completed
|
|
14
|
+
counter(Keyspace::PROCESSED)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def failed
|
|
18
|
+
counter(Keyspace::FAILED)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def ready
|
|
22
|
+
Channel.catalog(config: config).sum(&:size)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def planned
|
|
26
|
+
cardinality(Keyspace::PLANNED)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def retrying
|
|
30
|
+
cardinality(Keyspace::RETRIES)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def discarded
|
|
34
|
+
cardinality(Keyspace::DISCARDED)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def nodes
|
|
38
|
+
Integer(config.redis_pool.call("SCARD", Keyspace::NODES))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def executions
|
|
42
|
+
Nodes.new(config: config).sum(&:active_count)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def reset
|
|
46
|
+
config.redis_pool.call("DEL", Keyspace::PROCESSED, Keyspace::FAILED)
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def counter(key)
|
|
53
|
+
Integer(config.redis_pool.call("GET", key) || 0)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def cardinality(key)
|
|
57
|
+
Integer(config.redis_pool.call("ZCARD", key))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class Channel
|
|
62
|
+
include Enumerable
|
|
63
|
+
|
|
64
|
+
attr_reader :name
|
|
65
|
+
|
|
66
|
+
def self.catalog(config: SolidJobs.config)
|
|
67
|
+
Array(config.redis_pool.call("SMEMBERS", Keyspace::CHANNELS)).sort.map do |name|
|
|
68
|
+
new(name, config: config)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def initialize(name, config: SolidJobs.config)
|
|
73
|
+
@name = String(name)
|
|
74
|
+
@config = config
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def each
|
|
78
|
+
return enum_for(:each) unless block_given?
|
|
79
|
+
|
|
80
|
+
Array(@config.redis_pool.call("LRANGE", storage_key, 0, -1)).reverse_each do |raw|
|
|
81
|
+
yield StoredTask.new(raw, config: @config, owner: self)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def size
|
|
86
|
+
Integer(@config.redis_pool.call("LLEN", storage_key))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def waiting_time
|
|
90
|
+
raw = @config.redis_pool.call("LINDEX", storage_key, -1)
|
|
91
|
+
return 0.0 unless raw
|
|
92
|
+
|
|
93
|
+
envelope = JSON.parse(raw)
|
|
94
|
+
timestamp = envelope["queued_ms"] || envelope["created_ms"]
|
|
95
|
+
timestamp ? [Time.now.to_f - Float(timestamp) / 1_000, 0.0].max : 0.0
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def purge
|
|
99
|
+
@config.redis_pool.pipelined do |pipeline|
|
|
100
|
+
pipeline.call("DEL", storage_key)
|
|
101
|
+
pipeline.call("SREM", Keyspace::CHANNELS, name)
|
|
102
|
+
end
|
|
103
|
+
self
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def paused?
|
|
107
|
+
Integer(@config.redis_pool.call("SISMEMBER", Keyspace::PAUSED_CHANNELS, name)) == 1
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def pause
|
|
111
|
+
@config.redis_pool.call("SADD", Keyspace::PAUSED_CHANNELS, name)
|
|
112
|
+
self
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def resume
|
|
116
|
+
@config.redis_pool.call("SREM", Keyspace::PAUSED_CHANNELS, name)
|
|
117
|
+
self
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def storage_key
|
|
121
|
+
Keyspace.channel(name)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
class StoredTask
|
|
126
|
+
attr_reader :envelope, :score
|
|
127
|
+
|
|
128
|
+
def initialize(raw, score: nil, config: SolidJobs.config, owner: nil)
|
|
129
|
+
@raw = raw
|
|
130
|
+
@envelope = JSON.parse(raw)
|
|
131
|
+
@score = Float(score) if score
|
|
132
|
+
@config = config
|
|
133
|
+
@owner = owner
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def id
|
|
137
|
+
envelope["id"]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def channel
|
|
141
|
+
envelope["channel"]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def task
|
|
145
|
+
envelope["task"]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def arguments
|
|
149
|
+
envelope["arguments"]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def created_at
|
|
153
|
+
millisecond_time(envelope["created_ms"])
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def queued_at
|
|
157
|
+
millisecond_time(envelope["queued_ms"])
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def due_at
|
|
161
|
+
Time.at(score) if score
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def remove
|
|
165
|
+
case @owner
|
|
166
|
+
when TimedTasks
|
|
167
|
+
@config.redis_pool.call("ZREM", @owner.key, @raw)
|
|
168
|
+
when Channel
|
|
169
|
+
@config.redis_pool.call("LREM", @owner.storage_key, 1, @raw)
|
|
170
|
+
else
|
|
171
|
+
false
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def requeue
|
|
176
|
+
remove
|
|
177
|
+
Publisher.new(config: @config).publish(envelope)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
def millisecond_time(value)
|
|
183
|
+
Time.at(Float(value) / 1_000) if value
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
class TimedTasks
|
|
188
|
+
include Enumerable
|
|
189
|
+
|
|
190
|
+
attr_reader :key
|
|
191
|
+
|
|
192
|
+
def initialize(key, config: SolidJobs.config)
|
|
193
|
+
@key = key
|
|
194
|
+
@config = config
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def each
|
|
198
|
+
return enum_for(:each) unless block_given?
|
|
199
|
+
|
|
200
|
+
values = Array(@config.redis_pool.call("ZRANGE", key, 0, -1, "WITHSCORES"))
|
|
201
|
+
values.each_slice(2) do |raw, score|
|
|
202
|
+
yield StoredTask.new(raw, score: score, config: @config, owner: self)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def size
|
|
207
|
+
Integer(@config.redis_pool.call("ZCARD", key))
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def purge
|
|
211
|
+
@config.redis_pool.call("DEL", key)
|
|
212
|
+
self
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def locate(*ids)
|
|
216
|
+
wanted = ids.flatten
|
|
217
|
+
select { |task| wanted.include?(task.id) }
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
class PlannedTasks < TimedTasks
|
|
222
|
+
def initialize(config: SolidJobs.config)
|
|
223
|
+
super(Keyspace::PLANNED, config: config)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
class RetryingTasks < TimedTasks
|
|
228
|
+
def initialize(config: SolidJobs.config)
|
|
229
|
+
super(Keyspace::RETRIES, config: config)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def requeue_all
|
|
233
|
+
each.to_a.each(&:requeue)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
class DiscardedTasks < TimedTasks
|
|
238
|
+
def initialize(config: SolidJobs.config)
|
|
239
|
+
super(Keyspace::DISCARDED, config: config)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def requeue_all
|
|
243
|
+
each.to_a.each(&:requeue)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
class Node
|
|
248
|
+
CONTROL_SIGNALS = {
|
|
249
|
+
pause: "TSTP",
|
|
250
|
+
shutdown: "TERM",
|
|
251
|
+
backtraces: "TTIN",
|
|
252
|
+
}.freeze
|
|
253
|
+
|
|
254
|
+
attr_reader :identity
|
|
255
|
+
|
|
256
|
+
def initialize(identity, config: SolidJobs.config)
|
|
257
|
+
@identity = identity
|
|
258
|
+
@config = config
|
|
259
|
+
fields = Array(@config.redis_pool.call("HGETALL", Keyspace.node(identity)))
|
|
260
|
+
@attributes = fields.each_slice(2).to_h
|
|
261
|
+
@info = JSON.parse(@attributes.fetch("info", "{}"))
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def hostname
|
|
265
|
+
@info["hostname"]
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def pid
|
|
269
|
+
Integer(@info["pid"] || 0)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def channels
|
|
273
|
+
@info["channels"] || []
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def active_count
|
|
277
|
+
Integer(@attributes["busy"] || 0)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def concurrency
|
|
281
|
+
Integer(@attributes["concurrency"] || 0)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def quiet?
|
|
285
|
+
@attributes["quiet"] == "true"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def last_seen_at
|
|
289
|
+
Time.at(Float(@attributes["beat"])) if @attributes["beat"]
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def request_control(action)
|
|
293
|
+
name = CONTROL_SIGNALS.fetch(action)
|
|
294
|
+
@config.redis_pool.call("LPUSH", Keyspace.node_signals(identity), name)
|
|
295
|
+
true
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def executions
|
|
299
|
+
fields = Array(@config.redis_pool.call("HGETALL", Keyspace.node_work(identity)))
|
|
300
|
+
fields.each_slice(2).map do |executor_id, raw|
|
|
301
|
+
Execution.new(identity, executor_id, raw)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
class Nodes
|
|
307
|
+
include Enumerable
|
|
308
|
+
|
|
309
|
+
def initialize(config: SolidJobs.config)
|
|
310
|
+
@config = config
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def each
|
|
314
|
+
return enum_for(:each) unless block_given?
|
|
315
|
+
|
|
316
|
+
Array(@config.redis_pool.call("SMEMBERS", Keyspace::NODES)).each do |identity|
|
|
317
|
+
node = Node.new(identity, config: @config)
|
|
318
|
+
if node.last_seen_at
|
|
319
|
+
yield node
|
|
320
|
+
else
|
|
321
|
+
@config.redis_pool.call("SREM", Keyspace::NODES, identity)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
class Execution
|
|
328
|
+
attr_reader :node_id, :executor_id
|
|
329
|
+
|
|
330
|
+
def initialize(node_id, executor_id, raw)
|
|
331
|
+
@node_id = node_id
|
|
332
|
+
@executor_id = executor_id
|
|
333
|
+
@attributes = JSON.parse(raw)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def channel
|
|
337
|
+
@attributes["channel"]
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def envelope
|
|
341
|
+
@attributes["envelope"]
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def started_at
|
|
345
|
+
Time.at(Float(@attributes["started_at"]))
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def claim_token
|
|
349
|
+
@attributes["claim_token"]
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def attempt
|
|
353
|
+
Integer(@attributes["attempt"] || 1)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
class ClaimInfo
|
|
358
|
+
attr_reader :attributes
|
|
359
|
+
|
|
360
|
+
def initialize(raw)
|
|
361
|
+
@attributes = JSON.parse(raw)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
%w[task_id claim_token node_id executor_id channel].each do |name|
|
|
365
|
+
define_method(name) { attributes[name] }
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def attempt
|
|
369
|
+
Integer(attributes["attempt"])
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def claimed_at
|
|
373
|
+
Time.at(Float(attributes["claimed_at"]))
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
class Claims
|
|
378
|
+
include Enumerable
|
|
379
|
+
|
|
380
|
+
def initialize(config: SolidJobs.config)
|
|
381
|
+
@config = config
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def each
|
|
385
|
+
return enum_for(:each) unless block_given?
|
|
386
|
+
|
|
387
|
+
keys.each do |key|
|
|
388
|
+
Array(@config.redis_pool.call("HVALS", key)).each do |raw|
|
|
389
|
+
yield ClaimInfo.new(raw)
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def find_task(task_id)
|
|
395
|
+
find { |claim| claim.task_id == task_id }
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
private
|
|
399
|
+
|
|
400
|
+
def keys
|
|
401
|
+
cursor = "0"
|
|
402
|
+
found = []
|
|
403
|
+
loop do
|
|
404
|
+
cursor, batch = @config.redis_pool.call(
|
|
405
|
+
"SCAN", cursor, "MATCH", "#{Keyspace::PREFIX}:node:*:claims", "COUNT", 100,
|
|
406
|
+
)
|
|
407
|
+
found.concat(batch)
|
|
408
|
+
break if cursor == "0"
|
|
409
|
+
end
|
|
410
|
+
found
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
class Executions
|
|
415
|
+
include Enumerable
|
|
416
|
+
|
|
417
|
+
def initialize(config: SolidJobs.config)
|
|
418
|
+
@nodes = Nodes.new(config: config)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def each
|
|
422
|
+
return enum_for(:each) unless block_given?
|
|
423
|
+
|
|
424
|
+
@nodes.each { |node| node.executions.each { |execution| yield execution } }
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
end
|