acidic_job 0.9.0 → 1.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +4 -5
  3. data/.gitignore +1 -1
  4. data/.rubocop.yml +0 -10
  5. data/Gemfile.lock +117 -112
  6. data/README.md +123 -140
  7. data/acidic_job.gemspec +2 -0
  8. data/bin/sandbox +1958 -0
  9. data/gemfiles/{rails_6.1_sidekiq_6.4.gemfile → rails_6.1.gemfile} +0 -2
  10. data/gemfiles/{rails_7.0_sidekiq_6.4.gemfile → rails_7.0.gemfile} +0 -2
  11. data/lib/acidic_job/active_kiq.rb +15 -44
  12. data/lib/acidic_job/arguments.rb +0 -8
  13. data/lib/acidic_job/errors.rb +0 -1
  14. data/lib/acidic_job/mixin.rb +19 -30
  15. data/lib/acidic_job/perform_wrapper.rb +5 -5
  16. data/lib/acidic_job/processor.rb +9 -8
  17. data/lib/acidic_job/run.rb +6 -27
  18. data/lib/acidic_job/serializer.rb +2 -2
  19. data/lib/acidic_job/serializers/exception_serializer.rb +18 -23
  20. data/lib/acidic_job/serializers/job_serializer.rb +14 -6
  21. data/lib/acidic_job/serializers/worker_serializer.rb +6 -4
  22. data/lib/acidic_job/version.rb +1 -1
  23. data/lib/acidic_job/workflow.rb +8 -0
  24. data/lib/acidic_job.rb +10 -10
  25. metadata +35 -23
  26. data/.github/FUNDING.yml +0 -13
  27. data/gemfiles/rails_6.1_sidekiq_6.5.gemfile +0 -10
  28. data/gemfiles/rails_6.1_sidekiq_7.0.gemfile +0 -10
  29. data/gemfiles/rails_7.0_sidekiq_6.5.gemfile +0 -10
  30. data/gemfiles/rails_7.0_sidekiq_7.0.gemfile +0 -10
  31. data/gemfiles/rails_7.1_sidekiq_6.4.gemfile +0 -10
  32. data/gemfiles/rails_7.1_sidekiq_6.5.gemfile +0 -10
  33. data/gemfiles/rails_7.1_sidekiq_7.0.gemfile +0 -10
  34. data/lib/acidic_job/configured_job.rb +0 -11
  35. data/lib/acidic_job/extensions/action_mailer.rb +0 -19
  36. data/lib/acidic_job/extensions/noticed.rb +0 -46
  37. data/lib/acidic_job/perform_acidicly.rb +0 -23
  38. data/lib/acidic_job/railtie.rb +0 -44
  39. data/lib/acidic_job/serializers/active_kiq_serializer.rb +0 -25
  40. data/lib/acidic_job/serializers/new_record_serializer.rb +0 -25
  41. data/lib/acidic_job/test_case.rb +0 -9
  42. data/lib/acidic_job/testing.rb +0 -73
