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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +3 -2
- data/.gitignore +1 -1
- data/Gemfile.lock +4 -10
- data/README.md +101 -102
- data/acidic_job.gemspec +0 -2
- data/gemfiles/{rails_6.1.gemfile → rails_6.1_sidekiq_6.4.gemfile} +2 -0
- data/gemfiles/rails_6.1_sidekiq_6.5.gemfile +10 -0
- data/gemfiles/{rails_7.0.gemfile → rails_7.0_sidekiq_6.4.gemfile} +2 -0
- data/gemfiles/rails_7.0_sidekiq_6.5.gemfile +10 -0
- data/gemfiles/rails_7.1_sidekiq_6.4.gemfile +10 -0
- data/gemfiles/rails_7.1_sidekiq_6.5.gemfile +10 -0
- data/lib/acidic_job/active_kiq.rb +2 -2
- data/lib/acidic_job/extensions/action_mailer.rb +19 -0
- data/lib/acidic_job/extensions/noticed.rb +46 -0
- data/lib/acidic_job/mixin.rb +6 -0
- data/lib/acidic_job/perform_wrapper.rb +3 -3
- data/lib/acidic_job/run.rb +1 -1
- data/lib/acidic_job/testing.rb +73 -0
- data/lib/acidic_job/version.rb +1 -1
- data/lib/acidic_job.rb +2 -0
- metadata +11 -33
- data/bin/sandbox +0 -1958
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__)
|