acidic_job 1.0.0.pre29 → 1.0.0.rc2
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/.codacy.yml +4 -0
- data/.github/FUNDING.yml +13 -0
- data/.github/workflows/main.yml +12 -15
- data/.gitignore +3 -1
- data/.rubocop.yml +50 -5
- data/.ruby-version +1 -0
- data/Gemfile.lock +134 -193
- data/README.md +164 -246
- data/TODO +77 -0
- data/acidic_job.gemspec +10 -10
- data/app/models/acidic_job/entry.rb +19 -0
- data/app/models/acidic_job/execution.rb +50 -0
- data/app/models/acidic_job/record.rb +11 -0
- data/app/models/acidic_job/value.rb +7 -0
- data/bin/console +5 -2
- data/bin/test_all +26 -0
- data/gemfiles/rails_7.0.gemfile +4 -1
- data/gemfiles/rails_7.1.gemfile +11 -0
- data/gemfiles/rails_7.2.gemfile +11 -0
- data/gemfiles/rails_8.0.gemfile +11 -0
- data/lib/acidic_job/arguments.rb +31 -0
- data/lib/acidic_job/builder.rb +29 -0
- data/lib/acidic_job/context.rb +46 -0
- data/lib/acidic_job/engine.rb +46 -0
- data/lib/acidic_job/errors.rb +87 -12
- data/lib/acidic_job/log_subscriber.rb +50 -0
- data/lib/acidic_job/serializers/exception_serializer.rb +31 -0
- data/lib/acidic_job/serializers/job_serializer.rb +27 -0
- data/lib/acidic_job/serializers/new_record_serializer.rb +25 -0
- data/lib/acidic_job/serializers/range_serializer.rb +28 -0
- data/lib/acidic_job/testing.rb +8 -12
- data/lib/acidic_job/version.rb +1 -1
- data/lib/acidic_job/workflow.rb +182 -0
- data/lib/acidic_job.rb +15 -284
- data/lib/generators/acidic_job/install_generator.rb +3 -3
- data/lib/generators/acidic_job/templates/create_acidic_job_tables_migration.rb.erb +33 -0
- metadata +51 -95
- data/.ruby_version +0 -1
- data/.tool-versions +0 -1
- data/gemfiles/rails_6.1.gemfile +0 -8
- data/lib/acidic_job/awaiting.rb +0 -102
- data/lib/acidic_job/extensions/action_mailer.rb +0 -29
- data/lib/acidic_job/extensions/active_job.rb +0 -40
- data/lib/acidic_job/extensions/noticed.rb +0 -54
- data/lib/acidic_job/extensions/sidekiq.rb +0 -111
- data/lib/acidic_job/finished_point.rb +0 -16
- data/lib/acidic_job/idempotency_key.rb +0 -82
- data/lib/acidic_job/perform_wrapper.rb +0 -22
- data/lib/acidic_job/recovery_point.rb +0 -18
- data/lib/acidic_job/rspec_configuration.rb +0 -31
- data/lib/acidic_job/run.rb +0 -100
- data/lib/acidic_job/serializer.rb +0 -163
- data/lib/acidic_job/staging.rb +0 -38
- data/lib/acidic_job/step.rb +0 -104
- data/lib/acidic_job/test_case.rb +0 -9
- data/lib/acidic_job/upgrade_service.rb +0 -118
- data/lib/generators/acidic_job/drop_tables_generator.rb +0 -26
- 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
@@ -1,82 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module AcidicJob
|
4
|
-
class IdempotencyKey
|
5
|
-
def initialize(identifier = :job_id)
|
6
|
-
@identifier = identifier
|
7
|
-
end
|
8
|
-
|
9
|
-
def value_for(hash_or_job, *args, **kwargs)
|
10
|
-
value = case @identifier
|
11
|
-
when Proc
|
12
|
-
value_from_proc(hash_or_job, *args, **kwargs)
|
13
|
-
when :job_args
|
14
|
-
value_from_job_args(hash_or_job, *args, **kwargs)
|
15
|
-
else
|
16
|
-
if hash_or_job.is_a?(Hash)
|
17
|
-
value_from_job_id_for_hash(hash_or_job)
|
18
|
-
else
|
19
|
-
value_from_job_id_for_obj(hash_or_job)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
result = value || value_from_job_args(hash_or_job, *args, **kwargs)
|
24
|
-
|
25
|
-
if result.start_with?("STG__")
|
26
|
-
# "STG__#{idempotency_key}__#{encoded_global_id}"
|
27
|
-
_prefix, idempotency_key, _encoded_global_id = result.split("__")
|
28
|
-
idempotency_key
|
29
|
-
else
|
30
|
-
result
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
def value_from_job_id_for_hash(hash)
|
37
|
-
if hash.key?("job_id")
|
38
|
-
return if hash["job_id"].nil?
|
39
|
-
return if hash["job_id"].empty?
|
40
|
-
|
41
|
-
hash["job_id"]
|
42
|
-
elsif hash.key?("jid")
|
43
|
-
return if hash["jid"].nil?
|
44
|
-
return if hash["jid"].empty?
|
45
|
-
|
46
|
-
hash["jid"]
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def value_from_job_id_for_obj(obj)
|
51
|
-
if obj.respond_to?(:job_id)
|
52
|
-
return if obj.job_id.nil?
|
53
|
-
return if obj.job_id.empty?
|
54
|
-
|
55
|
-
obj.job_id
|
56
|
-
elsif obj.respond_to?(:jid)
|
57
|
-
return if obj.jid.nil?
|
58
|
-
return if obj.jid.empty?
|
59
|
-
|
60
|
-
obj.jid
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def value_from_job_args(hash_or_job, *args, **kwargs)
|
65
|
-
worker_class = case hash_or_job
|
66
|
-
when Hash
|
67
|
-
hash_or_job["worker"] || hash_or_job["job_class"]
|
68
|
-
else
|
69
|
-
hash_or_job.class.name
|
70
|
-
end
|
71
|
-
|
72
|
-
Digest::SHA1.hexdigest [worker_class, args, kwargs].flatten.join
|
73
|
-
end
|
74
|
-
|
75
|
-
def value_from_proc(_hash_or_job, *args, **kwargs)
|
76
|
-
return if args.empty? && kwargs.empty?
|
77
|
-
|
78
|
-
idempotency_args = Array(@identifier.call(*args, **kwargs))
|
79
|
-
Digest::SHA1.hexdigest idempotency_args.flatten.join
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
@@ -1,22 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module AcidicJob
|
4
|
-
# NOTE: it is essential that this be a bare module and not an ActiveSupport::Concern
|
5
|
-
module PerformWrapper
|
6
|
-
def perform(*args, **kwargs)
|
7
|
-
@__acidic_job_args = args
|
8
|
-
@__acidic_job_kwargs = kwargs
|
9
|
-
|
10
|
-
# we don't want to run the `perform` callbacks twice, since ActiveJob already handles that for us
|
11
|
-
if defined?(ActiveJob) && self.class < ActiveJob::Base
|
12
|
-
super(*args, **kwargs)
|
13
|
-
elsif defined?(Sidekiq) && self.class.include?(Sidekiq::Worker)
|
14
|
-
run_callbacks :perform do
|
15
|
-
super(*args, **kwargs)
|
16
|
-
end
|
17
|
-
else
|
18
|
-
raise UnknownJobAdapter
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
@@ -1,18 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Represents an action to set a new recovery point. One possible option for a
|
4
|
-
# return from an #atomic_phase block.
|
5
|
-
module AcidicJob
|
6
|
-
class RecoveryPoint
|
7
|
-
attr_accessor :name
|
8
|
-
|
9
|
-
def initialize(name)
|
10
|
-
@name = name
|
11
|
-
end
|
12
|
-
|
13
|
-
def call(run:)
|
14
|
-
# Skip AR callbacks as there are none on the model
|
15
|
-
run.update_column(:recovery_point, @name)
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
@@ -1,31 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "rspec"
|
4
|
-
require "database_cleaner/active_record"
|
5
|
-
|
6
|
-
# see https://github.com/DatabaseCleaner/database_cleaner#how-to-use
|
7
|
-
RSpec.configure do |config|
|
8
|
-
config.use_transactional_fixtures = false
|
9
|
-
|
10
|
-
config.before(:suite) do
|
11
|
-
DatabaseCleaner.clean_with :truncation
|
12
|
-
|
13
|
-
# Here we are defaulting to :transaction but swapping to deletion for some specs;
|
14
|
-
# if your spec or its code-under-test uses
|
15
|
-
# nested transactions then specify :transactional e.g.:
|
16
|
-
# describe "SomeWorker", :transactional do
|
17
|
-
#
|
18
|
-
DatabaseCleaner.strategy = :transaction
|
19
|
-
|
20
|
-
config.before(:context, transactional: true) { DatabaseCleaner.strategy = :deletion }
|
21
|
-
config.after(:context, transactional: true) { DatabaseCleaner.strategy = :transaction }
|
22
|
-
config.before(:context, type: :system) { DatabaseCleaner.strategy = :deletion }
|
23
|
-
config.after(:context, type: :system) { DatabaseCleaner.strategy = :transaction }
|
24
|
-
end
|
25
|
-
|
26
|
-
config.around(:each) do |example|
|
27
|
-
DatabaseCleaner.cleaning do
|
28
|
-
example.run
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
data/lib/acidic_job/run.rb
DELETED
@@ -1,100 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "active_record"
|
4
|
-
require "global_id"
|
5
|
-
require "active_support/core_ext/object/with_options"
|
6
|
-
require_relative "./serializer"
|
7
|
-
|
8
|
-
module AcidicJob
|
9
|
-
class Run < ActiveRecord::Base
|
10
|
-
include GlobalID::Identification
|
11
|
-
|
12
|
-
FINISHED_RECOVERY_POINT = "FINISHED"
|
13
|
-
|
14
|
-
self.table_name = "acidic_job_runs"
|
15
|
-
|
16
|
-
belongs_to :awaited_by, class_name: "AcidicJob::Run", optional: true
|
17
|
-
has_many :batched_runs, class_name: "AcidicJob::Run", foreign_key: "awaited_by_id"
|
18
|
-
|
19
|
-
after_create_commit :enqueue_staged_job, if: :staged?
|
20
|
-
|
21
|
-
serialize :serialized_job, JSON
|
22
|
-
serialize :error_object, Serializer
|
23
|
-
serialize :workflow, Serializer
|
24
|
-
serialize :returning_to, Serializer
|
25
|
-
store :attr_accessors, coder: Serializer
|
26
|
-
|
27
|
-
validates :staged, inclusion: { in: [true, false] } # uses database default
|
28
|
-
validates :serialized_job, presence: true
|
29
|
-
validates :idempotency_key, presence: true, uniqueness: true
|
30
|
-
validates :job_class, presence: true
|
31
|
-
|
32
|
-
scope :staged, -> { where(staged: true) }
|
33
|
-
scope :unstaged, -> { where(staged: false) }
|
34
|
-
scope :finished, -> { where(recovery_point: FINISHED_RECOVERY_POINT) }
|
35
|
-
scope :outstanding, lambda {
|
36
|
-
where.not(recovery_point: FINISHED_RECOVERY_POINT).or(where(recovery_point: [nil, ""]))
|
37
|
-
}
|
38
|
-
|
39
|
-
with_options unless: :staged? do
|
40
|
-
validates :last_run_at, presence: true
|
41
|
-
validates :recovery_point, presence: true
|
42
|
-
validates :workflow, presence: true
|
43
|
-
end
|
44
|
-
|
45
|
-
def self.purge
|
46
|
-
successfully_completed = where(
|
47
|
-
recovery_point: FINISHED_RECOVERY_POINT,
|
48
|
-
error_object: nil
|
49
|
-
)
|
50
|
-
count = successfully_completed.count
|
51
|
-
|
52
|
-
return 0 if count.zero?
|
53
|
-
|
54
|
-
Rails.logger.info("Deleting #{count} successfully completed AcidicJob runs")
|
55
|
-
successfully_completed.delete_all
|
56
|
-
end
|
57
|
-
|
58
|
-
def finished?
|
59
|
-
recovery_point == FINISHED_RECOVERY_POINT
|
60
|
-
end
|
61
|
-
|
62
|
-
def succeeded?
|
63
|
-
finished? && !failed?
|
64
|
-
end
|
65
|
-
|
66
|
-
def failed?
|
67
|
-
error_object.present?
|
68
|
-
end
|
69
|
-
|
70
|
-
def staged_job_id
|
71
|
-
# encode the identifier for this record in the job ID
|
72
|
-
# base64 encoding for minimal security
|
73
|
-
global_id = to_global_id.to_s.remove("gid://")
|
74
|
-
encoded_global_id = Base64.encode64(global_id).strip
|
75
|
-
|
76
|
-
"STG__#{idempotency_key}__#{encoded_global_id}"
|
77
|
-
end
|
78
|
-
|
79
|
-
private
|
80
|
-
|
81
|
-
def enqueue_staged_job
|
82
|
-
return unless staged?
|
83
|
-
|
84
|
-
serialized_staged_job = if serialized_job.key?("jid")
|
85
|
-
serialized_job.merge("jid" => staged_job_id)
|
86
|
-
elsif serialized_job.key?("job_id")
|
87
|
-
serialized_job.merge("job_id" => staged_job_id)
|
88
|
-
else
|
89
|
-
raise UnknownSerializedJobIdentifier
|
90
|
-
end
|
91
|
-
|
92
|
-
job = job_class.constantize.deserialize(serialized_staged_job)
|
93
|
-
|
94
|
-
job.enqueue
|
95
|
-
|
96
|
-
# NOTE: record will be deleted after the job has successfully been performed
|
97
|
-
true
|
98
|
-
end
|
99
|
-
end
|
100
|
-
end
|
@@ -1,163 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "active_job/serializers"
|
4
|
-
require "active_job/arguments"
|
5
|
-
require "json"
|
6
|
-
|
7
|
-
class WorkerSerializer < ActiveJob::Serializers::ObjectSerializer
|
8
|
-
def serialize(worker)
|
9
|
-
super(
|
10
|
-
"class" => worker.class.name,
|
11
|
-
"args" => worker.instance_variable_get(:@__acidic_job_args),
|
12
|
-
"kwargs" => worker.instance_variable_get(:@__acidic_job_kwargs)
|
13
|
-
)
|
14
|
-
end
|
15
|
-
|
16
|
-
def deserialize(hash)
|
17
|
-
worker_class = hash["class"].constantize
|
18
|
-
worker_class.new(*hash["args"], **hash["kwargs"])
|
19
|
-
end
|
20
|
-
|
21
|
-
def serialize?(argument)
|
22
|
-
defined?(::Sidekiq) && argument.class.include?(::Sidekiq::Worker)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
class JobSerializer < ActiveJob::Serializers::ObjectSerializer
|
27
|
-
def serialize(job)
|
28
|
-
super(job.serialize)
|
29
|
-
end
|
30
|
-
|
31
|
-
def deserialize(hash)
|
32
|
-
job = ActiveJob::Base.deserialize(hash)
|
33
|
-
job.send(:deserialize_arguments_if_needed)
|
34
|
-
if job.arguments.last.is_a?(Hash)
|
35
|
-
*args, kwargs = job.arguments
|
36
|
-
else
|
37
|
-
args = job.arguments
|
38
|
-
kwargs = {}
|
39
|
-
end
|
40
|
-
job.instance_variable_set(:@__acidic_job_args, args)
|
41
|
-
job.instance_variable_set(:@__acidic_job_kwargs, kwargs)
|
42
|
-
|
43
|
-
job
|
44
|
-
end
|
45
|
-
|
46
|
-
def serialize?(argument)
|
47
|
-
defined?(::ActiveJob::Base) && argument.class < ::ActiveJob::Base
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
class ExceptionSerializer < ActiveJob::Serializers::ObjectSerializer
|
52
|
-
def serialize(exception)
|
53
|
-
hash = {
|
54
|
-
"class" => exception.class.name,
|
55
|
-
"message" => exception.message,
|
56
|
-
"cause" => exception.cause,
|
57
|
-
"backtrace" => {}
|
58
|
-
}
|
59
|
-
|
60
|
-
exception.backtrace.map do |trace|
|
61
|
-
path, _, location = trace.rpartition("/")
|
62
|
-
|
63
|
-
next if hash["backtrace"].key?(path)
|
64
|
-
|
65
|
-
hash["backtrace"][path] = location
|
66
|
-
end
|
67
|
-
|
68
|
-
super(hash)
|
69
|
-
end
|
70
|
-
|
71
|
-
def deserialize(hash)
|
72
|
-
exception_class = hash["class"].constantize
|
73
|
-
exception = exception_class.new(hash["message"])
|
74
|
-
exception.set_backtrace(hash["backtrace"].map do |path, location|
|
75
|
-
[path, location].join("/")
|
76
|
-
end)
|
77
|
-
exception
|
78
|
-
end
|
79
|
-
|
80
|
-
def serialize?(argument)
|
81
|
-
defined?(Exception) && argument.is_a?(Exception)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
class FinishedPointSerializer < ActiveJob::Serializers::ObjectSerializer
|
86
|
-
def serialize(finished_point)
|
87
|
-
super(
|
88
|
-
"class" => finished_point.class.name
|
89
|
-
)
|
90
|
-
end
|
91
|
-
|
92
|
-
def deserialize(hash)
|
93
|
-
finished_point_class = hash["class"].constantize
|
94
|
-
finished_point_class.new
|
95
|
-
end
|
96
|
-
|
97
|
-
def serialize?(argument)
|
98
|
-
defined?(::AcidicJob::FinishedPoint) && argument.is_a?(::AcidicJob::FinishedPoint)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
class RecoveryPointSerializer < ActiveJob::Serializers::ObjectSerializer
|
103
|
-
def serialize(recovery_point)
|
104
|
-
super(
|
105
|
-
"class" => recovery_point.class.name,
|
106
|
-
"name" => recovery_point.name
|
107
|
-
)
|
108
|
-
end
|
109
|
-
|
110
|
-
def deserialize(hash)
|
111
|
-
recovery_point_class = hash["class"].constantize
|
112
|
-
recovery_point_class.new(hash["name"])
|
113
|
-
end
|
114
|
-
|
115
|
-
def serialize?(argument)
|
116
|
-
defined?(::AcidicJob::RecoveryPoint) && argument.is_a?(::AcidicJob::RecoveryPoint)
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
ActiveJob::Serializers.add_serializers(
|
121
|
-
WorkerSerializer,
|
122
|
-
JobSerializer,
|
123
|
-
ExceptionSerializer,
|
124
|
-
FinishedPointSerializer,
|
125
|
-
RecoveryPointSerializer
|
126
|
-
)
|
127
|
-
|
128
|
-
# ...
|
129
|
-
module AcidicJob
|
130
|
-
module Arguments
|
131
|
-
include ActiveJob::Arguments
|
132
|
-
extend self # rubocop:disable Style/ModuleFunction
|
133
|
-
|
134
|
-
# `ActiveJob` will throw an error if it tries to deserialize a GlobalID record.
|
135
|
-
# However, this isn't the behavior that we want for our custom `ActiveRecord` serializer.
|
136
|
-
# Since `ActiveRecord` does _not_ reset instance record state to its pre-transactional state
|
137
|
-
# on a transaction ROLLBACK, we can have GlobalID entries in a serialized column that point to
|
138
|
-
# non-persisted records. This is ok. We should simply return `nil` for that portion of the
|
139
|
-
# serialized field.
|
140
|
-
def deserialize_global_id(hash)
|
141
|
-
GlobalID::Locator.locate hash[GLOBALID_KEY]
|
142
|
-
rescue ActiveRecord::RecordNotFound
|
143
|
-
nil
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
class Serializer
|
148
|
-
# Used for `serialize` method in ActiveRecord
|
149
|
-
class << self
|
150
|
-
def load(json)
|
151
|
-
return if json.nil? || json.empty?
|
152
|
-
|
153
|
-
data = JSON.parse(json)
|
154
|
-
Arguments.deserialize(data).first
|
155
|
-
end
|
156
|
-
|
157
|
-
def dump(obj)
|
158
|
-
data = Arguments.serialize [obj]
|
159
|
-
data.to_json
|
160
|
-
end
|
161
|
-
end
|
162
|
-
end
|
163
|
-
end
|
data/lib/acidic_job/staging.rb
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "active_support/concern"
|
4
|
-
require "global_id/locator"
|
5
|
-
|
6
|
-
module AcidicJob
|
7
|
-
module Staging
|
8
|
-
extend ActiveSupport::Concern
|
9
|
-
|
10
|
-
private
|
11
|
-
|
12
|
-
def was_staged_job?
|
13
|
-
identifier.start_with? "STG__"
|
14
|
-
end
|
15
|
-
|
16
|
-
def staged_job_run
|
17
|
-
# "STG_#{idempotency_key}__#{encoded_global_id}"
|
18
|
-
encoded_global_id = identifier.split("__").last
|
19
|
-
staged_job_gid = "gid://#{Base64.decode64(encoded_global_id)}"
|
20
|
-
|
21
|
-
GlobalID::Locator.locate(staged_job_gid)
|
22
|
-
rescue ActiveRecord::RecordNotFound
|
23
|
-
nil
|
24
|
-
end
|
25
|
-
|
26
|
-
def identifier
|
27
|
-
return jid if defined?(jid) && !jid.nil?
|
28
|
-
return job_id if defined?(job_id) && !job_id.nil?
|
29
|
-
|
30
|
-
# might be defined already in `with_acidity` method
|
31
|
-
acidic_identifier = self.class.acidic_identifier
|
32
|
-
@__acidic_job_idempotency_key ||= IdempotencyKey.new(acidic_identifier)
|
33
|
-
.value_for(self, *@__acidic_job_args, **@__acidic_job_kwargs)
|
34
|
-
|
35
|
-
@__acidic_job_idempotency_key
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
data/lib/acidic_job/step.rb
DELETED
@@ -1,104 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module AcidicJob
|
4
|
-
# Each AcidicJob::Step requires two phases: [1] execution and [2] progression
|
5
|
-
class Step
|
6
|
-
def initialize(step, run, job, step_result = nil)
|
7
|
-
@step = step
|
8
|
-
@run = run
|
9
|
-
@job = job
|
10
|
-
@step_result = step_result
|
11
|
-
end
|
12
|
-
|
13
|
-
# The execution phase performs the work of the defined step
|
14
|
-
def execute
|
15
|
-
rescued_error = false
|
16
|
-
step_callable = wrap_step_as_acidic_callable @step
|
17
|
-
|
18
|
-
begin
|
19
|
-
@run.with_lock do
|
20
|
-
@step_result = step_callable.call(@run)
|
21
|
-
end
|
22
|
-
# QUESTION: Can an error not inherit from StandardError
|
23
|
-
rescue StandardError => e
|
24
|
-
rescued_error = e
|
25
|
-
raise e
|
26
|
-
ensure
|
27
|
-
if rescued_error
|
28
|
-
# If we're leaving under an error condition, try to unlock the job
|
29
|
-
# run right away so that another request can try again.
|
30
|
-
begin
|
31
|
-
@run.update_columns(locked_at: nil, error_object: rescued_error)
|
32
|
-
rescue StandardError => e
|
33
|
-
# We're already inside an error condition, so swallow any additional
|
34
|
-
# errors from here and just send them to logs.
|
35
|
-
# TODO: implement and use a logger here
|
36
|
-
puts "Failed to unlock AcidicJob::Run #{@run.id} because of #{e}."
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
# The progression phase advances the job run state machine onto the next step
|
43
|
-
def progress
|
44
|
-
@run.with_lock do
|
45
|
-
if @step_result.is_a?(FinishedPoint)
|
46
|
-
@job.run_callbacks :finish do
|
47
|
-
@step_result.call(run: @run)
|
48
|
-
end
|
49
|
-
else
|
50
|
-
@step_result.call(run: @run)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
private
|
56
|
-
|
57
|
-
def wrap_step_as_acidic_callable(step)
|
58
|
-
# {"does" => :enqueue_step, "then" => :next_step, "awaits" => [WorkerWithEnqueueStep::FirstWorker]}
|
59
|
-
current_step = step["does"]
|
60
|
-
next_step = step["then"]
|
61
|
-
# to support iteration within steps
|
62
|
-
iterable_key = step["for_each"]
|
63
|
-
iterated_key = "processed_#{current_step}_#{iterable_key}"
|
64
|
-
iterables = @run.attr_accessors.fetch(iterable_key, []) || []
|
65
|
-
iterateds = @run.attr_accessors.fetch(iterated_key, []) || []
|
66
|
-
next_item = iterables.reject { |item| iterateds.include? item }.first
|
67
|
-
|
68
|
-
# jobs can have no-op steps, especially so that they can use only the async/await mechanism for that step
|
69
|
-
callable = if @job.respond_to?(current_step, _include_private = true)
|
70
|
-
@job.method(current_step)
|
71
|
-
else
|
72
|
-
proc {}
|
73
|
-
end
|
74
|
-
|
75
|
-
# return a callable Proc with a consistent interface for the execution phase
|
76
|
-
proc do |run|
|
77
|
-
result = if iterable_key.present? && next_item.present?
|
78
|
-
callable.call(next_item)
|
79
|
-
elsif iterable_key.present? && next_item.nil?
|
80
|
-
true
|
81
|
-
elsif callable.arity.zero?
|
82
|
-
callable.call
|
83
|
-
elsif callable.arity == 1
|
84
|
-
callable.call(run)
|
85
|
-
else
|
86
|
-
raise TooManyParametersForStepMethod
|
87
|
-
end
|
88
|
-
|
89
|
-
if result.is_a?(FinishedPoint)
|
90
|
-
result
|
91
|
-
elsif next_item.present?
|
92
|
-
iterateds << next_item
|
93
|
-
@run.attr_accessors[iterated_key] = iterateds
|
94
|
-
@run.save!(validate: false)
|
95
|
-
RecoveryPoint.new(current_step)
|
96
|
-
elsif next_step.to_s == Run::FINISHED_RECOVERY_POINT
|
97
|
-
FinishedPoint.new
|
98
|
-
else
|
99
|
-
RecoveryPoint.new(next_step)
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|
data/lib/acidic_job/test_case.rb
DELETED
@@ -1,118 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "active_support/concern"
|
4
|
-
|
5
|
-
module AcidicJob
|
6
|
-
# recreate the original `Key` model
|
7
|
-
class Key < ::ActiveRecord::Base
|
8
|
-
RECOVERY_POINT_FINISHED = "FINISHED"
|
9
|
-
|
10
|
-
self.table_name = "acidic_job_keys"
|
11
|
-
|
12
|
-
serialize :error_object
|
13
|
-
serialize :job_args
|
14
|
-
serialize :workflow
|
15
|
-
store :attr_accessors
|
16
|
-
end
|
17
|
-
|
18
|
-
# recreate the original `Staged` model
|
19
|
-
class Staged < ActiveRecord::Base
|
20
|
-
self.table_name = "staged_acidic_jobs"
|
21
|
-
|
22
|
-
serialize :job_args
|
23
|
-
|
24
|
-
after_create_commit :enqueue_job
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
def enqueue_job
|
29
|
-
gid = { "staged_job_gid" => to_global_id.to_s }
|
30
|
-
|
31
|
-
if job_args.is_a?(Hash) && job_args.key?("arguments")
|
32
|
-
job_args["arguments"].concat([gid])
|
33
|
-
else
|
34
|
-
job_args.concat([gid])
|
35
|
-
end
|
36
|
-
|
37
|
-
case adapter
|
38
|
-
when "activejob"
|
39
|
-
::ActiveJob::Base.deserialize(job_args).enqueue
|
40
|
-
when "sidekiq"
|
41
|
-
job_name.constantize.perform_async(*job_args)
|
42
|
-
else
|
43
|
-
raise UnknownJobAdapter.new(adapter: adapter)
|
44
|
-
end
|
45
|
-
|
46
|
-
# NOTE: record will be deleted after the job has successfully been performed
|
47
|
-
true
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
module UpgradeService
|
52
|
-
def self.execute
|
53
|
-
# prepare an array to hold the attribute hashes to be passed to `insert_all`
|
54
|
-
run_attributes = []
|
55
|
-
# prepare an array to hold any `Key` records that we couldn't successfully map to `Run` records
|
56
|
-
errored_keys = []
|
57
|
-
|
58
|
-
# iterate over all `AcidicJob::Key` records in batches,
|
59
|
-
# preparing a `Run` attribute hash to be passed to `insert_all`
|
60
|
-
::AcidicJob::Key.find_each do |key|
|
61
|
-
# map all of the simple attributes directly
|
62
|
-
attributes = {
|
63
|
-
id: key.id,
|
64
|
-
staged: false,
|
65
|
-
idempotency_key: key.idempotency_key,
|
66
|
-
job_class: key.job_name,
|
67
|
-
last_run_at: key.last_run_at,
|
68
|
-
locked_at: key.locked_at,
|
69
|
-
recovery_point: key.recovery_point,
|
70
|
-
error_object: key.error_object,
|
71
|
-
attr_accessors: key.attr_accessors,
|
72
|
-
workflow: key.workflow,
|
73
|
-
created_at: key.created_at,
|
74
|
-
updated_at: key.updated_at
|
75
|
-
}
|
76
|
-
|
77
|
-
# prepare the more complicated `job_args` -> `serialized_job` translation
|
78
|
-
job_class = key.job_name.constantize
|
79
|
-
if defined?(::Sidekiq) && job_class.include?(::Sidekiq::Worker)
|
80
|
-
unless job_class.include?(::AcidicJob::Extensions::Sidekiq)
|
81
|
-
job_class.include(::AcidicJob::Extensions::Sidekiq)
|
82
|
-
end
|
83
|
-
job_instance = job_class.new
|
84
|
-
serialized_job = job_instance.serialize_job(*key.job_args)
|
85
|
-
elsif defined?(::ActiveJob) && job_class < ::ActiveJob::Base
|
86
|
-
unless job_class.include?(::AcidicJob::Extensions::ActiveJob)
|
87
|
-
job_class.include(::AcidicJob::Extensions::ActiveJob)
|
88
|
-
end
|
89
|
-
job_args = begin
|
90
|
-
::ActiveJob::Arguments.deserialize(key.job_args)
|
91
|
-
rescue ::ActiveJob::DeserializationError
|
92
|
-
key.job_args
|
93
|
-
end
|
94
|
-
job_instance = job_class.new(*job_args)
|
95
|
-
serialized_job = job_instance.serialize_job
|
96
|
-
end
|
97
|
-
|
98
|
-
attributes[:serialized_job] = serialized_job
|
99
|
-
run_attributes << attributes
|
100
|
-
rescue StandardError => e
|
101
|
-
errored_keys << [e, key]
|
102
|
-
end
|
103
|
-
|
104
|
-
# insert all of the `Run` records
|
105
|
-
::AcidicJob::Run.insert_all(run_attributes)
|
106
|
-
|
107
|
-
# delete all successfully migrated `Key` record
|
108
|
-
::AcidicJob::Key.where(id: ::AcidicJob::Run.select(:id)).delete_all
|
109
|
-
|
110
|
-
# return a report of the upgrade migration
|
111
|
-
{
|
112
|
-
run_records: ::AcidicJob::Run.count,
|
113
|
-
key_records: ::AcidicJob::Key.count,
|
114
|
-
errored_keys: errored_keys
|
115
|
-
}
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|