acidic_job 0.7.7 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +13 -0
  3. data/.github/workflows/main.yml +36 -12
  4. data/.gitignore +6 -0
  5. data/.rubocop.yml +21 -0
  6. data/.tool-versions +1 -0
  7. data/Gemfile +0 -25
  8. data/Gemfile.lock +176 -74
  9. data/README.md +299 -46
  10. data/UPGRADE_GUIDE.md +81 -0
  11. data/acidic_job.gemspec +15 -2
  12. data/bin/console +5 -2
  13. data/combustion/log/test.log +0 -0
  14. data/gemfiles/rails_6.1_sidekiq_6.4.gemfile +10 -0
  15. data/gemfiles/rails_6.1_sidekiq_6.5.gemfile +10 -0
  16. data/gemfiles/rails_7.0_sidekiq_6.4.gemfile +10 -0
  17. data/gemfiles/rails_7.0_sidekiq_6.5.gemfile +10 -0
  18. data/gemfiles/rails_7.1_sidekiq_6.4.gemfile +10 -0
  19. data/gemfiles/rails_7.1_sidekiq_6.5.gemfile +10 -0
  20. data/lib/acidic_job/active_kiq.rb +114 -0
  21. data/lib/acidic_job/arguments.rb +22 -0
  22. data/lib/acidic_job/base.rb +11 -0
  23. data/lib/acidic_job/errors.rb +11 -17
  24. data/lib/acidic_job/extensions/action_mailer.rb +19 -0
  25. data/lib/acidic_job/extensions/noticed.rb +46 -0
  26. data/lib/acidic_job/{response.rb → finished_point.rb} +5 -7
  27. data/lib/acidic_job/idempotency_key.rb +27 -0
  28. data/lib/acidic_job/logger.rb +31 -0
  29. data/lib/acidic_job/mixin.rb +250 -0
  30. data/lib/acidic_job/perform_wrapper.rb +14 -27
  31. data/lib/acidic_job/processor.rb +95 -0
  32. data/lib/acidic_job/rails.rb +40 -0
  33. data/lib/acidic_job/recovery_point.rb +4 -5
  34. data/lib/acidic_job/run.rb +303 -0
  35. data/lib/acidic_job/serializer.rb +24 -0
  36. data/lib/acidic_job/serializers/exception_serializer.rb +41 -0
  37. data/lib/acidic_job/serializers/finished_point_serializer.rb +24 -0
  38. data/lib/acidic_job/serializers/job_serializer.rb +27 -0
  39. data/lib/acidic_job/serializers/range_serializer.rb +28 -0
  40. data/lib/acidic_job/serializers/recovery_point_serializer.rb +25 -0
  41. data/lib/acidic_job/serializers/worker_serializer.rb +27 -0
  42. data/lib/acidic_job/test_case.rb +9 -0
  43. data/lib/acidic_job/testing.rb +73 -0
  44. data/lib/acidic_job/version.rb +1 -1
  45. data/lib/acidic_job/workflow.rb +70 -0
  46. data/lib/acidic_job/workflow_builder.rb +35 -0
  47. data/lib/acidic_job/workflow_step.rb +103 -0
  48. data/lib/acidic_job.rb +25 -334
  49. data/lib/generators/acidic_job/drop_tables_generator.rb +26 -0
  50. data/lib/generators/acidic_job/install_generator.rb +27 -0
  51. data/lib/generators/acidic_job/templates/create_acidic_job_runs_migration.rb.erb +19 -0
  52. data/lib/generators/{templates/create_acidic_job_keys_migration.rb.erb → acidic_job/templates/drop_acidic_job_keys_migration.rb.erb} +10 -3
  53. metadata +214 -18
  54. data/.ruby_version +0 -1
  55. data/lib/acidic_job/deliver_transactionally_extension.rb +0 -26
  56. data/lib/acidic_job/key.rb +0 -33
  57. data/lib/acidic_job/no_op.rb +0 -11
  58. data/lib/acidic_job/perform_transactionally_extension.rb +0 -33
  59. data/lib/acidic_job/sidekiq_callbacks.rb +0 -45
  60. data/lib/acidic_job/staged.rb +0 -50
  61. data/lib/generators/acidic_job_generator.rb +0 -44
  62. data/lib/generators/templates/create_staged_acidic_jobs_migration.rb.erb +0 -10
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AcidicJob
4
+ class Processor
5
+ def initialize(run, job)
6
+ @run = run
7
+ @job = job
8
+ @workflow = Workflow.new(run, job)
9
+ end
10
+
11
+ def process_run
12
+ # if the run record is already marked as finished, immediately return its result
13
+ return @run.succeeded? if @run.finished?
14
+
15
+ AcidicJob.logger.log_run_event("Processing #{@run.current_step_name}...", @job, @run)
16
+ loop do
17
+ break if @run.finished?
18
+
19
+ if !@run.known_recovery_point?
20
+ raise UnknownRecoveryPoint,
21
+ "Defined workflow does not reference this step: #{@run.current_step_name.inspect}"
22
+ elsif (awaited_jobs = jobs_from(@run.current_step_awaits)).any?
23
+ # We only execute the current step, without progressing to the next step.
24
+ # This ensures that any failures in parallel jobs will have this step retried in the main workflow
25
+ step_result = @workflow.execute_current_step
26
+ # We allow the `#step_done` method to manage progressing the recovery_point to the next step,
27
+ # and then calling `process_run` to restart the main workflow on the next step.
28
+ # We pass the `step_result` so that the async callback called after the step-parallel-jobs complete
29
+ # can move on to the appropriate next stage in the workflow.
30
+ enqueue_awaited_jobs(awaited_jobs, step_result)
31
+ # after processing the current step, break the processing loop
32
+ # and stop this method from blocking in the primary worker
33
+ # as it will continue once the background workers all succeed
34
+ # so we want to keep the primary worker queue free to process new work
35
+ # this CANNOT ever be `break` as that wouldn't exit the parent job,
36
+ # only this step in the workflow, blocking as it awaits the next step
37
+ return true
38
+ else
39
+ @workflow.execute_current_step
40
+ @workflow.progress_to_next_step
41
+ end
42
+ end
43
+ AcidicJob.logger.log_run_event("Processed #{@run.current_step_name}.", @job, @run)
44
+
45
+ @run.succeeded?
46
+ end
47
+
48
+ private
49
+
50
+ def enqueue_awaited_jobs(awaited_jobs, step_result)
51
+ AcidicJob.logger.log_run_event("Enqueuing #{awaited_jobs.count} awaited jobs...", @job, @run)
52
+ # All jobs created in the block are pushed atomically at the end of the block.
53
+ AcidicJob::Run.transaction do
54
+ awaited_jobs.each do |awaited_job|
55
+ worker_class, args = job_and_args(awaited_job)
56
+
57
+ job = worker_class.new(*args)
58
+
59
+ AcidicJob::Run.await!(job, by: @run, return_to: step_result)
60
+ end
61
+ end
62
+ AcidicJob.logger.log_run_event("Enqueued #{awaited_jobs.count} awaited jobs.", @job, @run)
63
+ end
64
+
65
+ def jobs_from(jobs_or_jobs_getter)
66
+ case jobs_or_jobs_getter
67
+ when Array
68
+ jobs_or_jobs_getter.compact
69
+ when Symbol, String
70
+ if @job.respond_to?(jobs_or_jobs_getter, _include_private = true)
71
+ jobs = @job.method(jobs_or_jobs_getter).call
72
+ Array(jobs).compact
73
+ else
74
+ raise UnknownAwaitedJob,
75
+ "Invalid `awaits`; unknown method `#{jobs_or_jobs_getter}` for this job"
76
+ end
77
+ else
78
+ raise UnknownAwaitedJob,
79
+ "Invalid `awaits`; must be either an jobs Array or method name, was: #{jobs_or_jobs_getter.class.name}"
80
+ end
81
+ end
82
+
83
+ def job_and_args(job)
84
+ case job
85
+ when Class
86
+ [job, []]
87
+ else
88
+ [
89
+ job.class,
90
+ job.arguments
91
+ ]
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module AcidicJob
6
+ class Rails < ::Rails::Railtie
7
+ initializer "acidic_job.action_mailer_extension" do
8
+ ::ActiveSupport.on_load(:action_mailer) do
9
+ # Add `deliver_acidicly` to ActionMailer
10
+ ::ActionMailer::Parameterized::MessageDelivery.include(Extensions::ActionMailer)
11
+ ::ActionMailer::MessageDelivery.include(Extensions::ActionMailer)
12
+ end
13
+ end
14
+
15
+ initializer "acidic_job.active_job_serializers" do
16
+ ::ActiveSupport.on_load(:active_job) do
17
+ ::ActiveJob::Serializers.add_serializers(
18
+ Serializers::ExceptionSerializer,
19
+ Serializers::FinishedPointSerializer,
20
+ Serializers::JobSerializer,
21
+ Serializers::RangeSerializer,
22
+ Serializers::RecoveryPointSerializer,
23
+ Serializers::WorkerSerializer
24
+ )
25
+ end
26
+ end
27
+
28
+ generators do
29
+ require "generators/acidic_job/install_generator"
30
+ end
31
+
32
+ # This hook happens after all initializers are run, just before returning
33
+ config.after_initialize do
34
+ if defined?(::Noticed)
35
+ # Add `deliver_acidicly` to Noticed
36
+ ::Noticed::Base.include(Extensions::Noticed)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -4,15 +4,14 @@
4
4
  # return from an #atomic_phase block.
