acidic_job 1.0.0.beta.1 → 1.0.0.beta.2

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.
data/bin/sandbox DELETED
@@ -1,1958 +0,0 @@
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__)