acidic_job 1.0.0.beta.1 → 1.0.0.beta.4

Sign up to get free protection for your applications and to get access to all the features.
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__)