data/bin/sandbox ADDED
@@ -0,0 +1,1958 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/inline"
5
+
6
+ gemfile(true) do
7
+ source "https://rubygems.org"
8
+
9
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
10
+
11
+ # Activate the gem you are reporting the issue against.
12
+ gem "activejob"
13
+ gem "activerecord"
14
+ gem "activesupport"
15
+ gem "sqlite3"
16
+ gem "combustion"
17
+ gem "database_cleaner"
18
+ gem "globalid"
19
+ end
20
+
21
+ # You can add fixtures and/or initialization code here to make experimenting
22
+ # with your gem easier. You can also use a different console, if you like.
23
+
24
+ require "active_record"
25
+ require "sqlite3"
26
+ require "global_id"
27
+ require "active_job"
28
+ require "active_support/concern"
29
+ require "active_support/tagged_logging"
30
+ require "logger"
31
+
32
+ ActiveRecord::Base.establish_connection(
33
+ adapter: "sqlite3",
34
+ database: "database.sqlite",
35
+ flags: SQLite3::Constants::Open::READWRITE |
36
+ SQLite3::Constants::Open::CREATE |
37
+ SQLite3::Constants::Open::SHAREDCACHE
38
+ )
39
+
40
+ GlobalID.app = :test
41
+
42
+ ActiveRecord::Schema.define do
43
+ create_table :acidic_job_runs, force: true do |t|
44
+ t.boolean :staged, null: false, default: false
45
+ t.string :idempotency_key, null: false, index: { unique: true }
46
+ t.text :serialized_job, null: false
47
+ t.string :job_class, null: false
48
+ t.references :awaited_by, null: true, index: true
49
+ t.text :returning_to, null: true
50
+ t.datetime :last_run_at, null: true, default: -> { "CURRENT_TIMESTAMP" }
51
+ t.datetime :locked_at, null: true
52
+ t.string :recovery_point, null: true
53
+ t.text :error_object, null: true
54
+ t.text :attr_accessors, null: true
55
+ t.text :workflow, null: true
56
+ t.timestamps
57
+ end
58
+ end
59
+
60
+ module AcidicJob
61
+ class Error < StandardError; end
62
+ class MissingWorkflowBlock < Error; end
63
+ class UnknownRecoveryPoint < Error; end
64
+ class NoDefinedSteps < Error; end
65
+ class RedefiningWorkflow < Error; end
66
+ class UndefinedStepMethod < Error; end
67
+ class UnknownForEachCollection < Error; end
68
+ class UniterableForEachCollection < Error; end
69
+ class UnknownJobAdapter < Error; end
70
+
71
+ class Logger < ::Logger
72
+ def log_run_event(msg, job, run = nil)
73
+ tags = [
74
+ run&.idempotency_key,
75
+ inspect_name(job)
76
+ ].compact
77
+
78
+ tagged(*tags) { debug(msg) }
79
+ end
80
+
81
+ def inspect_name(obj)
82
+ obj.inspect.split.first.remove("#<")
83
+ end
84
+ end
85
+
86
+ def self.logger
87
+ @logger ||= ActiveSupport::TaggedLogging.new(AcidicJob::Logger.new($stdout, level: :debug))
88
+ end
89
+
90
+ class Run < ActiveRecord::Base
91
+ include GlobalID::Identification
92
+
93
+ FINISHED_RECOVERY_POINT = "FINISHED"
94
+
95
+ self.table_name = "acidic_job_runs"
96
+
97
+ belongs_to :awaited_by, class_name: "AcidicJob::Run", optional: true
98
+ has_many :batched_runs, class_name: "AcidicJob::Run", foreign_key: "awaited_by_id"
99
+
100
+ after_create_commit :enqueue_job, if: :staged?
101
+
102
+ serialize :serialized_job
103
+ serialize :workflow
104
+ serialize :returning_to
105
+ serialize :error_object
106
+ store :attr_accessors
107
+
108
+ validate :foo
109
+
110
+ def foo
111
+ return true unless awaited? && !staged?
112
+
113
+ errors.add(:base, "cannot be awaited by another job but not staged")
114
+ end
115
+
116
+ def job
117
+ serialized_job_for_run = serialized_job.merge("job_id" => job_id)
118
+ job_class_for_run = job_class.constantize
119
+ job_class_for_run.deserialize(serialized_job_for_run)
120
+ end
121
+
122
+ def job_id
123
+ return idempotency_key unless staged?
124
+
125
+ # encode the identifier for this record in the job ID
126
+ global_id = to_global_id.to_s.remove("gid://")
127
+ # base64 encoding for minimal security
128
+ encoded_global_id = Base64.encode64(global_id).strip
129
+ "STG__#{idempotency_key}__#{encoded_global_id}"
130
+ end
131
+
132
+ def awaited?
133
+ awaited_by.present?
134
+ end
135
+
136
+ def workflow?
137
+ workflow.present?
138
+ end
139
+
140
+ def succeeded?
141
+ finished? && !failed?
142
+ end
143
+
144
+ def finished?
145
+ recovery_point == FINISHED_RECOVERY_POINT
146
+ end
147
+
148
+ def failed?
149
+ error_object.present?
150
+ end
151
+
152
+ def known_recovery_point?
153
+ workflow.key?(recovery_point)
154
+ end
155
+
156
+ def attr_accessors
157
+ self[:attr_accessors] || {}
158
+ end
159
+
160
+ def enqueue_job
161
+ job.enqueue wait: 1.seconds
162
+
163
+ # NOTE: record will be deleted after the job has successfully been performed
164
+ true
165
+ end
166
+ end
167
+
168
+ class WorkflowBuilder
169
+ def initialize
170
+ @__acidic_job_steps = []
171
+ end
172
+
173
+ def step(method_name, awaits: [], for_each: nil)
174
+ @__acidic_job_steps << {
175
+ "does" => method_name.to_s,
176
+ "awaits" => awaits,
177
+ "for_each" => for_each
178
+ }
179
+
180
+ @__acidic_job_steps
181
+ end
182
+
183
+ def steps
184
+ @__acidic_job_steps
185
+ end
186
+
187
+ def self.define_workflow(steps)
188
+ # [ { does: "step 1", awaits: [] }, { does: "step 2", awaits: [] }, ... ]
189
+ steps << { "does" => Run::FINISHED_RECOVERY_POINT.to_s }
190
+
191
+ {}.tap do |workflow|
192
+ steps.each_cons(2).map do |enter_step, exit_step|
193
+ enter_name = enter_step["does"]
194
+ workflow[enter_name] = enter_step.merge("then" => exit_step["does"])
195
+ end
196
+ end
197
+ # { "step 1": { does: "step 1", awaits: [], then: "step 2" }, ... }
198
+ end
199
+ end
200
+
201
+ class FinishedPoint
202
+ def call(run:)
203
+ # Skip AR callbacks as there are none on the model
204
+ run.update_columns(
205
+ locked_at: nil,
206
+ recovery_point: Run::FINISHED_RECOVERY_POINT
207
+ )
208
+ end
209
+ end
210
+
211
+ class RecoveryPoint
212
+ def initialize(name)
213
+ @name = name
214
+ end
215
+
216
+ def call(run:)
217
+ # Skip AR callbacks as there are none on the model
218
+ run.update_column(:recovery_point, @name)
219
+ end
220
+ end
221
+
222
+ class Workflow
223
+ # { "step 1": { does: "step 1", awaits: [], then: "step 2" }, ... }
224
+ def initialize(run, job, step_result = nil)
225
+ @run = run
226
+ @job = job
227
+ @step_result = step_result
228
+ @workflow_hash = @run.workflow
229
+ end
230
+
231
+ def execute_current_step
232
+ rescued_error = false
233
+
234
+ begin
235
+ run_current_step
236
+ rescue StandardError => e
237
+ rescued_error = e
238
+ raise e
239
+ ensure
240
+ if rescued_error
241
+ begin
242
+ @run.update_columns(locked_at: nil, error_object: rescued_error)
243
+ rescue StandardError => e
244
+ # We're already inside an error condition, so swallow any additional
245
+ # errors from here and just send them to logs.
246
+ AcidicJob.logger.error("Failed to unlock AcidicJob::Run #{@run.id} because of #{e}.")
247
+ end
248
+ end
249
+ end
250
+
251
+ # be sure to return the `step_result` from running the (wrapped) current step method
252
+ @step_result
253
+ end
254
+
255
+ def progress_to_next_step
256
+ return run_step_result unless next_step_finishes?
257
+
258
+ @job.run_callbacks :finish do
259
+ run_step_result
260
+ end
261
+ end
262
+
263
+ def current_step_name
264
+ @run.recovery_point
265
+ end
266
+
267
+ def current_step_hash
268
+ @workflow_hash[current_step_name]
269
+ end
270
+
271
+ private
272
+
273
+ def run_current_step
274
+ wrapped_method = wrapped_current_step_method
275
+
276
+ AcidicJob.logger.log_run_event("Executing #{current_step_name}...", @job, @run)
277
+ @run.with_lock do
278
+ @step_result = wrapped_method.call(@run)
279
+ end
280
+ AcidicJob.logger.log_run_event("Executed #{current_step_name}.", @job, @run)
281
+ end
282
+
283
+ def run_step_result
284
+ next_step = next_step_name
285
+ AcidicJob.logger.log_run_event("Progressing to #{next_step}...", @job, @run)
286
+ @run.with_lock do
287
+ @step_result.call(run: @run)
288
+ end
289
+ AcidicJob.logger.log_run_event("Progressed to #{next_step}.", @job, @run)
290
+ end
291
+
292
+ def next_step_name
293
+ current_step_hash["then"]
294
+ end
295
+
296
+ def next_step_finishes?
297
+ next_step_name.to_s == Run::FINISHED_RECOVERY_POINT.to_s
298
+ end
299
+
300
+ def wrapped_current_step_method
301
+ # return a callable Proc with a consistent interface for the execution phase
302
+ proc do |_run|
303
+ callable = current_step_method
304
+
305
+ # STEP ITERATION
306
+ # the `iterable_key` represents the name of the collection accessor
307
+ # that must be present in `@run.attr_accessors`,
308
+ # that is, it must have been passed to `providing` when calling `with_acidity`
309
+ iterable_key = current_step_hash["for_each"]
310
+ raise UnknownForEachCollection if iterable_key.present? && !@run.attr_accessors.key?(iterable_key)
311
+
312
+ # in order to ensure we don't iterate over successfully iterated values in previous runs,
313
+ # we need to store the collection of already processed values.
314
+ # we store this collection under a key bound to the current step to ensure multiple steps
315
+ # can iterate over the same collection.
316
+ iterated_key = "processed_#{current_step_name}_#{iterable_key}"
317
+
318
+ # Get the collection of values to iterate over (`prev_iterables`)
319
+ # and the collection of values already iterated (`prev_iterateds`)
320
+ # in order to determine the collection of values to iterate over (`curr_iterables`)
321
+ prev_iterables = @run.attr_accessors.fetch(iterable_key, []) || []
322
+ raise UniterableForEachCollection unless prev_iterables.is_a?(Enumerable)
323
+
324
+ prev_iterateds = @run.attr_accessors.fetch(iterated_key, []) || []
325
+ curr_iterables = prev_iterables.reject { |item| prev_iterateds.include? item }
326
+ next_item = curr_iterables.first
327
+
328
+ result = nil
329
+ if iterable_key.present? && next_item.present? # have an item to iterate over, so pass it to the step method
330
+ result = callable.call(next_item)
331
+ elsif iterable_key.present? && next_item.nil? # have iterated over all items
332
+ result = true
333
+ elsif callable.arity.zero?
334
+ result = callable.call
335
+ else
336
+ raise TooManyParametersForStepMethod
337
+ end
338
+
339
+ if result.is_a?(FinishedPoint)
340
+ result
341
+ elsif next_item.present?
342
+ prev_iterateds << next_item
343
+ @run.attr_accessors[iterated_key] = prev_iterateds
344
+ @run.save!(validate: false)
345
+ RecoveryPoint.new(current_step_name)
346
+ elsif next_step_finishes?
347
+ FinishedPoint.new
348
+ else
349
+ RecoveryPoint.new(next_step_name)
350
+ end
351
+ end
352
+ end
353
+
354
+ # jobs can have no-op steps, especially so that they can use only the async/await mechanism for that step
355
+ def current_step_method
356
+ return @job.method(current_step_name) if @job.respond_to?(current_step_name, _include_private = true)
357
+ return proc {} if current_step_hash["awaits"].present?
358
+
359
+ raise UndefinedStepMethod
360
+ end
361
+ end
362
+
363
+ class IdempotencyKey
364
+ def initialize(job)
365
+ @job = job
366
+ end
367
+
368
+ def value(acidic_by: :job_id)
369
+ case acidic_by
370
+ when Proc
371
+ proc_result = @job.instance_exec(&acidic_by)
372
+ Digest::SHA1.hexdigest [@job.class.name, proc_result].flatten.join
373
+ when :job_arguments
374
+ Digest::SHA1.hexdigest [@job.class.name, @job.arguments].flatten.join
375
+ else
376
+ if @job.job_id.start_with? "STG_"
377
+ # "STG__#{idempotency_key}__#{encoded_global_id}"
378
+ _prefix, idempotency_key, _encoded_global_id = @job.job_id.split("__")
379
+ idempotency_key
380
+ else
381
+ @job.job_id
382
+ end
383
+ end
384
+ end
385
+ end
386
+
387
+ class Processor
388
+ def initialize(run, job)
389
+ @run = run
390
+ @job = job
391
+ @workflow = Workflow.new(run, job)
392
+ end
393
+
394
+ def process_run
395
+ # if the run record is already marked as finished, immediately return its result
396
+ return @run.succeeded? if @run.finished?
397
+
398
+ AcidicJob.logger.log_run_event("Processing #{@workflow.current_step_name}...", @job, @run)
399
+ loop do
400
+ break if @run.finished?
401
+
402
+ if !@run.known_recovery_point?
403
+ raise UnknownRecoveryPoint,
404
+ "Defined workflow does not reference this step: #{@workflow.current_step_name.inspect}"
405
+ elsif !(awaited_jobs = @workflow.current_step_hash.fetch("awaits", []) || []).empty?
406
+ # We only execute the current step, without progressing to the next step.
407
+ # This ensures that any failures in parallel jobs will have this step retried in the main workflow
408
+ step_result = @workflow.execute_current_step
409
+ # We allow the `#step_done` method to manage progressing the recovery_point to the next step,
410
+ # and then calling `process_run` to restart the main workflow on the next step.
411
+ # We pass the `step_result` so that the async callback called after the step-parallel-jobs complete
412
+ # can move on to the appropriate next stage in the workflow.
413
+ enqueue_awaited_jobs(awaited_jobs, step_result)
414
+ # after processing the current step, break the processing loop
415
+ # and stop this method from blocking in the primary worker
416
+ # as it will continue once the background workers all succeed
417
+ # so we want to keep the primary worker queue free to process new work
418
+ # this CANNOT ever be `break` as that wouldn't exit the parent job,
419
+ # only this step in the workflow, blocking as it awaits the next step
420
+ break
421
+ else
422
+ @workflow.execute_current_step
423
+ @workflow.progress_to_next_step
424
+ end
425
+ end
426
+ AcidicJob.logger.log_run_event("Processed #{@workflow.current_step_name}.", @job, @run)
427
+
428
+ @run.succeeded?
429
+ end
430
+
431
+ private
432
+
433
+ def enqueue_awaited_jobs(jobs_or_jobs_getter, step_result)
434
+ awaited_jobs = jobs_from(jobs_or_jobs_getter)
435
+
436
+ AcidicJob.logger.log_run_event("Enqueuing #{awaited_jobs.count} awaited jobs...", @job, @run)
437
+ # All jobs created in the block are actually pushed atomically at the end of the block.
438
+ AcidicJob::Run.transaction do
439
+ awaited_jobs.each do |awaited_job|
440
+ worker_class, args, kwargs = job_args_and_kwargs(awaited_job)
441
+
442
+ job = worker_class.new(*args, **kwargs)
443
+
444
+ AcidicJob::Run.create!(
445
+ staged: true,
446
+ awaited_by: @run,
447
+ returning_to: step_result,
448
+ job_class: worker_class,
449
+ serialized_job: job.serialize,
450
+ idempotency_key: IdempotencyKey.new(job).value(acidic_by: worker_class.try(:acidic_identifier))
451
+ )
452
+ end
453
+ end
454
+ AcidicJob.logger.log_run_event("Enqueued #{awaited_jobs.count} awaited jobs.", @job, @run)
455
+ end
456
+
457
+ def jobs_from(jobs_or_jobs_getter)
458
+ case jobs_or_jobs_getter
459
+ when Array
460
+ jobs_or_jobs_getter
461
+ when Symbol, String
462
+ @job.method(jobs_or_jobs_getter).call
463
+ end
464
+ end
465
+
466
+ def job_args_and_kwargs(job)
467
+ case job
468
+ when Class
469
+ [job, [], {}]
470
+ when String
471
+ [job.constantize, [], {}]
472
+ when Symbol
473
+ [job.to_s.constantize, [], {}]
474
+ else
475
+ [
476
+ job.class,
477
+ job.arguments,
478
+ {}
479
+ ]
480
+ end
481
+ end
482
+ end
483
+
484
+ module Mixin
485
+ extend ActiveSupport::Concern
486
+
487
+ def self.included(other)
488
+ raise UnknownJobAdapter unless defined?(ActiveJob) && other < ActiveJob::Base
489
+
490
+ other.instance_variable_set(:@acidic_identifier, :job_id)
491
+ other.define_singleton_method(:acidic_by_job_identifier) { @acidic_identifier = :job_identifier }
492
+ other.define_singleton_method(:acidic_by_job_arguments) { @acidic_identifier = :job_arguments }
493
+ other.define_singleton_method(:acidic_by) { |&block| @acidic_identifier = block }
494
+ other.define_singleton_method(:acidic_identifier) { @acidic_identifier }
495
+
496
+ # other.set_callback :perform, :after, :finish_staged_job, if: -> { was_staged_job? && !was_workflow_job? }
497
+ other.set_callback :perform, :after, :reenqueue_awaited_by_job, if: -> { was_awaited_job? && !was_workflow_job? }
498
+ other.define_callbacks :finish
499
+ other.set_callback :finish, :after, :reenqueue_awaited_by_job, if: -> { was_awaited_job? && was_workflow_job? }
500
+ end
501
+
502
+ class_methods do
503
+ def perform_acidicly(*args, **kwargs)
504
+ job = new(*args, **kwargs)
505
+
506
+ AcidicJob::Run.create!(
507
+ staged: true,
508
+ job_class: name,
509
+ serialized_job: job.serialize,
510
+ idempotency_key: job.idempotency_key
511
+ )
512
+ end
513
+
514
+ def with(*args, **kwargs)
515
+ job = new(*args, **kwargs)
516
+ # force the job to resolve the `queue_name`, so that we don't try to serialize a Proc into ActiveRecord
517
+ job.queue_name
518
+ job
519
+ end
520
+ end
521
+
522
+ def idempotency_key
523
+ acidic_identifier = self.class.instance_variable_get(:@acidic_identifier)
524
+ IdempotencyKey.new(self).value(acidic_by: acidic_identifier)
525
+ end
526
+
527
+ # &block
528
+ def with_acidic_workflow(persisting: {})
529
+ raise RedefiningWorkflow if defined? @workflow_builder
530
+
531
+ @workflow_builder = WorkflowBuilder.new
532
+ yield @workflow_builder
533
+
534
+ raise NoDefinedSteps if @workflow_builder.steps.empty?
535
+
536
+ # convert the array of steps into a hash of recovery_points and next steps
537
+ workflow = WorkflowBuilder.define_workflow(@workflow_builder.steps)
538
+
539
+ AcidicJob.logger.log_run_event("Initializing run...", self, nil)
540
+ @acidic_job_run = ActiveRecord::Base.transaction(isolation: :read_uncommitted) do
541
+ run = Run.find_by(idempotency_key: idempotency_key)
542
+
543
+ if run.present?
544
+ run.update!(
545
+ last_run_at: Time.current,
546
+ locked_at: Time.current,
547
+ workflow: workflow,
548
+ recovery_point: run.recovery_point || workflow.keys.first
549
+ )
550
+ else
551
+ run = Run.create!(
552
+ staged: false,
553
+ idempotency_key: idempotency_key,
554
+ job_class: self.class.name,
555
+ locked_at: Time.current,
556
+ last_run_at: Time.current,
557
+ workflow: workflow,
558
+ recovery_point: workflow.keys.first,
559
+ serialized_job: serialize
560
+ )
561
+ end
562
+
563
+ # persist `persisting` values and set accessors for each
564
+ # first, get the current state of all accessors for both previously persisted and initialized values
565
+ current_accessors = persisting.stringify_keys.merge(run.attr_accessors)
566
+
567
+ # next, ensure that `Run#attr_accessors` is populated with initial values
568
+ # skip validations for this call to ensure a write
569
+ run.update_column(:attr_accessors, current_accessors) if current_accessors != run.attr_accessors
570
+
571
+ # finally, set reader and writer methods
572
+ current_accessors.each do |accessor, value|
573
+ # the reader method may already be defined
574
+ self.class.attr_reader accessor unless respond_to?(accessor)
575
+ # but we should always update the value to match the current value
576
+ instance_variable_set("@#{accessor}", value)
577
+ # and we overwrite the setter to ensure any updates to an accessor update the `Run` stored value
578
+ # Note: we must define the singleton method on the instance to avoid overwriting setters on other
579
+ # instances of the same class
580
+ define_singleton_method("#{accessor}=") do |updated_value|
581
+ instance_variable_set("@#{accessor}", updated_value)
582
+ run.attr_accessors[accessor] = updated_value
583
+ run.save!(validate: false)
584
+ updated_value
585
+ end
586
+ end
587
+
588
+ run
589
+ end
590
+ AcidicJob.logger.log_run_event("Initialized run.", self, @acidic_job_run)
591
+
592
+ Processor.new(@acidic_job_run, self).process_run
593
+ rescue LocalJumpError
594
+ raise MissingWorkflowBlock, "A block must be passed to `with_acidic_workflow`"
595
+ end
596
+
597
+ private
598
+
599
+ def was_staged_job?
600
+ job_id.start_with? "STG_"
601
+ end
602
+
603
+ def was_workflow_job?
604
+ @acidic_job_run.present?
605
+ end
606
+
607
+ def was_awaited_job?
608
+ was_staged_job? && staged_job_run.present? && staged_job_run.awaited_by.present?
609
+ end
610
+
611
+ def staged_job_run
612
+ return unless was_staged_job?
613
+ return @staged_job_run if defined? @staged_job_run
614
+
615
+ # "STG__#{idempotency_key}__#{encoded_global_id}"
616
+ _prefix, _idempotency_key, encoded_global_id = job_id.split("__")
617
+ staged_job_gid = "gid://#{Base64.decode64(encoded_global_id)}"
618
+
619
+ @staged_job_run = GlobalID::Locator.locate(staged_job_gid)
620
+ end
621
+
622
+ def finish_staged_job
623
+ delete_staged_job_record
624
+ mark_staged_run_as_finished
625
+ end
626
+
627
+ def reenqueue_awaited_by_job
628
+ run = staged_job_run.awaited_by
629
+ job = run.job
630
+ # this needs to be explicitly set so that `was_workflow_job?` appropriately returns `true`
631
+ job.instance_variable_set(:@acidic_job_run, run)
632
+ # re-hydrate the `step_result` object
633
+ step_result = staged_job_run.returning_to
634
+
635
+ workflow = Workflow.new(run, job, step_result)
636
+ # TODO: WRITE REGRESSION TESTS FOR PARALLEL JOB FAILING AND RETRYING THE ORIGINAL STEP
637
+ workflow.progress_to_next_step
638
+
639
+ # when a batch of jobs for a step succeeds, we begin processing the `AcidicJob::Run` record again
640
+ return if run.finished?
641
+
642
+ AcidicJob.logger.log_run_event("Re-enqueuing parent job...", job, run)
643
+ run.enqueue_job
644
+ AcidicJob.logger.log_run_event("Re-enqueued parent job.", job, run)
645
+ end
646
+ end
647
+
648
+ class Base < ActiveJob::Base
649
+ include Mixin
650
+ end
651
+ end
652
+
653
+ require "active_support/test_case"
654
+ require "minitest/mock"
655
+
656
+ ActiveJob::Base.logger = ActiveRecord::Base.logger = Logger.new(IO::NULL)
657
+ # ActiveJob::Base.logger = ActiveRecord::Base.logger = AcidicJob.logger = Logger.new($stdout)
658
+ # ActiveJob::Base.logger = ActiveRecord::Base.logger = Logger.new(IO::NULL)
659
+ # AcidicJob.logger = Logger.new($stdout)
660
+
661
+ class Performance
662
+ def self.reset!
663
+ @performances = 0
664
+ end
665
+
666
+ def self.performed!
667
+ @performances += 1
668
+ end
669
+
670
+ class << self
671
+ attr_reader :performances
672
+ end
673
+
674
+ def self.performed?
675
+ return true if performances.positive?
676
+
677
+ false
678
+ end
679
+
680
+ def self.performed_once?
681
+ return true if performances == 1
682
+
683
+ false
684
+ end
685
+ end
686
+
687
+ class CustomErrorForTesting < StandardError; end
688
+
689
+ # rubocop:disable Lint/ConstantDefinitionInBlock
690
+ class TestCases < ActiveSupport::TestCase
691
+ include ActiveJob::TestHelper
692
+
693
+ def before_setup
694
+ super()
695
+ AcidicJob::Run.delete_all
696
+ Performance.reset!
697
+ end
698
+
699
+ test "`AcidicJob::Base` only adds a few methods to job" do
700
+ class BareJob < AcidicJob::Base; end
701
+
702
+ assert_equal %i[_run_finish_callbacks _finish_callbacks with_acidic_workflow idempotency_key].sort,
703
+ (BareJob.instance_methods - ActiveJob::Base.instance_methods).sort
704
+ end
705
+
706
+ test "`AcidicJob::Base` in parent class adds methods to any job that inherit from parent" do
707
+ class ParentJob < AcidicJob::Base; end
708
+ class ChildJob < ParentJob; end
709
+
710
+ assert_equal %i[_run_finish_callbacks _finish_callbacks with_acidic_workflow idempotency_key].sort,
711
+ (ChildJob.instance_methods - ActiveJob::Base.instance_methods).sort
712
+ end
713
+
714
+ test "calling `with_acidic_workflow` without a block raises `MissingWorkflowBlock`" do
715
+ class JobWithoutBlock < AcidicJob::Base
716
+ def perform
717
+ with_acidic_workflow
718
+ end
719
+ end
720
+
721
+ assert_raises AcidicJob::MissingWorkflowBlock do
722
+ JobWithoutBlock.perform_now
723
+ end
724
+ end
725
+
726
+ test "calling `with_acidic_workflow` with a block without steps raises `NoDefinedSteps`" do
727
+ class JobWithoutSteps < AcidicJob::Base
728
+ def perform
729
+ with_acidic_workflow {} # rubocop:disable Lint/EmptyBlock
730
+ end
731
+ end
732
+
733
+ assert_raises AcidicJob::NoDefinedSteps do
734
+ JobWithoutSteps.perform_now
735
+ end
736
+ end
737
+
738
+ test "calling `with_acidic_workflow` twice raises `RedefiningWorkflow`" do
739
+ class JobWithDoubleWorkflow < AcidicJob::Base
740
+ def perform
741
+ with_acidic_workflow do |workflow|
742
+ workflow.step :do_something
743
+ end
744
+
745
+ with_acidic_workflow {} # rubocop:disable Lint/EmptyBlock
746
+ end
747
+
748
+ def do_something; end
749
+ end
750
+
751
+ assert_raises AcidicJob::RedefiningWorkflow do
752
+ JobWithDoubleWorkflow.perform_now
753
+ end
754
+ end
755
+
756
+ test "calling `with_acidic_workflow` with an undefined step method without `awaits` raises `UndefinedStepMethod`" do
757
+ class JobWithUndefinedStep < AcidicJob::Base
758
+ def perform
759
+ with_acidic_workflow do |workflow|
760
+ workflow.step :no_op
761
+ end
762
+ end
763
+ end
764
+
765
+ assert_raises AcidicJob::UndefinedStepMethod do
766
+ JobWithUndefinedStep.perform_now
767
+ end
768
+ end
769
+
770
+ test "calling `with_acidic_workflow` with `persisting` unserializable value throws `TypeError` error" do
771
+ class JobWithUnpersistableValue < AcidicJob::Base
772
+ def perform
773
+ with_acidic_workflow persisting: { key: -> { :some_proc } } do |workflow|
774
+ workflow.step :do_something
775
+ end
776
+ end
777
+
778
+ def do_something; end
779
+ end
780
+
781
+ assert_raises TypeError do
782
+ JobWithUnpersistableValue.perform_now
783
+ end
784
+ end
785
+
786
+ test "calling `with_acidic_workflow` with `persisting` serializes and saves the hash to the `Run` record" do
787
+ class JobWithPersisting < AcidicJob::Base
788
+ def perform
789
+ with_acidic_workflow persisting: { key: :value } do |workflow|
790
+ workflow.step :do_something
791
+ end
792
+ end
793
+
794
+ def do_something; end
795
+ end
796
+
797
+ result = JobWithPersisting.perform_now
798
+ assert_equal result, true
799
+ run = AcidicJob::Run.find_by(job_class: "TestCases::JobWithPersisting")
800
+ assert_equal run.attr_accessors, { "key" => :value }
801
+ end
802
+
803
+ test "calling `idempotency_key` when `acidic_identifier` is unconfigured returns `job_id`" do
804
+ class JobWithoutAcidicIdentifier < AcidicJob::Base
805
+ def perform; end
806
+ end
807
+
808
+ job = JobWithoutAcidicIdentifier.new
809
+ assert_equal job.job_id, job.idempotency_key
810
+ end
811
+
812
+ test "calling `idempotency_key` when `acidic_by_job_identifier` is set returns `job_id`" do
813
+ class JobWithAcidicByIdentifier < AcidicJob::Base
814
+ acidic_by_job_identifier
815
+
816
+ def perform; end
817
+ end
818
+
819
+ job = JobWithAcidicByIdentifier.new
820
+ assert_equal job.job_id, job.idempotency_key
821
+ end
822
+
823
+ test "calling `idempotency_key` when `acidic_by_job_arguments` is set returns hexidigest" do
824
+ class JobWithAcidicByArguments < AcidicJob::Base
825
+ acidic_by_job_arguments
826
+
827
+ def perform; end
828
+ end
829
+
830
+ job = JobWithAcidicByArguments.new
831
+ assert_equal "867593fcc38b8ee5709d61e4e9124def192d8f35", job.idempotency_key
832
+ end
833
+
834
+ test "calling `idempotency_key` when `acidic_by` is a block returns hexidigest" do
835
+ class JobWithAcidicByArguments < AcidicJob::Base
836
+ acidic_by do
837
+ "a"
838
+ end
839
+
840
+ def perform; end
841
+ end
842
+
843
+ job = JobWithAcidicByArguments.new
844
+ assert_equal "18a3c264100a68264d95a9a98d1aa115bd92107f", job.idempotency_key
845
+ end
846
+
847
+ test "basic one step workflow runs successfully" do
848
+ class BasicJob < AcidicJob::Base
849
+ def perform
850
+ with_acidic_workflow do |workflow|
851
+ workflow.step :do_something
852
+ end
853
+ end
854
+
855
+ def do_something
856
+ Performance.performed!
857
+ end
858
+ end
859
+
860
+ result = BasicJob.perform_now
861
+ assert_equal true, result
862
+ assert_equal true, Performance.performed_once?
863
+ end
864
+
865
+ test "an error raised in a step method is stored in the run record" do
866
+ class ErroringJob < AcidicJob::Base
867
+ def perform
868
+ with_acidic_workflow do |workflow|
869
+ workflow.step :do_something
870
+ end
871
+ end
872
+
873
+ def do_something
874
+ raise CustomErrorForTesting
875
+ end
876
+ end
877
+
878
+ assert_raises CustomErrorForTesting do
879
+ ErroringJob.perform_now
880
+ end
881
+
882
+ run = AcidicJob::Run.find_by(job_class: "TestCases::ErroringJob")
883
+ assert_equal CustomErrorForTesting, run.error_object.class
884
+ end
885
+
886
+ test "basic two step workflow runs successfully" do
887
+ class TwoStepJob < AcidicJob::Base
888
+ def perform
889
+ with_acidic_workflow do |workflow|
890
+ workflow.step :step_one
891
+ workflow.step :step_two
892
+ end
893
+ end
894
+
895
+ def step_one
896
+ Performance.performed!
897
+ end
898
+
899
+ def step_two
900
+ Performance.performed!
901
+ end
902
+ end
903
+
904
+ result = TwoStepJob.perform_now
905
+ assert_equal true, result
906
+ assert_equal 2, Performance.performances
907
+ end
908
+
909
+ test "basic two step workflow can be started from second step if pre-existing run record present" do
910
+ class RestartedTwoStepJob < AcidicJob::Base
911
+ def perform
912
+ with_acidic_workflow do |workflow|
913
+ workflow.step :step_one
914
+ workflow.step :step_two
915
+ end
916
+ end
917
+
918
+ def step_one
919
+ Performance.performed!
920
+ end
921
+
922
+ def step_two
923
+ Performance.performed!
924
+ end
925
+ end
926
+
927
+ run = AcidicJob::Run.create!(
928
+ idempotency_key: "67b823ea-34f0-40a0-88d9-7e3b7ff9e769",
929
+ serialized_job: {
930
+ "job_class" => "TestCases::RestartedTwoStepJob",
931
+ "job_id" => "67b823ea-34f0-40a0-88d9-7e3b7ff9e769",
932
+ "provider_job_id" => nil,
933
+ "queue_name" => "default",
934
+ "priority" => nil,
935
+ "arguments" => [],
936
+ "executions" => 1,
937
+ "exception_executions" => {},
938
+ "locale" => "en",
939
+ "timezone" => "UTC",
940
+ "enqueued_at" => ""
941
+ },
942
+ job_class: "TestCases::RestartedTwoStepJob",
943
+ last_run_at: Time.current,
944
+ recovery_point: "step_two",
945
+ workflow: {
946
+ "step_one" => { "does" => "step_one", "awaits" => [], "for_each" => nil, "then" => "step_two" },
947
+ "step_two" => { "does" => "step_two", "awaits" => [], "for_each" => nil, "then" => "FINISHED" }
948
+ }
949
+ )
950
+ AcidicJob::Run.stub(:find_by, ->(*) { run }) do
951
+ result = RestartedTwoStepJob.perform_now
952
+ assert_equal true, result
953
+ end
954
+ assert_equal 1, Performance.performances
955
+ end
956
+
957
+ test "passing `for_each` option not in `providing` hash throws `UnknownForEachCollection` error" do
958
+ class UnknownForEachStep < AcidicJob::Base
959
+ def perform
960
+ with_acidic_workflow do |workflow|
961
+ workflow.step :do_something, for_each: :unknown_collection
962
+ end
963
+ end
964
+
965
+ def do_something(item); end
966
+ end
967
+
968
+ assert_raises AcidicJob::UnknownForEachCollection do
969
+ UnknownForEachStep.perform_now
970
+ end
971
+ end
972
+
973
+ test "passing `for_each` option that isn't iterable throws `UniterableForEachCollection` error" do
974
+ class UniterableForEachStep < AcidicJob::Base
975
+ def perform
976
+ with_acidic_workflow persisting: { collection: true } do |workflow|
977
+ workflow.step :do_something, for_each: :collection
978
+ end
979
+ end
980
+
981
+ def do_something(item); end
982
+ end
983
+
984
+ assert_raises AcidicJob::UniterableForEachCollection do
985
+ UniterableForEachStep.perform_now
986
+ end
987
+ end
988
+
989
+ test "passing valid `for_each` option iterates over collection with step method" do
990
+ class ValidForEachStep < AcidicJob::Base
991
+ attr_reader :processed_items
992
+
993
+ def initialize
994
+ @processed_items = []
995
+ super()
996
+ end
997
+
998
+ def perform
999
+ with_acidic_workflow persisting: { collection: (1..5) } do |workflow|
1000
+ workflow.step :do_something, for_each: :collection
1001
+ end
1002
+ end
1003
+
1004
+ def do_something(item)
1005
+ @processed_items << item
1006
+ end
1007
+ end
1008
+
1009
+ job = ValidForEachStep.new
1010
+ job.perform_now
1011
+ assert_equal [1, 2, 3, 4, 5], job.processed_items
1012
+ end
1013
+
1014
+ test "can pass same `for_each` option to multiple step methods" do
1015
+ class MultipleForEachSteps < AcidicJob::Base
1016
+ attr_reader :step_one_processed_items, :step_two_processed_items
1017
+
1018
+ def initialize
1019
+ @step_one_processed_items = []
1020
+ @step_two_processed_items = []
1021
+ super()
1022
+ end
1023
+
1024
+ def perform
1025
+ with_acidic_workflow persisting: { items: (1..5) } do |workflow|
1026
+ workflow.step :step_one, for_each: :items
1027
+ workflow.step :step_two, for_each: :items
1028
+ end
1029
+ end
1030
+
1031
+ def step_one(item)
1032
+ @step_one_processed_items << item
1033
+ end
1034
+
1035
+ def step_two(item)
1036
+ @step_two_processed_items << item
1037
+ end
1038
+ end
1039
+
1040
+ job = MultipleForEachSteps.new
1041
+ job.perform_now
1042
+ assert_equal [1, 2, 3, 4, 5], job.step_one_processed_items
1043
+ assert_equal [1, 2, 3, 4, 5], job.step_two_processed_items
1044
+ end
1045
+
1046
+ test "can define `after_finish` callbacks" do
1047
+ class JobWithAfterFinishCallback < AcidicJob::Base
1048
+ set_callback :finish, :after, :delete_run_record
1049
+
1050
+ def perform
1051
+ with_acidic_workflow do |workflow|
1052
+ workflow.step :do_something
1053
+ end
1054
+ end
1055
+
1056
+ def do_something; end
1057
+
1058
+ def delete_run_record
1059
+ @acidic_job_run.destroy!
1060
+ end
1061
+ end
1062
+
1063
+ result = JobWithAfterFinishCallback.perform_now
1064
+ assert_equal true, result
1065
+ assert_equal 0, AcidicJob::Run.count
1066
+ end
1067
+
1068
+ test "`after_finish` callbacks don't run if job errors" do
1069
+ class ErroringJobWithAfterFinishCallback < AcidicJob::Base
1070
+ set_callback :finish, :after, :delete_run_record
1071
+
1072
+ def perform
1073
+ with_acidic_workflow do |workflow|
1074
+ workflow.step :do_something
1075
+ end
1076
+ end
1077
+
1078
+ def do_something
1079
+ raise CustomErrorForTesting
1080
+ end
1081
+
1082
+ def delete_run_record
1083
+ @acidic_job_run.destroy!
1084
+ end
1085
+ end
1086
+
1087
+ assert_raises CustomErrorForTesting do
1088
+ ErroringJobWithAfterFinishCallback.perform_now
1089
+ end
1090
+ assert_equal 1, AcidicJob::Run.count
1091
+ assert_equal 1, AcidicJob::Run.where(job_class: "TestCases::ErroringJobWithAfterFinishCallback").count
1092
+ end
1093
+
1094
+ test "rescued error in `perform` doesn't prevent `Run#error_object` from being stored" do
1095
+ class JobWithErrorAndRescueInPerform < AcidicJob::Base
1096
+ def perform
1097
+ with_acidic_workflow do |workflow|
1098
+ workflow.step :do_something
1099
+ end
1100
+ rescue CustomErrorForTesting
1101
+ true
1102
+ end
1103
+
1104
+ def do_something
1105
+ raise CustomErrorForTesting
1106
+ end
1107
+ end
1108
+
1109
+ result = JobWithErrorAndRescueInPerform.perform_now
1110
+ assert_equal result, true
1111
+ assert_equal 1, AcidicJob::Run.count
1112
+ run = AcidicJob::Run.find_by(job_class: "TestCases::JobWithErrorAndRescueInPerform")
1113
+ assert_equal CustomErrorForTesting, run.error_object.class
1114
+ end
1115
+
1116
+ test "error in first step rolls back step transaction" do
1117
+ class JobWithErrorInStepMethod < AcidicJob::Base
1118
+ def perform
1119
+ with_acidic_workflow persisting: { accessor: nil } do |workflow|
1120
+ workflow.step :do_something
1121
+ end
1122
+ end
1123
+
1124
+ def do_something
1125
+ self.accessor = "value"
1126
+ raise CustomErrorForTesting
1127
+ end
1128
+ end
1129
+
1130
+ assert_raises CustomErrorForTesting do
1131
+ JobWithErrorInStepMethod.perform_now
1132
+ end
1133
+
1134
+ assert_equal AcidicJob::Run.count, 1
1135
+ run = AcidicJob::Run.find_by(job_class: "TestCases::JobWithErrorInStepMethod")
1136
+ assert_equal run.error_object.class, CustomErrorForTesting
1137
+ assert_equal run.attr_accessors, { "accessor" => nil }
1138
+ end
1139
+
1140
+ test "logic inside `with_acidic_workflow` block is executed appropriately" do
1141
+ class JobWithSwitchOnStep < AcidicJob::Base
1142
+ def perform(bool)
1143
+ with_acidic_workflow do |workflow|
1144
+ workflow.step :do_something if bool
1145
+ end
1146
+ end
1147
+
1148
+ def do_something
1149
+ raise CustomErrorForTesting
1150
+ end
1151
+ end
1152
+
1153
+ assert_raises CustomErrorForTesting do
1154
+ JobWithSwitchOnStep.perform_now(true)
1155
+ end
1156
+
1157
+ assert_raises AcidicJob::NoDefinedSteps do
1158
+ JobWithSwitchOnStep.perform_now(false)
1159
+ end
1160
+
1161
+ assert_equal 1, AcidicJob::Run.count
1162
+ end
1163
+
1164
+ test "invalid worker throws `UnknownJobAdapter` error" do
1165
+ assert_raises AcidicJob::UnknownJobAdapter do
1166
+ Class.new do
1167
+ include AcidicJob::Mixin
1168
+ end
1169
+ end
1170
+ end
1171
+
1172
+ test "`with_acidic_workflow` always returns boolean, regardless of last value of the block" do
1173
+ class JobWithArbitraryReturnValue < AcidicJob::Base
1174
+ def perform
1175
+ with_acidic_workflow do |workflow|
1176
+ workflow.step :do_something
1177
+ 12_345
1178
+ end
1179
+ end
1180
+
1181
+ def do_something
1182
+ Performance.performed!
1183
+ end
1184
+ end
1185
+
1186
+ result = JobWithArbitraryReturnValue.perform_now
1187
+ assert_equal true, result
1188
+ assert_equal true, Performance.performed_once?
1189
+ end
1190
+
1191
+ test "staged workflow job only creates on `AcidicJob::Run` record" do
1192
+ class StagedWorkflowJob < AcidicJob::Base
1193
+ def perform
1194
+ with_acidic_workflow do |workflow|
1195
+ workflow.step :do_something
1196
+ end
1197
+ end
1198
+
1199
+ def do_something
1200
+ Performance.performed!
1201
+ end
1202
+ end
1203
+
1204
+ perform_enqueued_jobs do
1205
+ StagedWorkflowJob.perform_acidicly
1206
+ end
1207
+
1208
+ assert_equal 1, AcidicJob::Run.count
1209
+
1210
+ run = AcidicJob::Run.find_by(job_class: "TestCases::StagedWorkflowJob")
1211
+ assert_equal "FINISHED", run.recovery_point
1212
+ assert_equal 1, Performance.performances
1213
+ end
1214
+
1215
+ test "workflow job with successful `awaits` job runs successfully" do
1216
+ class SimpleWorkflowJob < AcidicJob::Base
1217
+ class SuccessfulAsyncJob < AcidicJob::Base
1218
+ def perform
1219
+ Performance.performed!
1220
+ end
1221
+ end
1222
+
1223
+ def perform
1224
+ with_acidic_workflow do |workflow|
1225
+ workflow.step :await_step, awaits: [SuccessfulAsyncJob]
1226
+ workflow.step :do_something
1227
+ end
1228
+ end
1229
+
1230
+ def do_something
1231
+ Performance.performed!
1232
+ end
1233
+ end
1234
+
1235
+ perform_enqueued_jobs do
1236
+ SimpleWorkflowJob.perform_later
1237
+ end
1238
+
1239
+ assert_equal 2, AcidicJob::Run.count
1240
+
1241
+ parent_run = AcidicJob::Run.find_by(job_class: "TestCases::SimpleWorkflowJob")
1242
+ assert_equal "FINISHED", parent_run.recovery_point
1243
+ assert_equal false, parent_run.staged?
1244
+
1245
+ child_run = AcidicJob::Run.find_by(job_class: "TestCases::SimpleWorkflowJob::SuccessfulAsyncJob")
1246
+ assert_nil child_run.recovery_point
1247
+ assert_equal true, child_run.staged?
1248
+
1249
+ assert_equal 2, Performance.performances
1250
+ end
1251
+
1252
+ test "workflow job with erroring `awaits` job does not progress and does not store error object" do
1253
+ class WorkflowWithErroringAwaitsJob < AcidicJob::Base
1254
+ class ErroringAsyncJob < AcidicJob::Base
1255
+ def perform
1256
+ raise CustomErrorForTesting
1257
+ end
1258
+ end
1259
+
1260
+ def perform
1261
+ with_acidic_workflow do |workflow|
1262
+ workflow.step :await_step, awaits: [ErroringAsyncJob]
1263
+ workflow.step :do_something
1264
+ end
1265
+ end
1266
+
1267
+ def do_something
1268
+ Performance.performed!
1269
+ end
1270
+ end
1271
+
1272
+ perform_enqueued_jobs do
1273
+ assert_raises CustomErrorForTesting do
1274
+ WorkflowWithErroringAwaitsJob.perform_later
1275
+ end
1276
+ end
1277
+
1278
+ assert_equal 2, AcidicJob::Run.count
1279
+
1280
+ parent_run = AcidicJob::Run.find_by(job_class: "TestCases::WorkflowWithErroringAwaitsJob")
1281
+ assert_equal "await_step", parent_run.recovery_point
1282
+ assert_nil parent_run.error_object
1283
+ assert_equal false, parent_run.staged?
1284
+
1285
+ child_run = AcidicJob::Run.find_by(job_class: "TestCases::WorkflowWithErroringAwaitsJob::ErroringAsyncJob")
1286
+ assert_nil child_run.recovery_point
1287
+ assert_nil child_run.error_object
1288
+ assert_equal true, child_run.staged?
1289
+
1290
+ assert_equal 0, Performance.performances
1291
+ end
1292
+
1293
+ test "workflow job with successful `awaits` job that itself `awaits` another successful job" do
1294
+ class NestedSuccessfulAwaitSteps < AcidicJob::Base
1295
+ class SuccessfulAwaitedAndAwaits < AcidicJob::Base
1296
+ class NestedSuccessfulAwaited < AcidicJob::Base
1297
+ def perform
1298
+ Performance.performed!
1299
+ end
1300
+ end
1301
+
1302
+ def perform
1303
+ with_acidic_workflow do |workflow|
1304
+ workflow.step :await_nested_step, awaits: [NestedSuccessfulAwaited]
1305
+ end
1306
+ end
1307
+ end
1308
+
1309
+ def perform
1310
+ with_acidic_workflow do |workflow|
1311
+ workflow.step :await_step, awaits: [SuccessfulAwaitedAndAwaits]
1312
+ workflow.step :do_something
1313
+ end
1314
+ end
1315
+
1316
+ def do_something
1317
+ Performance.performed!
1318
+ end
1319
+ end
1320
+
1321
+ perform_enqueued_jobs do
1322
+ NestedSuccessfulAwaitSteps.perform_later
1323
+ end
1324
+
1325
+ assert_equal 3, AcidicJob::Run.count
1326
+
1327
+ parent_run = AcidicJob::Run.find_by(job_class: "TestCases::NestedSuccessfulAwaitSteps")
1328
+ assert_equal "FINISHED", parent_run.recovery_point
1329
+ assert_nil parent_run.error_object
1330
+ assert_equal false, parent_run.staged?
1331
+
1332
+ child_run = AcidicJob::Run.find_by(
1333
+ job_class: "TestCases::NestedSuccessfulAwaitSteps::SuccessfulAwaitedAndAwaits"
1334
+ )
1335
+ assert_equal "FINISHED", child_run.recovery_point
1336
+ assert_nil child_run.error_object
1337
+ assert_equal true, child_run.staged?
1338
+
1339
+ grandchild_run = AcidicJob::Run.find_by(
1340
+ job_class: "TestCases::NestedSuccessfulAwaitSteps::SuccessfulAwaitedAndAwaits::NestedSuccessfulAwaited"
1341
+ )
1342
+ assert_nil grandchild_run.recovery_point
1343
+ assert_nil grandchild_run.error_object
1344
+ assert_equal true, grandchild_run.staged?
1345
+
1346
+ assert_equal 2, Performance.performances
1347
+ end
1348
+
1349
+ test "workflow job with successful `awaits` job that itself `awaits` another erroring job" do
1350
+ class JobWithNestedErroringAwaitSteps < AcidicJob::Base
1351
+ class SuccessfulAwaitedAndAwaitsJob < AcidicJob::Base
1352
+ class NestedErroringAwaitedJob < AcidicJob::Base
1353
+ def perform
1354
+ raise CustomErrorForTesting
1355
+ end
1356
+ end
1357
+
1358
+ def perform
1359
+ with_acidic_workflow do |workflow|
1360
+ workflow.step :await_nested_step, awaits: [NestedErroringAwaitedJob]
1361
+ end
1362
+ end
1363
+ end
1364
+
1365
+ def perform
1366
+ with_acidic_workflow do |workflow|
1367
+ workflow.step :await_step, awaits: [SuccessfulAwaitedAndAwaitsJob]
1368
+ workflow.step :do_something
1369
+ end
1370
+ end
1371
+
1372
+ def do_something
1373
+ Performance.performed!
1374
+ end
1375
+ end
1376
+
1377
+ perform_enqueued_jobs do
1378
+ assert_raises CustomErrorForTesting do
1379
+ JobWithNestedErroringAwaitSteps.perform_later
1380
+ end
1381
+ end
1382
+
1383
+ assert_equal 3, AcidicJob::Run.count
1384
+
1385
+ parent_run = AcidicJob::Run.find_by(job_class: "TestCases::JobWithNestedErroringAwaitSteps")
1386
+ assert_equal "await_step", parent_run.recovery_point
1387
+ assert_nil parent_run.error_object
1388
+ assert_equal false, parent_run.staged?
1389
+
1390
+ child_run = AcidicJob::Run.find_by(
1391
+ job_class: "TestCases::JobWithNestedErroringAwaitSteps::SuccessfulAwaitedAndAwaitsJob"
1392
+ )
1393
+ assert_equal "await_nested_step", child_run.recovery_point
1394
+ assert_nil child_run.error_object
1395
+ assert_equal true, child_run.staged?
1396
+
1397
+ grandchild_run = AcidicJob::Run.find_by(
1398
+ job_class: "TestCases::JobWithNestedErroringAwaitSteps::SuccessfulAwaitedAndAwaitsJob::NestedErroringAwaitedJob"
1399
+ )
1400
+ assert_nil grandchild_run.recovery_point
1401
+ assert_nil grandchild_run.error_object
1402
+ assert_equal true, grandchild_run.staged?
1403
+
1404
+ assert_equal 0, Performance.performances
1405
+ end
1406
+
1407
+ test "workflow job with successful `awaits` initialized with arguments" do
1408
+ class JobWithSuccessfulArgAwaitStep < AcidicJob::Base
1409
+ class SuccessfulArgJob < AcidicJob::Base
1410
+ def perform(_arg)
1411
+ Performance.performed!
1412
+ end
1413
+ end
1414
+
1415
+ def perform
1416
+ with_acidic_workflow do |workflow|
1417
+ workflow.step :await_step, awaits: [SuccessfulArgJob.with(123)]
1418
+ end
1419
+ end
1420
+ end
1421
+
1422
+ perform_enqueued_jobs do
1423
+ JobWithSuccessfulArgAwaitStep.perform_later
1424
+ end
1425
+
1426
+ assert_equal 2, AcidicJob::Run.count
1427
+
1428
+ parent_run = AcidicJob::Run.find_by(job_class: "TestCases::JobWithSuccessfulArgAwaitStep")
1429
+ assert_equal "FINISHED", parent_run.recovery_point
1430
+ assert_nil parent_run.error_object
1431
+ assert_equal false, parent_run.staged?
1432
+
1433
+ child_run = AcidicJob::Run.find_by(job_class: "TestCases::JobWithSuccessfulArgAwaitStep::SuccessfulArgJob")
1434
+ assert_nil child_run.recovery_point
1435
+ assert_nil child_run.error_object
1436
+ assert_equal true, child_run.staged?
1437
+
1438
+ assert_equal 1, Performance.performances
1439
+ end
1440
+
1441
+ test "workflow job with dynamic `awaits` method as Symbol that returns successful awaited job" do
1442
+ class JobWithDynamicAwaitsAsSymbol < AcidicJob::Base
1443
+ class SuccessfulDynamicAwaitFromSymbolJob < AcidicJob::Base
1444
+ def perform(_arg)
1445
+ Performance.performed!
1446
+ end
1447
+ end
1448
+
1449
+ class ErroringDynamicAwaitFromSymbolJob < AcidicJob::Base
1450
+ def perform
1451
+ raise CustomErrorForTesting
1452
+ end
1453
+ end
1454
+
1455
+ def perform(bool)
1456
+ @bool = bool
1457
+
1458
+ with_acidic_workflow do |workflow|
1459
+ workflow.step :await_step, awaits: :dynamic_awaiting
1460
+ end
1461
+ end
1462
+
1463
+ def dynamic_awaiting
1464
+ return [SuccessfulDynamicAwaitFromSymbolJob.with(123)] if @bool
1465
+
1466
+ [ErroringDynamicAwaitFromSymbolJob]
1467
+ end
1468
+ end
1469
+
1470
+ perform_enqueued_jobs do
1471
+ JobWithDynamicAwaitsAsSymbol.perform_later(true)
1472
+ end
1473
+
1474
+ parent_run = AcidicJob::Run.find_by(job_class: "TestCases::JobWithDynamicAwaitsAsSymbol")
1475
+ assert_equal "FINISHED", parent_run.recovery_point
1476
+ assert_nil parent_run.error_object
1477
+ assert_equal false, parent_run.staged?
1478
+
1479
+ child_run = AcidicJob::Run.find_by(
1480
+ job_class: "TestCases::JobWithDynamicAwaitsAsSymbol::SuccessfulDynamicAwaitFromSymbolJob"
1481
+ )
1482
+ assert_nil child_run.recovery_point
1483
+ assert_nil child_run.error_object
1484
+ assert_equal true, child_run.staged?
1485
+
1486
+ assert_equal 1, Performance.performances
1487
+ end
1488
+
1489
+ test "workflow job with dynamic `awaits` method as Symbol that returns erroring awaited job" do
1490
+ class JobWithDynamicAwaitsAsSymbol < AcidicJob::Base
1491
+ class SuccessfulDynamicAwaitFromSymbolJob < AcidicJob::Base
1492
+ def perform(_arg)
1493
+ Performance.performed!
1494
+ end
1495
+ end
1496
+
1497
+ class ErroringDynamicAwaitFromSymbolJob < AcidicJob::Base
1498
+ def perform
1499
+ raise CustomErrorForTesting
1500
+ end
1501
+ end
1502
+
1503
+ def perform(bool)
1504
+ @bool = bool
1505
+
1506
+ with_acidic_workflow do |workflow|
1507
+ workflow.step :await_step, awaits: :dynamic_awaiting
1508
+ end
1509
+ end
1510
+
1511
+ def dynamic_awaiting
1512
+ return [SuccessfulDynamicAwaitFromSymbolJob.with(123)] if @bool
1513
+
1514
+ [ErroringDynamicAwaitFromSymbolJob]
1515
+ end
1516
+ end
1517
+
1518
+ perform_enqueued_jobs do
1519
+ assert_raises CustomErrorForTesting do
1520
+ JobWithDynamicAwaitsAsSymbol.perform_later(false)
1521
+ end
1522
+ end
1523
+
1524
+ parent_run = AcidicJob::Run.find_by(job_class: "TestCases::JobWithDynamicAwaitsAsSymbol")
1525
+ assert_equal "await_step", parent_run.recovery_point
1526
+ assert_nil parent_run.error_object
1527
+ assert_equal false, parent_run.staged?
1528
+
1529
+ child_run = AcidicJob::Run.find_by(
1530
+ job_class: "TestCases::JobWithDynamicAwaitsAsSymbol::ErroringDynamicAwaitFromSymbolJob"
1531
+ )
1532
+ assert_nil child_run.recovery_point
1533
+ assert_nil child_run.error_object
1534
+ assert_equal true, child_run.staged?
1535
+
1536
+ assert_equal 0, Performance.performances
1537
+ end
1538
+
1539
+ test "workflow job with dynamic `awaits` method as String that returns successful awaited job" do
1540
+ class JobWithDynamicAwaitsAsString < AcidicJob::Base
1541
+ class SuccessfulDynamicAwaitFromStringJob < AcidicJob::Base
1542
+ def perform(_arg)
1543
+ Performance.performed!
1544
+ end
1545
+ end
1546
+
1547
+ class ErroringDynamicAwaitFromStringJob < AcidicJob::Base
1548
+ def perform
1549
+ raise CustomErrorForTesting
1550
+ end
1551
+ end
1552
+
1553
+ def perform(bool)
1554
+ @bool = bool
1555
+
1556
+ with_acidic_workflow do |workflow|
1557
+ workflow.step :await_step, awaits: "dynamic_awaiting"
1558
+ end
1559
+ end
1560
+
1561
+ def dynamic_awaiting
1562
+ return [SuccessfulDynamicAwaitFromStringJob.with(123)] if @bool
1563
+
1564
+ [ErroringDynamicAwaitFromStringJob]
1565
+ end
1566
+ end
1567
+
1568
+ perform_enqueued_jobs do
1569
+ JobWithDynamicAwaitsAsString.perform_later(true)
1570
+ end
1571
+
1572
+ parent_run = AcidicJob::Run.find_by(job_class: "TestCases::JobWithDynamicAwaitsAsString")
1573
+ assert_equal "FINISHED", parent_run.recovery_point
1574
+ assert_nil parent_run.error_object
1575
+ assert_equal false, parent_run.staged?
1576
+
1577
+ child_run = AcidicJob::Run.find_by(
1578
+ job_class: "TestCases::JobWithDynamicAwaitsAsString::SuccessfulDynamicAwaitFromStringJob"
1579
+ )
1580
+ assert_nil child_run.recovery_point
1581
+ assert_nil child_run.error_object
1582
+ assert_equal true, child_run.staged?
1583
+
1584
+ assert_equal 1, Performance.performances
1585
+ end
1586
+
1587
+ test "workflow job with dynamic `awaits` method as String that returns erroring awaited job" do
1588
+ class JobWithDynamicAwaitsAsString < AcidicJob::Base
1589
+ class SuccessfulDynamicAwaitFromStringJob < AcidicJob::Base
1590
+ def perform(_arg)
1591
+ Performance.performed!
1592
+ end
1593
+ end
1594
+
1595
+ class ErroringDynamicAwaitFromStringJob < AcidicJob::Base
1596
+ def perform
1597
+ raise CustomErrorForTesting
1598
+ end
1599
+ end
1600
+
1601
+ def perform(bool)
1602
+ @bool = bool
1603
+
1604
+ with_acidic_workflow do |workflow|
1605
+ workflow.step :await_step, awaits: "dynamic_awaiting"
1606
+ end
1607
+ end
1608
+
1609
+ def dynamic_awaiting
1610
+ return [SuccessfulDynamicAwaitFromStringJob.with(123)] if @bool
1611
+
1612
+ [ErroringDynamicAwaitFromStringJob]
1613
+ end
1614
+ end
1615
+
1616
+ perform_enqueued_jobs do
1617
+ assert_raises CustomErrorForTesting do
1618
+ JobWithDynamicAwaitsAsString.perform_later(false)
1619
+ end
1620
+ end
1621
+
1622
+ assert_equal 2, AcidicJob::Run.count
1623
+
1624
+ parent_run = AcidicJob::Run.find_by(job_class: "TestCases::JobWithDynamicAwaitsAsString")
1625
+ assert_equal "await_step", parent_run.recovery_point
1626
+ assert_nil parent_run.error_object
1627
+ assert_equal false, parent_run.staged?
1628
+
1629
+ child_run = AcidicJob::Run.find_by(
1630
+ job_class: "TestCases::JobWithDynamicAwaitsAsString::ErroringDynamicAwaitFromStringJob"
1631
+ )
1632
+ assert_nil child_run.recovery_point
1633
+ assert_nil child_run.error_object
1634
+ assert_equal true, child_run.staged?
1635
+
1636
+ assert_equal 0, Performance.performances
1637
+ end
1638
+
1639
+ # -----------------------------------------------------------------------------------------------
1640
+ # MATRIX OF POSSIBLE KINDS OF JOBS
1641
+ # [
1642
+ # ["workflow", "staged", "awaited"],
1643
+ # ["workflow", "staged", "unawaited"],
1644
+ # ["workflow", "unstaged", "awaited"],
1645
+ # ["workflow", "unstaged", "unawaited"],
1646
+ # ["non-workflow", "staged", "awaited"],
1647
+ # ["non-workflow", "staged", "unawaited"],
1648
+ # ["non-workflow", "unstaged", "awaited"],
1649
+ # ["non-workflow", "unstaged", "unawaited"],
1650
+ # ]
1651
+
1652
+ test "non-workflow, unstaged, unawaited job successfully performs without `Run` records" do
1653
+ class NowJobNonWorkflowUnstagedUnawaited < AcidicJob::Base
1654
+ def perform
1655
+ Performance.performed!
1656
+ end
1657
+ end
1658
+
1659
+ NowJobNonWorkflowUnstagedUnawaited.perform_now
1660
+
1661
+ assert_equal 0, AcidicJob::Run.count
1662
+ assert_equal 1, Performance.performances
1663
+ end
1664
+
1665
+ test "non-workflow, unstaged, awaited job is invalid" do
1666
+ class AwaitingJob < AcidicJob::Base; end
1667
+
1668
+ class JobNonWorkflowUnstagedAwaited < AcidicJob::Base
1669
+ def perform
1670
+ Performance.performed!
1671
+ end
1672
+ end
1673
+
1674
+ assert_raises ActiveRecord::RecordInvalid do
1675
+ AcidicJob::Run.create!(
1676
+ idempotency_key: "12a345bc-67e8-90f1-23g4-5h6i7jk8l901",
1677
+ serialized_job: {
1678
+ "job_class" => "TestCases::JobNonWorkflowUnstagedAwaited",
1679
+ "job_id" => "12a345bc-67e8-90f1-23g4-5h6i7jk8l901",
1680
+ "provider_job_id" => nil,
1681
+ "queue_name" => "default",
1682
+ "priority" => nil,
1683
+ "arguments" => [],
1684
+ "executions" => 1,
1685
+ "exception_executions" => {},
1686
+ "locale" => "en",
1687
+ "timezone" => "UTC",
1688
+ "enqueued_at" => ""
1689
+ },
1690
+ job_class: "TestCases::JobNonWorkflowUnstagedAwaited",
1691
+ staged: false,
1692
+ last_run_at: Time.current,
1693
+ recovery_point: nil,
1694
+ workflow: nil,
1695
+ awaited_by: AcidicJob::Run.create!(
1696
+ idempotency_key: "67b823ea-34f0-40a0-88d9-7e3b7ff9e769",
1697
+ serialized_job: {
1698
+ "job_class" => "TestCases::AwaitingJob",
1699
+ "job_id" => "67b823ea-34f0-40a0-88d9-7e3b7ff9e769",
1700
+ "provider_job_id" => nil,
1701
+ "queue_name" => "default",
1702
+ "priority" => nil,
1703
+ "arguments" => [],
1704
+ "executions" => 1,
1705
+ "exception_executions" => {},
1706
+ "locale" => "en",
1707
+ "timezone" => "UTC",
1708
+ "enqueued_at" => ""
1709
+ },
1710
+ job_class: "TestCases::AwaitingJob",
1711
+ staged: false
1712
+ )
1713
+ )
1714
+ end
1715
+ end
1716
+
1717
+ test "non-workflow, staged, unawaited job successfully performs with `Run` record" do
1718
+ class NowJobNonWorkflowStagedUnawaited < AcidicJob::Base
1719
+ def perform
1720
+ Performance.performed!
1721
+ end
1722
+ end
1723
+
1724
+ perform_enqueued_jobs do
1725
+ NowJobNonWorkflowStagedUnawaited.perform_acidicly
1726
+ end
1727
+
1728
+ assert_equal 1, AcidicJob::Run.count
1729
+ assert_equal 1, Performance.performances
1730
+
1731
+ run = AcidicJob::Run.find_by(job_class: "TestCases::NowJobNonWorkflowStagedUnawaited")
1732
+ assert_nil run.recovery_point
1733
+ assert_nil run.error_object
1734
+ assert_equal false, run.workflow?
1735
+ assert_equal true, run.staged?
1736
+ assert_equal false, run.awaited?
1737
+ end
1738
+
1739
+ test "non-workflow, staged, awaited job successfully perfoms with 2 `Run` records" do
1740
+ class JobNonWorkflowStagedAwaited < AcidicJob::Base
1741
+ def perform
1742
+ Performance.performed!
1743
+ end
1744
+ end
1745
+
1746
+ class AwaitingJob < AcidicJob::Base
1747
+ def perform
1748
+ with_acidic_workflow do |workflow|
1749
+ workflow.step :await_step, awaits: [JobNonWorkflowStagedAwaited]
1750
+ end
1751
+ end
1752
+ end
1753
+
1754
+ perform_enqueued_jobs do
1755
+ AwaitingJob.perform_now
1756
+ end
1757
+
1758
+ assert_equal 2, AcidicJob::Run.count
1759
+ assert_equal 1, Performance.performances
1760
+
1761
+ parent_run = AcidicJob::Run.find_by(job_class: "TestCases::AwaitingJob")
1762
+ assert_equal "FINISHED", parent_run.recovery_point
1763
+ assert_equal true, parent_run.workflow?
1764
+ assert_equal false, parent_run.staged?
1765
+ assert_equal false, parent_run.awaited?
1766
+
1767
+ child_run = AcidicJob::Run.find_by(job_class: "TestCases::JobNonWorkflowStagedAwaited")
1768
+ assert_nil child_run.recovery_point
1769
+ assert_equal false, child_run.workflow?
1770
+ assert_equal true, child_run.staged?
1771
+ assert_equal true, child_run.awaited?
1772
+ end
1773
+
1774
+ test "workflow, unstaged, unawaited job successfully performs with `Run` record" do
1775
+ class JobWorkflowUnstagedUnawaited < AcidicJob::Base
1776
+ def perform
1777
+ with_acidic_workflow do |workflow|
1778
+ workflow.step :do_something
1779
+ end
1780
+ end
1781
+
1782
+ def do_something
1783
+ Performance.performed!
1784
+ end
1785
+ end
1786
+
1787
+ JobWorkflowUnstagedUnawaited.perform_now
1788
+
1789
+ assert_equal 1, AcidicJob::Run.count
1790
+
1791
+ run = AcidicJob::Run.find_by(job_class: "TestCases::JobWorkflowUnstagedUnawaited")
1792
+ assert_equal "FINISHED", run.recovery_point
1793
+ assert_nil run.error_object
1794
+ assert_equal true, run.workflow?
1795
+ assert_equal false, run.staged?
1796
+ assert_equal false, run.awaited?
1797
+
1798
+ assert_equal 1, Performance.performances
1799
+ end
1800
+
1801
+ test "workflow, unstaged, awaited job is invalid" do
1802
+ class AwaitingJob < AcidicJob::Base; end
1803
+
1804
+ class JobWorkflowUnstagedAwaited < AcidicJob::Base
1805
+ def perform
1806
+ Performance.performed!
1807
+ end
1808
+ end
1809
+
1810
+ assert_raises ActiveRecord::RecordInvalid do
1811
+ AcidicJob::Run.create!(
1812
+ idempotency_key: "12a345bc-67e8-90f1-23g4-5h6i7jk8l901",
1813
+ serialized_job: {
1814
+ "job_class" => "TestCases::JobWorkflowUnstagedAwaited",
1815
+ "job_id" => "12a345bc-67e8-90f1-23g4-5h6i7jk8l901",
1816
+ "provider_job_id" => nil,
1817
+ "queue_name" => "default",
1818
+ "priority" => nil,
1819
+ "arguments" => [],
1820
+ "executions" => 1,
1821
+ "exception_executions" => {},
1822
+ "locale" => "en",
1823
+ "timezone" => "UTC",
1824
+ "enqueued_at" => ""
1825
+ },
1826
+ job_class: "TestCases::JobWorkflowUnstagedAwaited",
1827
+ staged: false,
1828
+ last_run_at: Time.current,
1829
+ recovery_point: nil,
1830
+ workflow: nil,
1831
+ awaited_by: AcidicJob::Run.create!(
1832
+ idempotency_key: "67b823ea-34f0-40a0-88d9-7e3b7ff9e769",
1833
+ serialized_job: {
1834
+ "job_class" => "TestCases::AwaitingJob",
1835
+ "job_id" => "67b823ea-34f0-40a0-88d9-7e3b7ff9e769",
1836
+ "provider_job_id" => nil,
1837
+ "queue_name" => "default",
1838
+ "priority" => nil,
1839
+ "arguments" => [],
1840
+ "executions" => 1,
1841
+ "exception_executions" => {},
1842
+ "locale" => "en",
1843
+ "timezone" => "UTC",
1844
+ "enqueued_at" => ""
1845
+ },
1846
+ job_class: "TestCases::AwaitingJob",
1847
+ staged: false
1848
+ )
1849
+ )
1850
+ end
1851
+ end
1852
+
1853
+ test "workflow, staged, unawaited job successfully performs with `Run` record" do
1854
+ class JobWorkflowStagedUnawaited < AcidicJob::Base
1855
+ def perform
1856
+ with_acidic_workflow do |workflow|
1857
+ workflow.step :do_something
1858
+ end
1859
+ end
1860
+
1861
+ def do_something
1862
+ Performance.performed!
1863
+ end
1864
+ end
1865
+
1866
+ perform_enqueued_jobs do
1867
+ JobWorkflowStagedUnawaited.perform_acidicly
1868
+ end
1869
+
1870
+ assert_equal 1, AcidicJob::Run.count
1871
+
1872
+ run = AcidicJob::Run.find_by(job_class: "TestCases::JobWorkflowStagedUnawaited")
1873
+ assert_equal "FINISHED", run.recovery_point
1874
+ assert_nil run.error_object
1875
+ assert_equal true, run.workflow?
1876
+ assert_equal true, run.staged?
1877
+ assert_equal false, run.awaited?
1878
+
1879
+ assert_equal 1, Performance.performances
1880
+ end
1881
+
1882
+ test "workflow, staged, awaited job successfully perfoms with 2 `Run` records" do
1883
+ class JobWorkflowStagedAwaited < AcidicJob::Base
1884
+ def perform
1885
+ with_acidic_workflow do |workflow|
1886
+ workflow.step :do_something
1887
+ end
1888
+ end
1889
+
1890
+ def do_something
1891
+ Performance.performed!
1892
+ end
1893
+ end
1894
+
1895
+ class AwaitingJob < AcidicJob::Base
1896
+ def perform
1897
+ with_acidic_workflow do |workflow|
1898
+ workflow.step :await_step, awaits: [JobWorkflowStagedAwaited]
1899
+ end
1900
+ end
1901
+ end
1902
+
1903
+ perform_enqueued_jobs do
1904
+ AwaitingJob.perform_now
1905
+ end
1906
+
1907
+ assert_equal 2, AcidicJob::Run.count
1908
+ assert_equal 1, Performance.performances
1909
+
1910
+ parent_run = AcidicJob::Run.find_by(job_class: "TestCases::AwaitingJob")
1911
+ assert_equal "FINISHED", parent_run.recovery_point
1912
+ assert_equal true, parent_run.workflow?
1913
+ assert_equal false, parent_run.staged?
1914
+ assert_equal false, parent_run.awaited?
1915
+
1916
+ child_run = AcidicJob::Run.find_by(job_class: "TestCases::JobWorkflowStagedAwaited")
1917
+ assert_equal "FINISHED", child_run.recovery_point
1918
+ assert_equal true, child_run.workflow?
1919
+ assert_equal true, child_run.staged?
1920
+ assert_equal true, child_run.awaited?
1921
+ end
1922
+ end
1923
+ # rubocop:enable Lint/ConstantDefinitionInBlock
1924
+
1925
+ Minitest.run
1926
+
1927
+ # rubocop:disable Metrics/ParameterLists
1928
+ class SerializableJob < AcidicJob::Base
1929
+ self.queue_name_prefix = :prefix
1930
+ self.queue_name_delimiter = "."
1931
+ queue_as :some_queue
1932
+ queue_with_priority 50
1933
+
1934
+ def perform(required_positional,
1935
+ optional_positional = "OPTIONAL POSITIONAL",
1936
+ *splat_args,
1937
+ required_keyword:,
1938
+ optional_keyword: "OPTIONAL KEYWORD",
1939
+ **double_splat_kwargs)
1940
+ # no-op
1941
+ end
1942
+ end
1943
+ # rubocop:enable Metrics/ParameterLists
1944
+ # <SerializableJob:0x00000001092acd08
1945
+ # @arguments=["positional", {:required_keyword=>"required"}],
1946
+ # @exception_executions={},
1947
+ # @executions=1,
1948
+ # @job_id="6237fa38-eb6e-4f41-8ea6-be80854b5add",
1949
+ # @priority=50,
1950
+ # @queue_name="prefix.some_queue",
1951
+ # @timezone="UTC">
1952
+
1953
+ # (If you use this, don't forget to add pry to your Gemfile!)
1954
+ # require "pry"
1955
+ # Pry.start
1956
+
1957
+ require "irb"
1958
+ IRB.start(__FILE__)