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
@@ -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
@@ -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
@@ -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