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/fetch.rb
DELETED
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "securerandom"
|
|
5
|
-
require "digest/sha1"
|
|
6
|
-
|
|
7
|
-
module SolidJobs
|
|
8
|
-
class Fetch
|
|
9
|
-
TIMEOUT = 0.25
|
|
10
|
-
RESERVE = <<~LUA.freeze
|
|
11
|
-
local payload = redis.call("rpop", KEYS[1])
|
|
12
|
-
if not payload then
|
|
13
|
-
return nil
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
redis.call("lpush", KEYS[2], payload)
|
|
17
|
-
local job = cjson.decode(payload)
|
|
18
|
-
local attempt = redis.call("hincrby", KEYS[4], job["jid"], 1)
|
|
19
|
-
local metadata = {
|
|
20
|
-
job_id = job["jid"],
|
|
21
|
-
reservation_id = ARGV[1],
|
|
22
|
-
process_id = ARGV[2],
|
|
23
|
-
worker_id = ARGV[3],
|
|
24
|
-
queue = job["queue"],
|
|
25
|
-
reserved_at = tonumber(ARGV[4]),
|
|
26
|
-
attempt = attempt
|
|
27
|
-
}
|
|
28
|
-
redis.call("hset", KEYS[3], ARGV[3], cjson.encode(metadata))
|
|
29
|
-
return {payload, attempt}
|
|
30
|
-
LUA
|
|
31
|
-
REGISTER = <<~LUA.freeze
|
|
32
|
-
local attempt = redis.call("hincrby", KEYS[2], ARGV[1], 1)
|
|
33
|
-
local metadata = cjson.decode(ARGV[3])
|
|
34
|
-
metadata["attempt"] = attempt
|
|
35
|
-
redis.call("hset", KEYS[1], ARGV[2], cjson.encode(metadata))
|
|
36
|
-
return attempt
|
|
37
|
-
LUA
|
|
38
|
-
ACK = <<~LUA.freeze
|
|
39
|
-
local metadata = redis.call("hget", KEYS[2], ARGV[2])
|
|
40
|
-
if not metadata then
|
|
41
|
-
return 0
|
|
42
|
-
end
|
|
43
|
-
if cjson.decode(metadata)["reservation_id"] ~= ARGV[3] then
|
|
44
|
-
return 0
|
|
45
|
-
end
|
|
46
|
-
local removed = redis.call("lrem", KEYS[1], 1, ARGV[1])
|
|
47
|
-
redis.call("hdel", KEYS[2], ARGV[2])
|
|
48
|
-
if removed > 0 then
|
|
49
|
-
redis.call("hdel", KEYS[3], ARGV[4])
|
|
50
|
-
end
|
|
51
|
-
return removed
|
|
52
|
-
LUA
|
|
53
|
-
REQUEUE = <<~LUA.freeze
|
|
54
|
-
local metadata = redis.call("hget", KEYS[2], ARGV[2])
|
|
55
|
-
if not metadata then
|
|
56
|
-
return 0
|
|
57
|
-
end
|
|
58
|
-
if cjson.decode(metadata)["reservation_id"] ~= ARGV[3] then
|
|
59
|
-
return 0
|
|
60
|
-
end
|
|
61
|
-
local removed = redis.call("lrem", KEYS[1], 1, ARGV[1])
|
|
62
|
-
if removed > 0 then
|
|
63
|
-
redis.call("rpush", KEYS[3], ARGV[1])
|
|
64
|
-
end
|
|
65
|
-
redis.call("hdel", KEYS[2], ARGV[2])
|
|
66
|
-
return removed
|
|
67
|
-
LUA
|
|
68
|
-
RESERVE_SHA = Digest::SHA1.hexdigest(RESERVE).freeze
|
|
69
|
-
REGISTER_SHA = Digest::SHA1.hexdigest(REGISTER).freeze
|
|
70
|
-
ACK_SHA = Digest::SHA1.hexdigest(ACK).freeze
|
|
71
|
-
REQUEUE_SHA = Digest::SHA1.hexdigest(REQUEUE).freeze
|
|
72
|
-
|
|
73
|
-
UnitOfWork = Struct.new(
|
|
74
|
-
:queue,
|
|
75
|
-
:payload,
|
|
76
|
-
:job,
|
|
77
|
-
:redis_pool,
|
|
78
|
-
:reserved_key,
|
|
79
|
-
:metadata_key,
|
|
80
|
-
:metadata_field,
|
|
81
|
-
:reservation_id,
|
|
82
|
-
:attempt,
|
|
83
|
-
keyword_init: true,
|
|
84
|
-
) do
|
|
85
|
-
def acknowledge
|
|
86
|
-
if reserved_key
|
|
87
|
-
removed = begin
|
|
88
|
-
redis_pool.call(
|
|
89
|
-
"EVALSHA", Fetch::ACK_SHA, 3,
|
|
90
|
-
reserved_key, metadata_key, "solid-jobs:attempts",
|
|
91
|
-
payload, metadata_field, reservation_id, job.fetch("jid"),
|
|
92
|
-
)
|
|
93
|
-
rescue SolidRedis::CommandError => error
|
|
94
|
-
raise unless error.message.include?("NOSCRIPT")
|
|
95
|
-
|
|
96
|
-
redis_pool.call(
|
|
97
|
-
"EVAL", Fetch::ACK, 3,
|
|
98
|
-
reserved_key, metadata_key, "solid-jobs:attempts",
|
|
99
|
-
payload, metadata_field, reservation_id, job.fetch("jid"),
|
|
100
|
-
)
|
|
101
|
-
end
|
|
102
|
-
return Integer(removed) == 1
|
|
103
|
-
end
|
|
104
|
-
true
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def requeue
|
|
108
|
-
if reserved_key
|
|
109
|
-
destination = "queue:#{queue}"
|
|
110
|
-
removed = begin
|
|
111
|
-
redis_pool.call(
|
|
112
|
-
"EVALSHA", Fetch::REQUEUE_SHA, 3,
|
|
113
|
-
reserved_key, metadata_key, destination,
|
|
114
|
-
payload, metadata_field, reservation_id,
|
|
115
|
-
)
|
|
116
|
-
rescue SolidRedis::CommandError => error
|
|
117
|
-
raise unless error.message.include?("NOSCRIPT")
|
|
118
|
-
|
|
119
|
-
redis_pool.call(
|
|
120
|
-
"EVAL", Fetch::REQUEUE, 3,
|
|
121
|
-
reserved_key, metadata_key, destination,
|
|
122
|
-
payload, metadata_field, reservation_id,
|
|
123
|
-
)
|
|
124
|
-
end
|
|
125
|
-
return Integer(removed) == 1
|
|
126
|
-
else
|
|
127
|
-
redis_pool.call("RPUSH", "queue:#{queue}", payload)
|
|
128
|
-
end
|
|
129
|
-
true
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def initialize(config, identity: nil, processor_id: nil)
|
|
134
|
-
@redis_pool = config.redis_pool
|
|
135
|
-
@queue_mode = config.queue_mode
|
|
136
|
-
@reliable = config.reliable_fetch
|
|
137
|
-
@queues = config.queue_entries.flat_map do |name, weight|
|
|
138
|
-
Array.new(weight, "queue:#{name}")
|
|
139
|
-
end.freeze
|
|
140
|
-
@reserved_key = "#{identity}:reserved:#{processor_id}" if @reliable && identity
|
|
141
|
-
@metadata_key = "#{identity}:reservations" if @reserved_key
|
|
142
|
-
@identity = identity
|
|
143
|
-
@processor_id = processor_id
|
|
144
|
-
@paused = []
|
|
145
|
-
@paused_refresh_at = 0.0
|
|
146
|
-
@queue_index = 0
|
|
147
|
-
@check_reserved = !!@reserved_key
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def retrieve
|
|
151
|
-
if @check_reserved
|
|
152
|
-
@check_reserved = false
|
|
153
|
-
if (payload = @redis_pool.call("LINDEX", @reserved_key, -1))
|
|
154
|
-
return existing_work(payload)
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
refresh_paused
|
|
159
|
-
queues = @queues.reject { |queue| @paused.include?(queue.delete_prefix("queue:")) }
|
|
160
|
-
return sleep(0.05) if queues.empty?
|
|
161
|
-
|
|
162
|
-
if @reserved_key
|
|
163
|
-
reserve_reliable(queues)
|
|
164
|
-
else
|
|
165
|
-
queues = queues.shuffle if @queue_mode == :random
|
|
166
|
-
result = @redis_pool.blocking_call(TIMEOUT, "BRPOP", *queues, TIMEOUT)
|
|
167
|
-
result && work(result.fetch(1), result.fetch(0).delete_prefix("queue:"))
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def connection_failed!
|
|
172
|
-
@check_reserved = !!@reserved_key
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
private
|
|
176
|
-
|
|
177
|
-
def work(payload, queue = nil, reservation_id: nil, attempt: nil)
|
|
178
|
-
job = JSON.parse(payload)
|
|
179
|
-
queue ||= job.fetch("queue")
|
|
180
|
-
reservation_id, attempt = register(job, queue) unless reservation_id
|
|
181
|
-
UnitOfWork.new(
|
|
182
|
-
queue: queue,
|
|
183
|
-
payload: payload,
|
|
184
|
-
job: job,
|
|
185
|
-
redis_pool: @redis_pool,
|
|
186
|
-
reserved_key: @reserved_key,
|
|
187
|
-
metadata_key: @metadata_key,
|
|
188
|
-
metadata_field: @processor_id.to_s,
|
|
189
|
-
reservation_id: reservation_id,
|
|
190
|
-
attempt: attempt,
|
|
191
|
-
)
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def existing_work(payload)
|
|
195
|
-
raw = @redis_pool.call("HGET", @metadata_key, @processor_id)
|
|
196
|
-
if raw
|
|
197
|
-
metadata = JSON.parse(raw)
|
|
198
|
-
job = JSON.parse(payload)
|
|
199
|
-
if metadata["job_id"] == job["jid"]
|
|
200
|
-
return UnitOfWork.new(
|
|
201
|
-
queue: job.fetch("queue"),
|
|
202
|
-
payload: payload,
|
|
203
|
-
job: job,
|
|
204
|
-
redis_pool: @redis_pool,
|
|
205
|
-
reserved_key: @reserved_key,
|
|
206
|
-
metadata_key: @metadata_key,
|
|
207
|
-
metadata_field: @processor_id.to_s,
|
|
208
|
-
reservation_id: metadata.fetch("reservation_id"),
|
|
209
|
-
attempt: Integer(metadata.fetch("attempt")),
|
|
210
|
-
)
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
work(payload)
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def register(job, queue)
|
|
217
|
-
return [nil, nil] unless @reserved_key
|
|
218
|
-
|
|
219
|
-
reservation_id = SecureRandom.hex(12)
|
|
220
|
-
metadata = JSON.generate(
|
|
221
|
-
"job_id" => job["jid"],
|
|
222
|
-
"reservation_id" => reservation_id,
|
|
223
|
-
"process_id" => @identity,
|
|
224
|
-
"worker_id" => @processor_id,
|
|
225
|
-
"queue" => queue,
|
|
226
|
-
"reserved_at" => Time.now.to_f,
|
|
227
|
-
)
|
|
228
|
-
attempt = begin
|
|
229
|
-
@redis_pool.call(
|
|
230
|
-
"EVALSHA", REGISTER_SHA, 2,
|
|
231
|
-
@metadata_key, "solid-jobs:attempts",
|
|
232
|
-
job["jid"], @processor_id, metadata,
|
|
233
|
-
)
|
|
234
|
-
rescue SolidRedis::CommandError => error
|
|
235
|
-
raise unless error.message.include?("NOSCRIPT")
|
|
236
|
-
|
|
237
|
-
@redis_pool.call(
|
|
238
|
-
"EVAL", REGISTER, 2,
|
|
239
|
-
@metadata_key, "solid-jobs:attempts",
|
|
240
|
-
job["jid"], @processor_id, metadata,
|
|
241
|
-
)
|
|
242
|
-
end
|
|
243
|
-
[reservation_id, Integer(attempt)]
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
def next_queue(queues)
|
|
247
|
-
return queues.sample if @queue_mode == :random
|
|
248
|
-
|
|
249
|
-
queue = queues[@queue_index % queues.length]
|
|
250
|
-
@queue_index += 1
|
|
251
|
-
queue
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
def reserve_reliable(queues)
|
|
255
|
-
if @queue_mode == :strict
|
|
256
|
-
queues.each do |source|
|
|
257
|
-
unit = reserve_now(source)
|
|
258
|
-
return unit if unit
|
|
259
|
-
end
|
|
260
|
-
source = queues.first
|
|
261
|
-
else
|
|
262
|
-
source = next_queue(queues)
|
|
263
|
-
unit = reserve_now(source)
|
|
264
|
-
return unit if unit
|
|
265
|
-
end
|
|
266
|
-
payload = @redis_pool.blocking_call(
|
|
267
|
-
TIMEOUT,
|
|
268
|
-
"BLMOVE", source, @reserved_key, "RIGHT", "LEFT", TIMEOUT,
|
|
269
|
-
)
|
|
270
|
-
payload && work(payload, source.delete_prefix("queue:"))
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
def reserve_now(source)
|
|
274
|
-
reservation_id = SecureRandom.hex(12)
|
|
275
|
-
reserved_at = Time.now.to_f
|
|
276
|
-
result = begin
|
|
277
|
-
@redis_pool.call(
|
|
278
|
-
"EVALSHA", RESERVE_SHA, 4,
|
|
279
|
-
source, @reserved_key, @metadata_key, "solid-jobs:attempts",
|
|
280
|
-
reservation_id, @identity, @processor_id, reserved_at,
|
|
281
|
-
)
|
|
282
|
-
rescue SolidRedis::CommandError => error
|
|
283
|
-
raise unless error.message.include?("NOSCRIPT")
|
|
284
|
-
|
|
285
|
-
@redis_pool.call(
|
|
286
|
-
"EVAL", RESERVE, 4,
|
|
287
|
-
source, @reserved_key, @metadata_key, "solid-jobs:attempts",
|
|
288
|
-
reservation_id, @identity, @processor_id, reserved_at,
|
|
289
|
-
)
|
|
290
|
-
end
|
|
291
|
-
return unless result
|
|
292
|
-
|
|
293
|
-
payload, attempt = result
|
|
294
|
-
work(
|
|
295
|
-
payload,
|
|
296
|
-
source.delete_prefix("queue:"),
|
|
297
|
-
reservation_id: reservation_id,
|
|
298
|
-
attempt: Integer(attempt),
|
|
299
|
-
)
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
def refresh_paused
|
|
303
|
-
now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
304
|
-
return if now < @paused_refresh_at
|
|
305
|
-
|
|
306
|
-
@paused = Array(@redis_pool.call("SMEMBERS", "paused"))
|
|
307
|
-
@paused_refresh_at = now + 5
|
|
308
|
-
end
|
|
309
|
-
end
|
|
310
|
-
end
|
data/lib/solid_jobs/job.rb
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SolidJobs
|
|
4
|
-
module Job
|
|
5
|
-
def self.included(base)
|
|
6
|
-
base.extend(ClassMethods)
|
|
7
|
-
base.include(InstanceMethods)
|
|
8
|
-
base.solid_jobs_options
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
module InstanceMethods
|
|
12
|
-
attr_accessor :jid
|
|
13
|
-
attr_writer :_context
|
|
14
|
-
|
|
15
|
-
def _context
|
|
16
|
-
@_context ||= {}
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def logger
|
|
20
|
-
SolidJobs.config.logger
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def interrupted?
|
|
24
|
-
_context[:interrupted]&.call || false
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
module ClassMethods
|
|
29
|
-
def solid_jobs_options(options = nil)
|
|
30
|
-
if options
|
|
31
|
-
merged = get_solid_jobs_options.merge(Utilities.stringify_keys(options))
|
|
32
|
-
@solid_jobs_options = Utilities.shareable_copy(merged)
|
|
33
|
-
else
|
|
34
|
-
@solid_jobs_options ||= Utilities.shareable_copy(SolidJobs.config.default_job_options)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
alias_method :sidekiq_options, :solid_jobs_options
|
|
38
|
-
|
|
39
|
-
def get_solid_jobs_options
|
|
40
|
-
solid_jobs_options
|
|
41
|
-
end
|
|
42
|
-
alias_method :get_sidekiq_options, :get_solid_jobs_options
|
|
43
|
-
|
|
44
|
-
def solid_jobs_retry_in(&block)
|
|
45
|
-
define_singleton_method(:solid_jobs_retry_in_callback, &block) if block
|
|
46
|
-
method(:solid_jobs_retry_in_callback) if respond_to?(:solid_jobs_retry_in_callback)
|
|
47
|
-
end
|
|
48
|
-
alias_method :sidekiq_retry_in, :solid_jobs_retry_in
|
|
49
|
-
|
|
50
|
-
def solid_jobs_retries_exhausted(&block)
|
|
51
|
-
define_singleton_method(:solid_jobs_retries_exhausted_callback, &block) if block
|
|
52
|
-
method(:solid_jobs_retries_exhausted_callback) if respond_to?(:solid_jobs_retries_exhausted_callback)
|
|
53
|
-
end
|
|
54
|
-
alias_method :sidekiq_retries_exhausted, :solid_jobs_retries_exhausted
|
|
55
|
-
|
|
56
|
-
def perform_async(*args)
|
|
57
|
-
client_push("class" => self, "args" => args)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def perform_bulk(args, batch_size: Client::DEFAULT_BATCH_SIZE)
|
|
61
|
-
Client.new.push_bulk(
|
|
62
|
-
get_solid_jobs_options.merge(
|
|
63
|
-
"class" => self,
|
|
64
|
-
"args" => args,
|
|
65
|
-
"batch_size" => batch_size,
|
|
66
|
-
),
|
|
67
|
-
)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def perform_in(interval, *args)
|
|
71
|
-
timestamp = interval.is_a?(Time) ? interval.to_f : Float(interval)
|
|
72
|
-
timestamp += Time.now.to_f if timestamp < 1_000_000_000
|
|
73
|
-
item = {"class" => self, "args" => args}
|
|
74
|
-
item["at"] = timestamp if timestamp > Time.now.to_f
|
|
75
|
-
client_push(item)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def perform_at(timestamp, *args)
|
|
79
|
-
perform_in(timestamp, *args)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def perform_inline(*args)
|
|
83
|
-
payload = get_solid_jobs_options.merge(
|
|
84
|
-
"class" => self,
|
|
85
|
-
"args" => args,
|
|
86
|
-
)
|
|
87
|
-
SolidJobs::Testing.execute_inline(payload)
|
|
88
|
-
end
|
|
89
|
-
alias_method :perform_sync, :perform_inline
|
|
90
|
-
|
|
91
|
-
def set(options)
|
|
92
|
-
Setter.new(self, options)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def client_push(item)
|
|
96
|
-
Client.new.push(get_solid_jobs_options.merge(item))
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def jobs
|
|
100
|
-
Testing.jobs_for(self)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def clear
|
|
104
|
-
jobs.clear
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def perform_one
|
|
108
|
-
payload = jobs.shift
|
|
109
|
-
raise EmptyQueueError, "No jobs for #{name}" unless payload
|
|
110
|
-
|
|
111
|
-
Testing.execute_inline(payload)
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
class Setter
|
|
116
|
-
def initialize(job_class, options)
|
|
117
|
-
@job_class = job_class
|
|
118
|
-
@options = Utilities.stringify_keys(options)
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def perform_async(*args)
|
|
122
|
-
if @options.delete("sync")
|
|
123
|
-
@job_class.perform_inline(*args)
|
|
124
|
-
elsif (wait = @options.delete("wait"))
|
|
125
|
-
perform_in(wait, *args)
|
|
126
|
-
elsif (wait_until = @options.delete("wait_until"))
|
|
127
|
-
perform_at(wait_until, *args)
|
|
128
|
-
else
|
|
129
|
-
push(args)
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def perform_in(interval, *args)
|
|
134
|
-
timestamp = interval.is_a?(Time) ? interval.to_f : Float(interval)
|
|
135
|
-
timestamp += Time.now.to_f if timestamp < 1_000_000_000
|
|
136
|
-
push(args, "at" => timestamp)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def perform_at(timestamp, *args)
|
|
140
|
-
perform_in(timestamp, *args)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
private
|
|
144
|
-
|
|
145
|
-
def push(args, extra = {})
|
|
146
|
-
Client.new.push(@job_class.get_solid_jobs_options.merge(@options).merge(
|
|
147
|
-
extra,
|
|
148
|
-
"class" => @job_class,
|
|
149
|
-
"args" => args,
|
|
150
|
-
))
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
class EmptyQueueError < Error; end
|
|
156
|
-
end
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SolidJobs
|
|
4
|
-
module Middleware
|
|
5
|
-
class Chain
|
|
6
|
-
Entry = Struct.new(:klass, :args, keyword_init: true)
|
|
7
|
-
|
|
8
|
-
include Enumerable
|
|
9
|
-
|
|
10
|
-
def initialize(copy = nil)
|
|
11
|
-
@entries = copy ? copy.map { |entry| Entry.new(klass: entry.klass, args: entry.args.dup) } : []
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def each(&block)
|
|
15
|
-
@entries.each(&block)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def add(klass, *args)
|
|
19
|
-
remove(klass)
|
|
20
|
-
@entries << Entry.new(klass: klass, args: args)
|
|
21
|
-
self
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def prepend(klass, *args)
|
|
25
|
-
remove(klass)
|
|
26
|
-
@entries.unshift(Entry.new(klass: klass, args: args))
|
|
27
|
-
self
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def remove(klass)
|
|
31
|
-
@entries.delete_if { |entry| entry.klass == klass }
|
|
32
|
-
self
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def insert_before(oldklass, newklass, *args)
|
|
36
|
-
insert_relative(oldklass, newklass, args, 0)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def insert_after(oldklass, newklass, *args)
|
|
40
|
-
insert_relative(oldklass, newklass, args, 1)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def exists?(klass)
|
|
44
|
-
@entries.any? { |entry| entry.klass == klass }
|
|
45
|
-
end
|
|
46
|
-
alias_method :include?, :exists?
|
|
47
|
-
|
|
48
|
-
def clear
|
|
49
|
-
@entries.clear
|
|
50
|
-
self
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def empty?
|
|
54
|
-
@entries.empty?
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def retrieve
|
|
58
|
-
@entries.map { |entry| entry.klass.new(*entry.args) }
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def snapshot
|
|
62
|
-
Utilities.shareable_copy(
|
|
63
|
-
@entries.map { |entry| [entry.klass, entry.args] },
|
|
64
|
-
)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def restore(snapshot)
|
|
68
|
-
clear
|
|
69
|
-
snapshot.each { |klass, args| add(klass, *args) }
|
|
70
|
-
self
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def invoke(*arguments, &final)
|
|
74
|
-
return yield if empty?
|
|
75
|
-
|
|
76
|
-
stack = retrieve
|
|
77
|
-
traverse(stack, 0, arguments, &final)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def dup
|
|
81
|
-
self.class.new(@entries)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
private
|
|
85
|
-
|
|
86
|
-
def traverse(stack, index, arguments, &final)
|
|
87
|
-
return final.call if index >= stack.length
|
|
88
|
-
|
|
89
|
-
stack[index].call(*arguments) do
|
|
90
|
-
traverse(stack, index + 1, arguments, &final)
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def insert_relative(oldklass, newklass, args, offset)
|
|
95
|
-
remove(newklass)
|
|
96
|
-
index = @entries.index { |entry| entry.klass == oldklass }
|
|
97
|
-
raise ArgumentError, "#{oldklass} is not in the middleware chain" unless index
|
|
98
|
-
|
|
99
|
-
@entries.insert(index + offset, Entry.new(klass: newklass, args: args))
|
|
100
|
-
self
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
|
-
module SolidJobs
|
|
6
|
-
class RetryService < Service::Base
|
|
7
|
-
DEFAULT_MAX_RETRIES = 25
|
|
8
|
-
|
|
9
|
-
def call
|
|
10
|
-
payload = @payload.dup
|
|
11
|
-
now = Time.now
|
|
12
|
-
now_ms = Utilities.realtime_milliseconds
|
|
13
|
-
retry_count = payload.key?("retry_count") ? Integer(payload["retry_count"]) + 1 : 0
|
|
14
|
-
payload["error_message"] = @error.message.to_s
|
|
15
|
-
payload["error_class"] = @error.class.name
|
|
16
|
-
payload["failed_at"] ||= now_ms
|
|
17
|
-
payload["retried_at"] = now_ms if retry_count.positive?
|
|
18
|
-
payload["retry_count"] = retry_count
|
|
19
|
-
payload["queue"] = payload["retry_queue"] if payload["retry_queue"]
|
|
20
|
-
|
|
21
|
-
setting = payload["retry"]
|
|
22
|
-
if setting && retry_allowed?(payload, setting, retry_count, now)
|
|
23
|
-
decision = retry_decision(payload, retry_count)
|
|
24
|
-
return nil if decision == :discard
|
|
25
|
-
return move_to_dead(payload, now.to_f) if decision == :kill
|
|
26
|
-
|
|
27
|
-
retry_at = now.to_f + (decision || retry_delay(retry_count))
|
|
28
|
-
@config.redis_pool.call("ZADD", "retry", retry_at, JSON.generate(payload))
|
|
29
|
-
append_message("retry")
|
|
30
|
-
retry_at
|
|
31
|
-
else
|
|
32
|
-
invoke_exhausted(payload)
|
|
33
|
-
move_to_dead(payload, now.to_f)
|
|
34
|
-
append_message("dead")
|
|
35
|
-
nil
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
private
|
|
40
|
-
|
|
41
|
-
def retry_allowed?(payload, setting, retry_count, now)
|
|
42
|
-
if payload["retry_for"]
|
|
43
|
-
first_failure = Float(payload["failed_at"]) / 1_000
|
|
44
|
-
now.to_f - first_failure < Float(payload["retry_for"])
|
|
45
|
-
else
|
|
46
|
-
maximum = setting == true ? DEFAULT_MAX_RETRIES : Integer(setting)
|
|
47
|
-
retry_count < maximum
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def retry_delay(retry_count)
|
|
52
|
-
ceiling = [
|
|
53
|
-
Float(@config.retry_base_delay) * (2**retry_count),
|
|
54
|
-
Float(@config.retry_max_delay),
|
|
55
|
-
].min
|
|
56
|
-
(ceiling / 2) + (rand * ceiling / 2)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def retry_decision(payload, retry_count)
|
|
60
|
-
job_class = resolve_job_class(payload)
|
|
61
|
-
return unless job_class.respond_to?(:solid_jobs_retry_in_callback)
|
|
62
|
-
|
|
63
|
-
value = job_class.solid_jobs_retry_in_callback(retry_count, @error, payload)
|
|
64
|
-
return value if value.is_a?(Numeric) || %i[discard kill].include?(value)
|
|
65
|
-
|
|
66
|
-
nil
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def invoke_exhausted(payload)
|
|
70
|
-
job_class = resolve_job_class(payload)
|
|
71
|
-
return unless job_class.respond_to?(:solid_jobs_retries_exhausted_callback)
|
|
72
|
-
|
|
73
|
-
job_class.solid_jobs_retries_exhausted_callback(payload, @error)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def resolve_job_class(payload)
|
|
77
|
-
Utilities.constantize(payload.fetch("class"))
|
|
78
|
-
rescue NameError
|
|
79
|
-
nil
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def move_to_dead(payload, timestamp)
|
|
83
|
-
@config.redis_pool.pipelined do |pipeline|
|
|
84
|
-
pipeline.call("ZADD", "dead", timestamp, JSON.generate(payload))
|
|
85
|
-
pipeline.call(
|
|
86
|
-
"ZREMRANGEBYSCORE",
|
|
87
|
-
"dead",
|
|
88
|
-
"-inf",
|
|
89
|
-
timestamp - @config.dead_timeout,
|
|
90
|
-
)
|
|
91
|
-
pipeline.call(
|
|
92
|
-
"ZREMRANGEBYRANK",
|
|
93
|
-
"dead",
|
|
94
|
-
0,
|
|
95
|
-
-(@config.dead_max_jobs + 1),
|
|
96
|
-
)
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
data/lib/solid_jobs/worker.rb
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
|
-
module SolidJobs
|
|
6
|
-
class Worker
|
|
7
|
-
attr_reader :config
|
|
8
|
-
|
|
9
|
-
def initialize(redis_config:, config: nil)
|
|
10
|
-
@config = config || Config.new(redis: redis_config)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def perform(payload)
|
|
14
|
-
payload = JSON.parse(payload) if payload.is_a?(String)
|
|
15
|
-
raise InvalidJobError, "Job payload must be a Hash" unless payload.is_a?(Hash)
|
|
16
|
-
|
|
17
|
-
class_name = payload["class"] || payload["job_class"]
|
|
18
|
-
raise InvalidJobError, "Job must include a non-empty class" if class_name.nil? || class_name.empty?
|
|
19
|
-
raise InvalidJobError, "Job args must be an Array" unless payload["args"].is_a?(Array)
|
|
20
|
-
raise InvalidJobError, "Job queue must be present" if payload["queue"].nil? || payload["queue"].empty?
|
|
21
|
-
|
|
22
|
-
job_class = Utilities.constantize(class_name)
|
|
23
|
-
job = job_class.new
|
|
24
|
-
job.jid = payload["jid"] if job.respond_to?(:jid=)
|
|
25
|
-
|
|
26
|
-
if config.server_middleware.empty?
|
|
27
|
-
job.perform(*payload.fetch("args"))
|
|
28
|
-
else
|
|
29
|
-
config.server_middleware.invoke(job, payload, payload.fetch("queue")) do
|
|
30
|
-
job.perform(*payload.fetch("args"))
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|