5
5
  module AcidicJob
6
6
  class RecoveryPoint
7
- attr_accessor :name
7
+ attr_reader :name
8
8
 
9
9
  def initialize(name)
10
- self.name = name
10
+ @name = name
11
11
  end
12
12
 
13
- def call(key:)
14
- # Skip AR callbacks as there are none on the model
15
- key.update_column(:recovery_point, name)
13
+ def call(run:)
14
+ run.recover_to!(@name)
16
15
  end
17
16
  end
18
17
  end
@@ -0,0 +1,303 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "global_id"
5
+ require "base64"
6
+ require "active_support/core_ext/object/with_options"
7
+ require "active_support/core_ext/module/concerning"
8
+ require "active_support/concern"
9
+
10
+ module AcidicJob
11
+ class Run < ActiveRecord::Base
12
+ include GlobalID::Identification
13
+
14
+ FINISHED_RECOVERY_POINT = "FINISHED"
15
+ STAGED_JOB_ID_PREFIX = "STG"
16
+ STAGED_JOB_ID_DELIMITER = "__"
17
+ IDEMPOTENCY_KEY_LOCK_TIMEOUT_SECONDS = 2
18
+
19
+ self.table_name = "acidic_job_runs"
20
+
21
+ validates :idempotency_key, presence: true
22
+ validate :not_awaited_but_unstaged
23
+
24
+ def self.clear_finished
25
+ # over-write any pre-existing relation queries on `recovery_point` and/or `error_object`
26
+ to_purge = finished
27
+
28
+ count = to_purge.count
29
+
30
+ return 0 if count.zero?
31
+
32
+ AcidicJob.logger.info("Deleting #{count} finished AcidicJob runs")
33
+ to_purge.delete_all
34
+ end
35
+
36
+ def succeeded?
37
+ finished? && !errored?
38
+ end
39
+
40
+ concerning :Awaitable do
41
+ included do
42
+ belongs_to :awaited_by, class_name: "AcidicJob::Run", optional: true
43
+ has_many :batched_runs, class_name: "AcidicJob::Run", foreign_key: "awaited_by_id"
44
+
45
+ scope :awaited, -> { where.not(awaited_by: nil) }
46
+ scope :unawaited, -> { where(awaited_by: nil) }
47
+
48
+ after_update_commit :proceed_with_parent, if: :finished?
49
+
50
+ serialize :returning_to, AcidicJob::Serializer
51
+ end
52
+
53
+ class_methods do
54
+ def await!(job, by:, return_to:)
55
+ create!(
56
+ staged: true,
57
+ awaited_by: by,
58
+ job_class: job.class.name,
59
+ serialized_job: job.serialize,
60
+ idempotency_key: job.idempotency_key
61
+ )
62
+ by.update(returning_to: return_to)
63
+ end
64
+ end
65
+
66
+ def awaited?
67
+ awaited_by.present?
68
+ end
69
+
70
+ private
71
+
72
+ def proceed_with_parent
73
+ return unless finished?
74
+ return unless awaited_by.present?
75
+ return if awaited_by.batched_runs.outstanding.any?
76
+
77
+ AcidicJob.logger.log_run_event("Proceeding with parent job...", job, self)
78
+ awaited_by.unlock!
79
+ awaited_by.proceed
80
+ AcidicJob.logger.log_run_event("Proceeded with parent job.", job, self)
81
+ end
82
+
83
+ protected
84
+
85
+ def proceed
86
+ # this needs to be explicitly set so that `was_workflow_job?` appropriately returns `true`
87
+ # TODO: replace this with some way to check the type of the job directly
88
+ # either via class method or explicit module inclusion
89
+ job.instance_variable_set(:@acidic_job_run, self)
90
+
91
+ workflow = Workflow.new(self, job, returning_to)
92
+ # TODO: WRITE REGRESSION TESTS FOR PARALLEL JOB FAILING AND RETRYING THE ORIGINAL STEP
93
+ workflow.progress_to_next_step
94
+
95
+ # when a batch of jobs for a step succeeds, we begin processing the `AcidicJob::Run` record again
96
+ return if finished?
97
+
98
+ AcidicJob.logger.log_run_event("Re-enqueuing parent job...", job, self)
99
+ enqueue_job
100
+ AcidicJob.logger.log_run_event("Re-enqueued parent job.", job, self)
101
+ end
102
+ end
103
+
104
+ concerning :Stageable do
105
+ included do
106
+ after_create_commit :enqueue_job, if: :staged?
107
+
108
+ validates :staged, inclusion: { in: [true, false] } # uses database default
109
+
110
+ scope :staged, -> { where(staged: true) }
111
+ scope :unstaged, -> { where(staged: false) }
112
+ end
113
+
114
+ class_methods do
115
+ def stage!(job)
116
+ create!(
117
+ staged: true,
118
+ job_class: job.class.name,
119
+ serialized_job: job.serialize,
120
+ idempotency_key: job.try(:idempotency_key) || job.job_id
121
+ )
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ def job_id
128
+ return idempotency_key unless staged?
129
+
130
+ # encode the identifier for this record in the job ID
131
+ global_id = to_global_id.to_s.remove("gid://")
132
+ # base64 encoding for minimal security
133
+ encoded_global_id = Base64.urlsafe_encode64(global_id, padding: false)
134
+
135
+ [
136
+ STAGED_JOB_ID_PREFIX,
137
+ idempotency_key,
138
+ encoded_global_id
139
+ ].join(STAGED_JOB_ID_DELIMITER)
140
+ end
141
+ end
142
+
143
+ concerning :Workflowable do
144
+ included do
145
+ serialize :workflow, AcidicJob::Serializer
146
+ serialize :error_object, AcidicJob::Serializer
147
+ store :attr_accessors, coder: AcidicJob::Serializer
148
+
149
+ with_options unless: :staged? do
150
+ validates :last_run_at, presence: true
151
+ validates :recovery_point, presence: true
152
+ validates :workflow, presence: true
153
+ end
154
+ end
155
+
156
+ def workflow?
157
+ self[:workflow].present?
158
+ end
159
+
160
+ def attr_accessors
161
+ self[:attr_accessors] || {}
162
+ end
163
+
164
+ def current_step_name
165
+ recovery_point
166
+ end
167
+
168
+ def current_step_hash
169
+ workflow[current_step_name]
170
+ end
171
+
172
+ def next_step_name
173
+ current_step_hash.fetch("then")
174
+ end
175
+
176
+ def current_step_awaits
177
+ current_step_hash["awaits"]
178
+ end
179
+
180
+ def next_step_finishes?
181
+ next_step_name.to_s == FINISHED_RECOVERY_POINT
182
+ end
183
+
184
+ def current_step_finished?
185
+ current_step_name.to_s == FINISHED_RECOVERY_POINT
186
+ end
187
+ end
188
+
189
+ concerning :Jobbable do
190
+ included do
191
+ serialize :serialized_job, JSON
192
+
193
+ validates :serialized_job, presence: true
194
+ validates :job_class, presence: true
195
+ end
196
+
197
+ def job
198
+ return @job if defined? @job
199
+
200
+ serialized_job_for_run = serialized_job.merge("job_id" => job_id)
201
+ job_class_for_run = job_class.constantize
202
+
203
+ @job = job_class_for_run.deserialize(serialized_job_for_run)
204
+ end
205
+
206
+ def enqueue_job
207
+ job.enqueue
208
+
209
+ # NOTE: record will be deleted after the job has successfully been performed
210
+ true
211
+ end
212
+ end
213
+
214
+ concerning :Finishable do
215
+ included do
216
+ scope :finished, -> { where(recovery_point: FINISHED_RECOVERY_POINT) }
217
+ scope :outstanding, lambda {
218
+ where.not(recovery_point: FINISHED_RECOVERY_POINT).or(where(recovery_point: [nil, ""]))
219
+ }
220
+ end
221
+
222
+ def finish!
223
+ finish and unlock and save!
224
+ end
225
+
226
+ def finish
227
+ self.recovery_point = FINISHED_RECOVERY_POINT
228
+ self
229
+ end
230
+
231
+ def finished?
232
+ recovery_point.to_s == FINISHED_RECOVERY_POINT
233
+ end
234
+ end
235
+
236
+ concerning :Unlockable do
237
+ included do
238
+ scope :unlocked, -> { where(locked_at: nil) }
239
+ scope :locked, -> { where.not(locked_at: nil) }
240
+ end
241
+
242
+ def unlock!
243
+ unlock and save!
244
+ end
245
+
246
+ def unlock
247
+ self.locked_at = nil
248
+ self
249
+ end
250
+
251
+ def locked?
252
+ locked_at.present?
253
+ end
254
+
255
+ def lock_active?
256
+ return false if locked_at.nil?
257
+
258
+ locked_at > Time.current - IDEMPOTENCY_KEY_LOCK_TIMEOUT_SECONDS
259
+ end
260
+ end
261
+
262
+ concerning :ErrorStoreable do
263
+ included do
264
+ scope :unerrored, -> { where(error_object: nil) }
265
+ scope :errored, -> { where.not(error_object: nil) }
266
+ end
267
+
268
+ def store_error!(error)
269
+ reload and unlock and store_error(error) and save!
270
+ end
271
+
272
+ def store_error(error)
273
+ self.error_object = error
274
+ self
275
+ end
276
+
277
+ def errored?
278
+ error_object.present?
279
+ end
280
+ end
281
+
282
+ concerning :Recoverable do
283
+ def recover_to!(point)
284
+ recover_to(point) and save!
285
+ end
286
+
287
+ def recover_to(point)
288
+ self.recovery_point = point
289
+ self
290
+ end
291
+
292
+ def known_recovery_point?
293
+ workflow.key?(recovery_point)
294
+ end
295
+ end
296
+
297
+ def not_awaited_but_unstaged
298
+ return true unless awaited? && !staged?
299
+
300
+ errors.add(:base, "cannot be awaited by another job but not staged")
301
+ end
302
+ end
303
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module AcidicJob
6
+ class Serializer
7
+ # Used for `serialize` method in ActiveRecord
8
+ class << self
9
+ def load(json)
10
+ return if json.nil? || json.empty?
11
+
12
+ data = JSON.parse(json)
13
+ Arguments.send :deserialize_argument, data
14
+ end
15
+
16
+ def dump(obj)
17
+ data = Arguments.send :serialize_argument, obj
18
+ data.to_json
19
+ rescue ActiveJob::SerializationError
20
+ raise UnserializableValue
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job/serializers/object_serializer"
4
+
5
+ module AcidicJob
6
+ module Serializers
7
+ class ExceptionSerializer < ::ActiveJob::Serializers::ObjectSerializer
8
+ def serialize(exception)
9
+ hash = {
10
+ "class" => exception.class.name,
11
+ "message" => exception.message,
12
+ "cause" => exception.cause,
13
+ "backtrace" => {}
14
+ }
15
+
16
+ exception.backtrace.map do |trace|
17
+ path, _, location = trace.rpartition("/")
18
+
19
+ next if hash["backtrace"].key?(path)
20
+
21
+ hash["backtrace"][path] = location
22
+ end
23
+
24
+ super(hash)
25
+ end
26
+
27
+ def deserialize(hash)
28
+ exception_class = hash["class"].constantize
29
+ exception = exception_class.new(hash["message"])
30
+ exception.set_backtrace(hash["backtrace"].map do |path, location|
31
+ [path, location].join("/")
32
+ end)
33
+ exception
34
+ end
35
+
36
+ def serialize?(argument)
37
+ defined?(Exception) && argument.is_a?(Exception)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job/serializers/object_serializer"
4
+
5
+ module AcidicJob
6
+ module Serializers
7
+ class FinishedPointSerializer < ::ActiveJob::Serializers::ObjectSerializer
8
+ def serialize(finished_point)
9
+ super(
10
+ "class" => finished_point.class.name
11
+ )
12
+ end
13
+
14
+ def deserialize(hash)
15
+ finished_point_class = hash["class"].constantize
16
+ finished_point_class.new
17
+ end
18
+
19
+ def serialize?(argument)
20
+ defined?(::AcidicJob::FinishedPoint) && argument.is_a?(::AcidicJob::FinishedPoint)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job/serializers/object_serializer"
4
+
5
+ module AcidicJob
6
+ module Serializers
7
+ class JobSerializer < ::ActiveJob::Serializers::ObjectSerializer
8
+ def serialize(job)
9
+ # don't serialize the `enqueued_at` value, as ActiveRecord will check if the Run record has changed
10
+ # by comparing the deserialized database value with a temporary in-memory generated value.
11
+ # That temporary in-memory generated value can sometimes have an `enqueued_at` value that is 1 second off
12
+ # from the original. In this case, ActiveRecord will think the record has unsaved changes and block the lock.
13
+ super(job.as_json.merge("job_class" => job.class.name))
14
+ end
15
+
16
+ def deserialize(hash)
17
+ job = ::ActiveJob::Base.deserialize(hash)
18
+ job.send(:deserialize_arguments_if_needed)
19
+ job
20
+ end
21
+
22
+ def serialize?(argument)
23
+ defined?(::ActiveJob::Base) && argument.class < ::ActiveJob::Base
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job/serializers/object_serializer"
4
+
5
+ # :nocov:
6
+ module AcidicJob
7
+ module Serializers
8
+ class RangeSerializer < ::ActiveJob::Serializers::ObjectSerializer
9
+ KEYS = %w[begin end exclude_end].freeze
10
+
11
+ def serialize(range)
12
+ args = Arguments.serialize([range.begin, range.end, range.exclude_end?])
13
+ super(KEYS.zip(args).to_h)
14
+ end
15
+
16
+ def deserialize(hash)
17
+ klass.new(*Arguments.deserialize(hash.values_at(*KEYS)))
18
+ end
19
+
20
+ private
21
+
22
+ def klass
23
+ ::Range
24
+ end
25
+ end
26
+ end
27
+ end
28
+ # :nocov:
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job/serializers/object_serializer"
4
+
5
+ module AcidicJob
6
+ module Serializers
7
+ class RecoveryPointSerializer < ::ActiveJob::Serializers::ObjectSerializer
8
+ def serialize(recovery_point)
9
+ super(
10
+ "class" => recovery_point.class.name,
11
+ "name" => recovery_point.name
12
+ )
13
+ end
14
+
15
+ def deserialize(hash)
16
+ recovery_point_class = hash["class"].constantize
17
+ recovery_point_class.new(hash["name"])
18
+ end
19
+
20
+ def serialize?(argument)
21
+ defined?(::AcidicJob::RecoveryPoint) && argument.is_a?(::AcidicJob::RecoveryPoint)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job/serializers/object_serializer"
4
+
5
+ # :nocov:
6
+ module AcidicJob
7
+ module Serializers
8
+ class WorkerSerializer < ::ActiveJob::Serializers::ObjectSerializer
9
+ def serialize(worker)
10
+ super(
11
+ "job_class" => worker.class.name,
12
+ "arguments" => worker.arguments,
13
+ )
14
+ end
15
+
16
+ def deserialize(hash)
17
+ worker_class = hash["job_class"].constantize
18
+ worker_class.new(*hash["arguments"])
19
+ end
20
+
21
+ def serialize?(argument)
22
+ defined?(::Sidekiq) && argument.class.include?(::Sidekiq::Worker)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ # :nocov:
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./testing"
4
+
5
+ module AcidicJob
6
+ class TestCase < ::ActiveJob::TestCase
7
+ include ::AcidicJob::Testing
8
+ end
9
+ end