acidic_job 1.0.0.beta.10 → 1.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +12 -36
- data/.gitignore +0 -5
- data/.ruby_version +1 -0
- data/Gemfile +31 -0
- data/Gemfile.lock +130 -136
- data/README.md +58 -278
- data/acidic_job.gemspec +2 -15
- data/bin/console +2 -4
- data/lib/acidic_job/awaiting.rb +68 -0
- data/lib/acidic_job/errors.rb +19 -11
- data/lib/acidic_job/extensions/action_mailer.rb +11 -3
- data/lib/acidic_job/extensions/active_job.rb +39 -0
- data/lib/acidic_job/extensions/noticed.rb +11 -5
- data/lib/acidic_job/extensions/sidekiq.rb +101 -0
- data/lib/acidic_job/finished_point.rb +5 -3
- data/lib/acidic_job/idempotency_key.rb +15 -18
- data/lib/acidic_job/perform_wrapper.rb +36 -9
- data/lib/acidic_job/recovery_point.rb +3 -2
- data/lib/acidic_job/run.rb +42 -268
- data/lib/acidic_job/staging.rb +30 -0
- data/lib/acidic_job/step.rb +83 -0
- data/lib/acidic_job/version.rb +1 -1
- data/lib/acidic_job.rb +244 -20
- data/lib/generators/acidic_job_generator.rb +35 -0
- data/lib/generators/templates/create_acidic_job_runs_migration.rb.erb +19 -0
- metadata +15 -209
- data/.github/FUNDING.yml +0 -13
- data/.tool-versions +0 -1
- data/UPGRADE_GUIDE.md +0 -81
- data/combustion/log/test.log +0 -0
- data/gemfiles/rails_6.1_sidekiq_6.4.gemfile +0 -10
- data/gemfiles/rails_6.1_sidekiq_6.5.gemfile +0 -10
- data/gemfiles/rails_7.0_sidekiq_6.4.gemfile +0 -10
- data/gemfiles/rails_7.0_sidekiq_6.5.gemfile +0 -10
- data/gemfiles/rails_7.1_sidekiq_6.4.gemfile +0 -10
- data/gemfiles/rails_7.1_sidekiq_6.5.gemfile +0 -10
- data/lib/acidic_job/active_kiq.rb +0 -114
- data/lib/acidic_job/arguments.rb +0 -22
- data/lib/acidic_job/base.rb +0 -11
- data/lib/acidic_job/logger.rb +0 -31
- data/lib/acidic_job/mixin.rb +0 -250
- data/lib/acidic_job/processor.rb +0 -95
- data/lib/acidic_job/rails.rb +0 -40
- data/lib/acidic_job/serializer.rb +0 -24
- data/lib/acidic_job/serializers/exception_serializer.rb +0 -41
- data/lib/acidic_job/serializers/finished_point_serializer.rb +0 -24
- data/lib/acidic_job/serializers/job_serializer.rb +0 -27
- data/lib/acidic_job/serializers/range_serializer.rb +0 -28
- data/lib/acidic_job/serializers/recovery_point_serializer.rb +0 -25
- data/lib/acidic_job/serializers/worker_serializer.rb +0 -27
- data/lib/acidic_job/test_case.rb +0 -9
- data/lib/acidic_job/testing.rb +0 -73
- data/lib/acidic_job/workflow.rb +0 -70
- data/lib/acidic_job/workflow_builder.rb +0 -35
- data/lib/acidic_job/workflow_step.rb +0 -103
- data/lib/generators/acidic_job/drop_tables_generator.rb +0 -26
- data/lib/generators/acidic_job/install_generator.rb +0 -27
- data/lib/generators/acidic_job/templates/create_acidic_job_runs_migration.rb.erb +0 -19
- data/lib/generators/acidic_job/templates/drop_acidic_job_keys_migration.rb.erb +0 -27
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
module AcidicJob
|
6
|
+
module Extensions
|
7
|
+
module ActiveJob
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
concerning :Serialization do
|
11
|
+
class_methods do
|
12
|
+
def serialize_with_arguments(*args, **kwargs)
|
13
|
+
job_or_instantiate(*args, **kwargs).serialize
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def serialize_job(*_args, **_kwargs)
|
18
|
+
serialize
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class_methods do
|
23
|
+
def perform_acidicly(*args, **kwargs)
|
24
|
+
raise UnsupportedExtension unless defined?(::ActiveJob) && self < ::ActiveJob::Base
|
25
|
+
|
26
|
+
serialized_job = serialize_with_arguments(*args, **kwargs)
|
27
|
+
|
28
|
+
AcidicJob::Run.create!(
|
29
|
+
staged: true,
|
30
|
+
job_class: name,
|
31
|
+
serialized_job: serialized_job,
|
32
|
+
idempotency_key: IdempotencyKey.value_for(serialized_job)
|
33
|
+
)
|
34
|
+
end
|
35
|
+
alias_method :perform_transactionally, :perform_acidicly
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -11,10 +11,10 @@ module AcidicJob
|
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
-
# THIS IS A HACK THAT COPIES AND PASTES KEY PARTS OF THE `Noticed::Base` CODE
|
15
|
-
# IN ORDER TO ALLOW US TO TRANSACTIONALLY DELIVER NOTIFICATIONS
|
16
|
-
# THIS IS THUS LIABLE TO BREAK WHENEVER THAT GEM IS UPDATED
|
17
14
|
def deliver_acidicly(recipients)
|
15
|
+
# THIS IS A HACK THAT COPIES AND PASTES KEY PARTS OF THE `Noticed::Base` CODE
|
16
|
+
# IN ORDER TO ALLOW US TO TRANSACTIONALLY DELIVER NOTIFICATIONS
|
17
|
+
# THIS IS THUS LIABLE TO BREAK WHENEVER THAT GEM IS UPDATED
|
18
18
|
delivery_methods = self.class.delivery_methods.dup
|
19
19
|
|
20
20
|
Array.wrap(recipients).uniq.each do |recipient|
|
@@ -35,12 +35,18 @@ module AcidicJob
|
|
35
35
|
recipient: recipient,
|
36
36
|
record: record
|
37
37
|
}
|
38
|
-
|
38
|
+
serialized_job = job_class.send(:job_or_instantiate, args).serialize
|
39
39
|
|
40
|
-
AcidicJob::Run.
|
40
|
+
AcidicJob::Run.create!(
|
41
|
+
staged: true,
|
42
|
+
job_class: job_class.name,
|
43
|
+
serialized_job: serialized_job,
|
44
|
+
idempotency_key: IdempotencyKey.value_for(serialized_job)
|
45
|
+
)
|
41
46
|
end
|
42
47
|
end
|
43
48
|
end
|
49
|
+
alias deliver_transactionally deliver_acidicly
|
44
50
|
end
|
45
51
|
end
|
46
52
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
require "active_support/callbacks"
|
5
|
+
require "active_support/core_ext/module/concerning"
|
6
|
+
|
7
|
+
module AcidicJob
|
8
|
+
module Extensions
|
9
|
+
module Sidekiq
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
concerning :Serialization do
|
13
|
+
class_methods do
|
14
|
+
# called only from `AcidicJob::Run#enqueue_staged_job`
|
15
|
+
def deserialize(serialized_job_hash)
|
16
|
+
klass = serialized_job_hash["class"].constantize
|
17
|
+
worker = klass.new
|
18
|
+
worker.jid = serialized_job_hash["jid"]
|
19
|
+
worker.instance_variable_set(:@args, serialized_job_hash["args"])
|
20
|
+
|
21
|
+
worker
|
22
|
+
end
|
23
|
+
|
24
|
+
# called only from `AcidicJob::PerformAcidicly#perform_acidicly`
|
25
|
+
# and `AcidicJob::DeliverAcidicly#deliver_acidicly`
|
26
|
+
def serialize_with_arguments(args = [], _kwargs = nil)
|
27
|
+
# THIS IS A HACK THAT ESSENTIALLY COPIES THE CODE FROM THE SIDEKIQ CODEBASE TO MIMIC THE BEHAVIOR
|
28
|
+
args = Array[args]
|
29
|
+
normalized_args = ::Sidekiq.load_json(::Sidekiq.dump_json(args))
|
30
|
+
item = { "class" => self, "args" => normalized_args }
|
31
|
+
dummy_sidekiq_client = ::Sidekiq::Client.new
|
32
|
+
normed = dummy_sidekiq_client.send :normalize_item, item
|
33
|
+
dummy_sidekiq_client.send :process_single, item["class"], normed
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def serialize_job(args = [], _kwargs = nil)
|
38
|
+
# `@args` is only set via `deserialize`; it is not a standard Sidekiq thing
|
39
|
+
arguments = args || @args
|
40
|
+
normalized_args = ::Sidekiq.load_json(::Sidekiq.dump_json(arguments))
|
41
|
+
item = { "class" => self.class, "args" => normalized_args, "jid" => jid }
|
42
|
+
sidekiq_options = sidekiq_options_hash || {}
|
43
|
+
|
44
|
+
sidekiq_options.merge(item)
|
45
|
+
end
|
46
|
+
|
47
|
+
# called only from `AcidicJob::Run#enqueue_staged_job`
|
48
|
+
def enqueue
|
49
|
+
::Sidekiq::Client.push(
|
50
|
+
"class" => self.class,
|
51
|
+
"args" => @args,
|
52
|
+
"jid" => @jid
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
concerning :PerformAcidicly do
|
58
|
+
class_methods do
|
59
|
+
def perform_acidicly(*args, **kwargs)
|
60
|
+
serialized_job = serialize_with_arguments(*args, **kwargs)
|
61
|
+
|
62
|
+
AcidicJob::Run.create!(
|
63
|
+
staged: true,
|
64
|
+
job_class: name,
|
65
|
+
serialized_job: serialized_job,
|
66
|
+
idempotency_key: IdempotencyKey.value_for(serialized_job)
|
67
|
+
)
|
68
|
+
end
|
69
|
+
alias_method :perform_transactionally, :perform_acidicly
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# to balance `perform_async` class method
|
74
|
+
concerning :PerformSync do
|
75
|
+
class_methods do
|
76
|
+
def perform_sync(*args, **kwargs)
|
77
|
+
new.perform(*args, **kwargs)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Following approach used by ActiveJob
|
83
|
+
# https://github.com/rails/rails/blob/93c9534c9871d4adad4bc33b5edc355672b59c61/activejob/lib/active_job/callbacks.rb
|
84
|
+
concerning :Callbacks do
|
85
|
+
class_methods do
|
86
|
+
def around_perform(*filters, &blk)
|
87
|
+
set_callback(:perform, :around, *filters, &blk)
|
88
|
+
end
|
89
|
+
|
90
|
+
def before_perform(*filters, &blk)
|
91
|
+
set_callback(:perform, :before, *filters, &blk)
|
92
|
+
end
|
93
|
+
|
94
|
+
def after_perform(*filters, &blk)
|
95
|
+
set_callback(:perform, :after, *filters, &blk)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -1,14 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "run"
|
4
|
-
|
5
3
|
# Represents an action to set a new API response (which will be stored onto an
|
6
4
|
# idempotency key). One possible option for a return from an #atomic_phase
|
7
5
|
# block.
|
8
6
|
module AcidicJob
|
9
7
|
class FinishedPoint
|
10
8
|
def call(run:)
|
11
|
-
|
9
|
+
# Skip AR callbacks as there are none on the model
|
10
|
+
run.update_columns(
|
11
|
+
locked_at: nil,
|
12
|
+
recovery_point: Run::FINISHED_RECOVERY_POINT
|
13
|
+
)
|
12
14
|
end
|
13
15
|
end
|
14
16
|
end
|
@@ -2,26 +2,23 @@
|
|
2
2
|
|
3
3
|
module AcidicJob
|
4
4
|
class IdempotencyKey
|
5
|
-
def
|
6
|
-
|
7
|
-
|
5
|
+
def self.value_for(hash_or_job, *args, **kwargs)
|
6
|
+
return hash_or_job.job_id if hash_or_job.respond_to?(:job_id) && !hash_or_job.job_id.nil?
|
7
|
+
return hash_or_job.jid if hash_or_job.respond_to?(:jid) && !hash_or_job.jid.nil?
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
when Proc
|
12
|
-
proc_result = @job.instance_exec(&acidic_by)
|
13
|
-
Digest::SHA1.hexdigest [@job.class.name, proc_result].flatten.join
|
14
|
-
when :job_arguments
|
15
|
-
Digest::SHA1.hexdigest [@job.class.name, @job.arguments].flatten.join
|
16
|
-
else
|
17
|
-
if @job.job_id.start_with? Run::STAGED_JOB_ID_PREFIX
|
18
|
-
# "STG__#{idempotency_key}__#{encoded_global_id}"
|
19
|
-
_prefix, idempotency_key, _encoded_global_id = @job.job_id.split("__")
|
20
|
-
idempotency_key
|
21
|
-
else
|
22
|
-
@job.job_id
|
23
|
-
end
|
9
|
+
if hash_or_job.is_a?(Hash) && hash_or_job.key?("job_id") && !hash_or_job["job_id"].nil?
|
10
|
+
return hash_or_job["job_id"]
|
24
11
|
end
|
12
|
+
return hash_or_job["jid"] if hash_or_job.is_a?(Hash) && hash_or_job.key?("jid") && !hash_or_job["jid"].nil?
|
13
|
+
|
14
|
+
worker_class = case hash_or_job
|
15
|
+
when Hash
|
16
|
+
hash_or_job["worker"] || hash_or_job["job_class"]
|
17
|
+
else
|
18
|
+
hash_or_job.class.name
|
19
|
+
end
|
20
|
+
|
21
|
+
Digest::SHA1.hexdigest [worker_class, args, kwargs].flatten.join
|
25
22
|
end
|
26
23
|
end
|
27
24
|
end
|
@@ -2,21 +2,48 @@
|
|
2
2
|
|
3
3
|
module AcidicJob
|
4
4
|
# NOTE: it is essential that this be a bare module and not an ActiveSupport::Concern
|
5
|
-
# WHY?
|
6
5
|
module PerformWrapper
|
7
|
-
def perform(*args)
|
8
|
-
|
6
|
+
def perform(*args, **kwargs)
|
7
|
+
super_method = method(:perform).super_method
|
9
8
|
|
10
9
|
# we don't want to run the `perform` callbacks twice, since ActiveJob already handles that for us
|
11
|
-
if
|
12
|
-
|
13
|
-
elsif
|
14
|
-
|
15
|
-
super(*args)
|
16
|
-
end
|
10
|
+
if aj_job?
|
11
|
+
__acidic_job_perform_for_aj(super_method, *args, **kwargs)
|
12
|
+
elsif sk_job?
|
13
|
+
__acidic_job_perform_for_sk(super_method, *args, **kwargs)
|
17
14
|
else
|
18
15
|
raise UnknownJobAdapter
|
19
16
|
end
|
20
17
|
end
|
18
|
+
|
19
|
+
def sk_job?
|
20
|
+
defined?(Sidekiq) && self.class.include?(Sidekiq::Worker)
|
21
|
+
end
|
22
|
+
|
23
|
+
def aj_job?
|
24
|
+
defined?(ActiveJob) && self.class < ActiveJob::Base
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# don't run `perform` callbacks, as ActiveJob already does this
|
30
|
+
def __acidic_job_perform_for_aj(super_method, *args, **kwargs)
|
31
|
+
__acidic_job_perform_base(super_method, *args, **kwargs)
|
32
|
+
end
|
33
|
+
|
34
|
+
# ensure to run `perform` callbacks
|
35
|
+
def __acidic_job_perform_for_sk(super_method, *args, **kwargs)
|
36
|
+
run_callbacks :perform do
|
37
|
+
__acidic_job_perform_base(super_method, *args, **kwargs)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# capture arguments passed to `perform` to be used by AcidicJob later
|
42
|
+
def __acidic_job_perform_base(super_method, *args, **kwargs)
|
43
|
+
@__acidic_job_args = args
|
44
|
+
@__acidic_job_kwargs = kwargs
|
45
|
+
|
46
|
+
super_method.call(*args, **kwargs)
|
47
|
+
end
|
21
48
|
end
|
22
49
|
end
|
@@ -4,14 +4,15 @@
|
|
4
4
|
# return from an #atomic_phase block.
|
5
5
|
module AcidicJob
|
6
6
|
class RecoveryPoint
|
7
|
-
|
7
|
+
attr_accessor :name
|
8
8
|
|
9
9
|
def initialize(name)
|
10
10
|
@name = name
|
11
11
|
end
|
12
12
|
|
13
13
|
def call(run:)
|
14
|
-
|
14
|
+
# Skip AR callbacks as there are none on the model
|
15
|
+
run.update_column(:recovery_point, @name)
|
15
16
|
end
|
16
17
|
end
|
17
18
|
end
|
data/lib/acidic_job/run.rb
CHANGED
@@ -2,302 +2,76 @@
|
|
2
2
|
|
3
3
|
require "active_record"
|
4
4
|
require "global_id"
|
5
|
-
require "base64"
|
6
5
|
require "active_support/core_ext/object/with_options"
|
7
|
-
require "active_support/core_ext/module/concerning"
|
8
|
-
require "active_support/concern"
|
9
6
|
|
10
7
|
module AcidicJob
|
11
8
|
class Run < ActiveRecord::Base
|
12
9
|
include GlobalID::Identification
|
13
10
|
|
14
11
|
FINISHED_RECOVERY_POINT = "FINISHED"
|
15
|
-
STAGED_JOB_ID_PREFIX = "STG"
|
16
|
-
STAGED_JOB_ID_DELIMITER = "__"
|
17
|
-
IDEMPOTENCY_KEY_LOCK_TIMEOUT_SECONDS = 2
|
18
12
|
|
19
13
|
self.table_name = "acidic_job_runs"
|
20
14
|
|
21
|
-
|
22
|
-
validate :not_awaited_but_unstaged
|
15
|
+
after_create_commit :enqueue_staged_job, if: :staged?
|
23
16
|
|
24
|
-
|
25
|
-
|
26
|
-
|
17
|
+
serialize :error_object
|
18
|
+
serialize :serialized_job
|
19
|
+
serialize :workflow
|
20
|
+
store :attr_accessors
|
27
21
|
|
28
|
-
|
22
|
+
validates :staged, inclusion: { in: [true, false] } # uses database default
|
23
|
+
validates :serialized_job, presence: true
|
24
|
+
validates :idempotency_key, presence: true, uniqueness: true
|
25
|
+
validates :job_class, presence: true
|
29
26
|
|
30
|
-
|
27
|
+
scope :staged, -> { where(staged: true) }
|
28
|
+
scope :unstaged, -> { where(staged: false) }
|
29
|
+
scope :finished, -> { where(recovery_point: FINISHED_RECOVERY_POINT) }
|
30
|
+
scope :running, -> { where.not(recovery_point: FINISHED_RECOVERY_POINT) }
|
31
31
|
|
32
|
-
|
33
|
-
|
32
|
+
with_options unless: :staged? do
|
33
|
+
validates :last_run_at, presence: true
|
34
|
+
validates :recovery_point, presence: true
|
35
|
+
validates :workflow, presence: true
|
34
36
|
end
|
35
37
|
|
36
|
-
def
|
37
|
-
|
38
|
+
def finished?
|
39
|
+
recovery_point == FINISHED_RECOVERY_POINT
|
38
40
|
end
|
39
41
|
|
40
|
-
|
41
|
-
|
42
|
-
belongs_to :awaited_by, class_name: "AcidicJob::Run", optional: true
|
43
|
-
has_many :batched_runs, class_name: "AcidicJob::Run", foreign_key: "awaited_by_id"
|
44
|
-
|
45
|
-
scope :awaited, -> { where.not(awaited_by: nil) }
|
46
|
-
scope :unawaited, -> { where(awaited_by: nil) }
|
47
|
-
|
48
|
-
after_update_commit :proceed_with_parent, if: :finished?
|
49
|
-
|
50
|
-
serialize :returning_to, AcidicJob::Serializer
|
51
|
-
end
|
52
|
-
|
53
|
-
class_methods do
|
54
|
-
def await!(job, by:, return_to:)
|
55
|
-
create!(
|
56
|
-
staged: true,
|
57
|
-
awaited_by: by,
|
58
|
-
job_class: job.class.name,
|
59
|
-
serialized_job: job.serialize,
|
60
|
-
idempotency_key: job.idempotency_key
|
61
|
-
)
|
62
|
-
by.update(returning_to: return_to)
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
def awaited?
|
67
|
-
awaited_by.present?
|
68
|
-
end
|
69
|
-
|
70
|
-
private
|
71
|
-
|
72
|
-
def proceed_with_parent
|
73
|
-
return unless finished?
|
74
|
-
return unless awaited_by.present?
|
75
|
-
return if awaited_by.batched_runs.outstanding.any?
|
76
|
-
|
77
|
-
AcidicJob.logger.log_run_event("Proceeding with parent job...", job, self)
|
78
|
-
awaited_by.unlock!
|
79
|
-
awaited_by.proceed
|
80
|
-
AcidicJob.logger.log_run_event("Proceeded with parent job.", job, self)
|
81
|
-
end
|
82
|
-
|
83
|
-
protected
|
84
|
-
|
85
|
-
def proceed
|
86
|
-
# this needs to be explicitly set so that `was_workflow_job?` appropriately returns `true`
|
87
|
-
# TODO: replace this with some way to check the type of the job directly
|
88
|
-
# either via class method or explicit module inclusion
|
89
|
-
job.instance_variable_set(:@acidic_job_run, self)
|
90
|
-
|
91
|
-
workflow = Workflow.new(self, job, returning_to)
|
92
|
-
# TODO: WRITE REGRESSION TESTS FOR PARALLEL JOB FAILING AND RETRYING THE ORIGINAL STEP
|
93
|
-
workflow.progress_to_next_step
|
94
|
-
|
95
|
-
# when a batch of jobs for a step succeeds, we begin processing the `AcidicJob::Run` record again
|
96
|
-
return if finished?
|
97
|
-
|
98
|
-
AcidicJob.logger.log_run_event("Re-enqueuing parent job...", job, self)
|
99
|
-
enqueue_job
|
100
|
-
AcidicJob.logger.log_run_event("Re-enqueued parent job.", job, self)
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
concerning :Stageable do
|
105
|
-
included do
|
106
|
-
after_create_commit :enqueue_job, if: :staged?
|
107
|
-
|
108
|
-
validates :staged, inclusion: { in: [true, false] } # uses database default
|
109
|
-
|
110
|
-
scope :staged, -> { where(staged: true) }
|
111
|
-
scope :unstaged, -> { where(staged: false) }
|
112
|
-
end
|
113
|
-
|
114
|
-
class_methods do
|
115
|
-
def stage!(job)
|
116
|
-
create!(
|
117
|
-
staged: true,
|
118
|
-
job_class: job.class.name,
|
119
|
-
serialized_job: job.serialize,
|
120
|
-
idempotency_key: job.try(:idempotency_key) || job.job_id
|
121
|
-
)
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
private
|
126
|
-
|
127
|
-
def job_id
|
128
|
-
return idempotency_key unless staged?
|
129
|
-
|
130
|
-
# encode the identifier for this record in the job ID
|
131
|
-
global_id = to_global_id.to_s.remove("gid://")
|
132
|
-
# base64 encoding for minimal security
|
133
|
-
encoded_global_id = Base64.urlsafe_encode64(global_id, padding: false)
|
134
|
-
|
135
|
-
[
|
136
|
-
STAGED_JOB_ID_PREFIX,
|
137
|
-
idempotency_key,
|
138
|
-
encoded_global_id
|
139
|
-
].join(STAGED_JOB_ID_DELIMITER)
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
concerning :Workflowable do
|
144
|
-
included do
|
145
|
-
serialize :workflow, AcidicJob::Serializer
|
146
|
-
serialize :error_object, AcidicJob::Serializer
|
147
|
-
store :attr_accessors, coder: AcidicJob::Serializer
|
148
|
-
|
149
|
-
with_options unless: :staged? do
|
150
|
-
validates :last_run_at, presence: true
|
151
|
-
validates :recovery_point, presence: true
|
152
|
-
validates :workflow, presence: true
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
def workflow?
|
157
|
-
self[:workflow].present?
|
158
|
-
end
|
159
|
-
|
160
|
-
def attr_accessors
|
161
|
-
self[:attr_accessors] || {}
|
162
|
-
end
|
163
|
-
|
164
|
-
def current_step_name
|
165
|
-
recovery_point
|
166
|
-
end
|
167
|
-
|
168
|
-
def current_step_hash
|
169
|
-
workflow[current_step_name]
|
170
|
-
end
|
171
|
-
|
172
|
-
def next_step_name
|
173
|
-
current_step_hash.fetch("then")
|
174
|
-
end
|
175
|
-
|
176
|
-
def current_step_awaits
|
177
|
-
current_step_hash["awaits"]
|
178
|
-
end
|
179
|
-
|
180
|
-
def next_step_finishes?
|
181
|
-
next_step_name.to_s == FINISHED_RECOVERY_POINT
|
182
|
-
end
|
183
|
-
|
184
|
-
def current_step_finished?
|
185
|
-
current_step_name.to_s == FINISHED_RECOVERY_POINT
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
concerning :Jobbable do
|
190
|
-
included do
|
191
|
-
serialize :serialized_job, JSON
|
192
|
-
|
193
|
-
validates :serialized_job, presence: true
|
194
|
-
validates :job_class, presence: true
|
195
|
-
end
|
196
|
-
|
197
|
-
def job
|
198
|
-
return @job if defined? @job
|
199
|
-
|
200
|
-
serialized_job_for_run = serialized_job.merge("job_id" => job_id)
|
201
|
-
job_class_for_run = job_class.constantize
|
202
|
-
|
203
|
-
@job = job_class_for_run.deserialize(serialized_job_for_run)
|
204
|
-
end
|
205
|
-
|
206
|
-
def enqueue_job
|
207
|
-
job.enqueue
|
208
|
-
|
209
|
-
# NOTE: record will be deleted after the job has successfully been performed
|
210
|
-
true
|
211
|
-
end
|
212
|
-
end
|
213
|
-
|
214
|
-
concerning :Finishable do
|
215
|
-
included do
|
216
|
-
scope :finished, -> { where(recovery_point: FINISHED_RECOVERY_POINT) }
|
217
|
-
scope :outstanding, lambda {
|
218
|
-
where.not(recovery_point: FINISHED_RECOVERY_POINT).or(where(recovery_point: [nil, ""]))
|
219
|
-
}
|
220
|
-
end
|
221
|
-
|
222
|
-
def finish!
|
223
|
-
finish and unlock and save!
|
224
|
-
end
|
225
|
-
|
226
|
-
def finish
|
227
|
-
self.recovery_point = FINISHED_RECOVERY_POINT
|
228
|
-
self
|
229
|
-
end
|
230
|
-
|
231
|
-
def finished?
|
232
|
-
recovery_point.to_s == FINISHED_RECOVERY_POINT
|
233
|
-
end
|
42
|
+
def succeeded?
|
43
|
+
finished? && !failed?
|
234
44
|
end
|
235
45
|
|
236
|
-
|
237
|
-
|
238
|
-
scope :unlocked, -> { where(locked_at: nil) }
|
239
|
-
scope :locked, -> { where.not(locked_at: nil) }
|
240
|
-
end
|
241
|
-
|
242
|
-
def unlock!
|
243
|
-
unlock and save!
|
244
|
-
end
|
245
|
-
|
246
|
-
def unlock
|
247
|
-
self.locked_at = nil
|
248
|
-
self
|
249
|
-
end
|
250
|
-
|
251
|
-
def locked?
|
252
|
-
locked_at.present?
|
253
|
-
end
|
254
|
-
|
255
|
-
def lock_active?
|
256
|
-
return false if locked_at.nil?
|
257
|
-
|
258
|
-
locked_at > Time.current - IDEMPOTENCY_KEY_LOCK_TIMEOUT_SECONDS
|
259
|
-
end
|
46
|
+
def failed?
|
47
|
+
error_object.present?
|
260
48
|
end
|
261
49
|
|
262
|
-
|
263
|
-
included do
|
264
|
-
scope :unerrored, -> { where(error_object: nil) }
|
265
|
-
scope :errored, -> { where.not(error_object: nil) }
|
266
|
-
end
|
50
|
+
private
|
267
51
|
|
268
|
-
|
269
|
-
|
270
|
-
end
|
52
|
+
def enqueue_staged_job
|
53
|
+
return unless staged?
|
271
54
|
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
55
|
+
# encode the identifier for this record in the job ID
|
56
|
+
# base64 encoding for minimal security
|
57
|
+
global_id = to_global_id.to_s.remove("gid://")
|
58
|
+
encoded_global_id = Base64.encode64(global_id).strip
|
59
|
+
staged_job_id = "STG_#{idempotency_key}__#{encoded_global_id}"
|
276
60
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
61
|
+
serialized_staged_job = if serialized_job.key?("jid")
|
62
|
+
serialized_job.merge("jid" => staged_job_id)
|
63
|
+
elsif serialized_job.key?("job_id")
|
64
|
+
serialized_job.merge("job_id" => staged_job_id)
|
65
|
+
else
|
66
|
+
raise UnknownSerializedJobIdentifier
|
67
|
+
end
|
281
68
|
|
282
|
-
|
283
|
-
def recover_to!(point)
|
284
|
-
recover_to(point) and save!
|
285
|
-
end
|
286
|
-
|
287
|
-
def recover_to(point)
|
288
|
-
self.recovery_point = point
|
289
|
-
self
|
290
|
-
end
|
291
|
-
|
292
|
-
def known_recovery_point?
|
293
|
-
workflow.key?(recovery_point)
|
294
|
-
end
|
295
|
-
end
|
69
|
+
job = job_class.constantize.deserialize(serialized_staged_job)
|
296
70
|
|
297
|
-
|
298
|
-
return true unless awaited? && !staged?
|
71
|
+
job.enqueue
|
299
72
|
|
300
|
-
|
73
|
+
# NOTE: record will be deleted after the job has successfully been performed
|
74
|
+
true
|
301
75
|
end
|
302
76
|
end
|
303
77
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
module AcidicJob
|
6
|
+
module Staging
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
def delete_staged_job_record
|
10
|
+
return unless was_staged_job?
|
11
|
+
|
12
|
+
staged_job_run.delete
|
13
|
+
true
|
14
|
+
rescue ActiveRecord::RecordNotFound
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
def was_staged_job?
|
19
|
+
identifier.start_with? "STG_"
|
20
|
+
end
|
21
|
+
|
22
|
+
def staged_job_run
|
23
|
+
# "STG_#{idempotency_key}__#{encoded_global_id}"
|
24
|
+
encoded_global_id = identifier.split("__").last
|
25
|
+
staged_job_gid = "gid://#{Base64.decode64(encoded_global_id)}"
|
26
|
+
|
27
|
+
GlobalID::Locator.locate(staged_job_gid)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|