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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +73 -4
  3. data/README.md +149 -118
  4. data/docs/reliability.md +73 -91
  5. data/exe/solid-jobs +2 -3
  6. data/lib/active_job/queue_adapters/solid_jobs_adapter.rb +7 -8
  7. data/lib/solid_jobs/{config.rb → blueprint.rb} +51 -51
  8. data/lib/solid_jobs/catalog.rb +427 -0
  9. data/lib/solid_jobs/claim.rb +315 -0
  10. data/lib/solid_jobs/{server.rb → conductor.rb} +10 -10
  11. data/lib/solid_jobs/{cli.rb → console.rb} +12 -9
  12. data/lib/solid_jobs/{processor.rb → engine.rb} +33 -33
  13. data/lib/solid_jobs/errors.rb +1 -1
  14. data/lib/solid_jobs/executor.rb +34 -0
  15. data/lib/solid_jobs/failure_policy.rb +98 -0
  16. data/lib/solid_jobs/heartbeat.rb +15 -13
  17. data/lib/solid_jobs/integrity_check.rb +39 -36
  18. data/lib/solid_jobs/interceptor_registry.rb +57 -0
  19. data/lib/solid_jobs/keyspace.rb +42 -0
  20. data/lib/solid_jobs/{testing.rb → lab.rb} +19 -19
  21. data/lib/solid_jobs/publisher.rb +186 -0
  22. data/lib/solid_jobs/rails_adapter.rb +13 -0
  23. data/lib/solid_jobs/{rails.rb → railtie.rb} +1 -2
  24. data/lib/solid_jobs/recovery.rb +8 -7
  25. data/lib/solid_jobs/task.rb +147 -0
  26. data/lib/solid_jobs/{scheduler.rb → timer.rb} +8 -8
  27. data/lib/solid_jobs/utilities.rb +5 -11
  28. data/lib/solid_jobs/version.rb +1 -1
  29. data/lib/solid_jobs.rb +28 -27
  30. metadata +19 -18
  31. data/lib/solid_jobs/active_job.rb +0 -16
  32. data/lib/solid_jobs/api.rb +0 -457
  33. data/lib/solid_jobs/client.rb +0 -202
  34. data/lib/solid_jobs/fetch.rb +0 -310
  35. data/lib/solid_jobs/job.rb +0 -156
  36. data/lib/solid_jobs/middleware/chain.rb +0 -104
  37. data/lib/solid_jobs/retry_service.rb +0 -100
  38. 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