acidic_job 1.0.0.rc1 → 1.0.0.rc3
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/Gemfile.lock +29 -4
- data/README.md +4 -3
- data/acidic_job.gemspec +3 -1
- data/app/models/acidic_job/entry.rb +7 -7
- data/app/models/acidic_job/execution.rb +22 -1
- data/lib/acidic_job/builder.rb +22 -6
- data/lib/acidic_job/context.rb +20 -0
- data/lib/acidic_job/errors.rb +24 -6
- data/lib/acidic_job/log_subscriber.rb +8 -8
- data/lib/acidic_job/plugin_context.rb +51 -0
- data/lib/acidic_job/plugins/transactional_step.rb +38 -0
- data/lib/acidic_job/version.rb +1 -1
- data/lib/acidic_job/workflow.rb +78 -41
- data/lib/acidic_job.rb +4 -2
- data/lib/generators/acidic_job/templates/create_acidic_job_tables_migration.rb.erb +8 -7
- metadata +36 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8af9b244cf2343978f26322753e80c779c224ab6942c85de5a8fe05cf0ba55f2
|
4
|
+
data.tar.gz: 5f609ee36139400937e2a8b3cae42f721eb72f1abf6633ddbd275aa18c1b2242
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6becc8ed3a1c1c58e24d52753e53edb206854cec472ffb4438e4711ea2a8abcd5adae080c621ab7b8bd30eb386118a51012a4f446e997277da3e9b12e460c696
|
7
|
+
data.tar.gz: fe04f817a6c2cab6b2284f7ce0a79b44f452e1c490abac18e6385b801a7ca00860e0910286886da6d70270b508ca54fa3ae4c13369d539e47c3ea4d12e9316d8
|
data/.codacy.yml
ADDED
data/Gemfile.lock
CHANGED
@@ -1,15 +1,23 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
acidic_job (1.0.0.
|
4
|
+
acidic_job (1.0.0.rc3)
|
5
5
|
activejob (>= 7.1)
|
6
6
|
activerecord (>= 7.1)
|
7
7
|
activesupport (>= 7.1)
|
8
|
+
json (>= 2.7.0)
|
8
9
|
railties (>= 7.1)
|
9
10
|
|
10
11
|
GEM
|
11
12
|
remote: https://rubygems.org/
|
12
13
|
specs:
|
14
|
+
actionmailer (7.2.1.1)
|
15
|
+
actionpack (= 7.2.1.1)
|
16
|
+
actionview (= 7.2.1.1)
|
17
|
+
activejob (= 7.2.1.1)
|
18
|
+
activesupport (= 7.2.1.1)
|
19
|
+
mail (>= 2.8.0)
|
20
|
+
rails-dom-testing (~> 2.2)
|
13
21
|
actionpack (7.2.1.1)
|
14
22
|
actionview (= 7.2.1.1)
|
15
23
|
activesupport (= 7.2.1.1)
|
@@ -51,7 +59,7 @@ GEM
|
|
51
59
|
base64 (0.2.0)
|
52
60
|
bigdecimal (3.1.8)
|
53
61
|
builder (3.3.0)
|
54
|
-
chaotic_job (0.
|
62
|
+
chaotic_job (0.3.0)
|
55
63
|
combustion (1.3.7)
|
56
64
|
activesupport (>= 3.0.0)
|
57
65
|
railties (>= 3.0.0)
|
@@ -59,6 +67,7 @@ GEM
|
|
59
67
|
concurrent-ruby (1.3.4)
|
60
68
|
connection_pool (2.4.0)
|
61
69
|
crass (1.0.6)
|
70
|
+
date (3.4.1)
|
62
71
|
docile (1.4.0)
|
63
72
|
drb (2.2.1)
|
64
73
|
erubi (1.13.0)
|
@@ -70,13 +79,28 @@ GEM
|
|
70
79
|
irb (1.14.1)
|
71
80
|
rdoc (>= 4.0.0)
|
72
81
|
reline (>= 0.4.2)
|
73
|
-
json (2.
|
82
|
+
json (2.10.2)
|
74
83
|
logger (1.6.1)
|
75
84
|
loofah (2.22.0)
|
76
85
|
crass (~> 1.0.2)
|
77
86
|
nokogiri (>= 1.12.0)
|
87
|
+
mail (2.8.1)
|
88
|
+
mini_mime (>= 0.1.1)
|
89
|
+
net-imap
|
90
|
+
net-pop
|
91
|
+
net-smtp
|
92
|
+
mini_mime (1.1.5)
|
78
93
|
mini_portile2 (2.8.7)
|
79
94
|
minitest (5.25.1)
|
95
|
+
net-imap (0.5.1)
|
96
|
+
date
|
97
|
+
net-protocol
|
98
|
+
net-pop (0.1.2)
|
99
|
+
net-protocol
|
100
|
+
net-protocol (0.2.2)
|
101
|
+
timeout
|
102
|
+
net-smtp (0.5.0)
|
103
|
+
net-protocol
|
80
104
|
nokogiri (1.16.7)
|
81
105
|
mini_portile2 (~> 2.8.2)
|
82
106
|
racc (~> 1.4)
|
@@ -162,7 +186,8 @@ PLATFORMS
|
|
162
186
|
|
163
187
|
DEPENDENCIES
|
164
188
|
acidic_job!
|
165
|
-
|
189
|
+
actionmailer (>= 7.1)
|
190
|
+
chaotic_job
|
166
191
|
combustion
|
167
192
|
minitest
|
168
193
|
rake
|
data/README.md
CHANGED
@@ -24,13 +24,13 @@ With AcidicJob, you can write reliable and repeatable multi-step distributed ope
|
|
24
24
|
Install the gem and add to the application's Gemfile by executing:
|
25
25
|
|
26
26
|
```sh
|
27
|
-
bundle add acidic_job
|
27
|
+
bundle add acidic_job --version "1.0.0.rc1"
|
28
28
|
```
|
29
29
|
|
30
30
|
If `bundler` is not being used to manage dependencies, install the gem by executing:
|
31
31
|
|
32
32
|
```sh
|
33
|
-
gem install acidic_job
|
33
|
+
gem install acidic_job --pre
|
34
34
|
```
|
35
35
|
|
36
36
|
After installing the gem, run the installer:
|
@@ -101,7 +101,8 @@ class RideCreateJob < AcidicJob::Base
|
|
101
101
|
end
|
102
102
|
```
|
103
103
|
|
104
|
-
|
104
|
+
> [!IMPORTANT]
|
105
|
+
> The `unique_by` keyword argument is used to define the unique identifier for a particular execution of the workflow. This helps to ensure that the workflow is idempotent, as retries of the job will correctly resume the pre-existing workflow execution. The `unique_by` argument can **only** be something that `JSON.generate(..., strict: true)` can handle; that is, it must be made up of only the JSON native types: `Hash`, `Array`, `String`, `Integer`, `Float`, `true`, `false` and `nil`.
|
105
106
|
|
106
107
|
The block passed to `execute_workflow` is where you define the steps of the workflow. Each step is defined by calling the `step` method on the yielded workflow builder object. The `step` method takes the name of a method in the job that will be executed as part of the workflow. The `transactional` keyword argument can be used to ensure that the step is executed within a database transaction.
|
107
108
|
|
data/acidic_job.gemspec
CHANGED
@@ -27,14 +27,16 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
28
28
|
spec.require_paths = ["lib"]
|
29
29
|
|
30
|
+
spec.add_dependency "json", ">= 2.7.0" # see: https://github.com/ruby/json/pull/519
|
30
31
|
">= 7.1".tap do |rails_version|
|
31
32
|
spec.add_dependency "activejob", rails_version
|
32
33
|
spec.add_dependency "activerecord", rails_version
|
33
34
|
spec.add_dependency "activesupport", rails_version
|
34
35
|
spec.add_dependency "railties", rails_version
|
36
|
+
spec.add_development_dependency "actionmailer", rails_version
|
35
37
|
end
|
36
38
|
|
37
|
-
spec.add_development_dependency "chaotic_job"
|
39
|
+
spec.add_development_dependency "chaotic_job"
|
38
40
|
spec.add_development_dependency "combustion"
|
39
41
|
spec.add_development_dependency "minitest"
|
40
42
|
spec.add_development_dependency "rake"
|
@@ -4,16 +4,16 @@ module AcidicJob
|
|
4
4
|
class Entry < Record
|
5
5
|
belongs_to :execution, class_name: "AcidicJob::Execution"
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
scope :for_step, ->(step) { where(step: step) }
|
8
|
+
scope :for_action, ->(action) { where(action: action) }
|
9
|
+
scope :ordered, -> { order(timestamp: :asc) }
|
10
10
|
|
11
|
-
def
|
12
|
-
|
11
|
+
def self.most_recent
|
12
|
+
order(created_at: :desc).first
|
13
13
|
end
|
14
14
|
|
15
|
-
def
|
16
|
-
action ==
|
15
|
+
def action?(check)
|
16
|
+
action == check
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
@@ -29,7 +29,28 @@ module AcidicJob
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def finished?
|
32
|
-
|
32
|
+
# rubocop:disable Style/MultipleComparison
|
33
|
+
recover_to.to_s == FINISHED_RECOVERY_POINT ||
|
34
|
+
recover_to.to_s == "FINISHED" # old value pre-1.0, remove at v1.0
|
35
|
+
# rubocop:enable Style/MultipleComparison
|
36
|
+
end
|
37
|
+
|
38
|
+
def defined?(step)
|
39
|
+
if definition.key?("steps")
|
40
|
+
definition["steps"].key?(step)
|
41
|
+
else
|
42
|
+
# TODO: add deprecation warning
|
43
|
+
definition.key?(step)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def definition_for(step)
|
48
|
+
if definition.key?("steps")
|
49
|
+
definition["steps"].fetch(step)
|
50
|
+
else
|
51
|
+
# TODO: add deprecation warning
|
52
|
+
definition.fetch(step)
|
53
|
+
end
|
33
54
|
end
|
34
55
|
|
35
56
|
def deserialized_job
|
data/lib/acidic_job/builder.rb
CHANGED
@@ -4,12 +4,21 @@ module AcidicJob
|
|
4
4
|
class Builder
|
5
5
|
attr_reader :steps
|
6
6
|
|
7
|
-
def initialize
|
7
|
+
def initialize(plugins)
|
8
|
+
@plugins = plugins
|
8
9
|
@steps = []
|
9
10
|
end
|
10
11
|
|
11
|
-
def step(method_name,
|
12
|
-
|
12
|
+
def step(method_name, **kwargs)
|
13
|
+
step = { "does" => method_name.to_s }
|
14
|
+
|
15
|
+
@plugins.each do |plugin|
|
16
|
+
next unless kwargs.key?(plugin.keyword)
|
17
|
+
|
18
|
+
step[plugin.keyword.to_s] = plugin.validate(kwargs[plugin.keyword])
|
19
|
+
end
|
20
|
+
|
21
|
+
@steps << step
|
13
22
|
@steps
|
14
23
|
end
|
15
24
|
|
@@ -17,13 +26,20 @@ module AcidicJob
|
|
17
26
|
# [ { does: "step 1", transactional: true }, { does: "step 2", transactional: false }, ... ]
|
18
27
|
@steps << { "does" => FINISHED_RECOVERY_POINT }
|
19
28
|
|
20
|
-
|
29
|
+
definition = {
|
30
|
+
"meta" => {
|
31
|
+
"version" => VERSION
|
32
|
+
},
|
33
|
+
"steps" => {}
|
34
|
+
}
|
35
|
+
|
36
|
+
definition.tap do |workflow|
|
21
37
|
@steps.each_cons(2).map do |enter_step, exit_step|
|
22
38
|
enter_name = enter_step["does"]
|
23
|
-
workflow[enter_name] = enter_step.merge("then" => exit_step["does"])
|
39
|
+
workflow["steps"][enter_name] = enter_step.merge("then" => exit_step["does"])
|
24
40
|
end
|
25
41
|
end
|
26
|
-
# { "step 1": { does: "step 1", transactional: true, then: "step 2" }, ... }
|
42
|
+
# { meta: { ... }, steps: { "step 1": { does: "step 1", transactional: true, then: "step 2" }, ... } }
|
27
43
|
end
|
28
44
|
end
|
29
45
|
end
|
data/lib/acidic_job/context.rb
CHANGED
@@ -6,6 +6,26 @@ module AcidicJob
|
|
6
6
|
@execution = execution
|
7
7
|
end
|
8
8
|
|
9
|
+
def set(hash)
|
10
|
+
AcidicJob.instrument(:set_context, **hash) do
|
11
|
+
AcidicJob::Value.upsert_all(
|
12
|
+
hash.map do |key, value|
|
13
|
+
{ execution_id: @execution.id,
|
14
|
+
key: key,
|
15
|
+
value: value }
|
16
|
+
end,
|
17
|
+
unique_by: %i[execution_id key]
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def get(*keys)
|
23
|
+
AcidicJob.instrument(:get_context, keys: keys) do
|
24
|
+
@execution.values.select(:value).where(key: keys).pluck(:value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# TODO: deprecate these methods
|
9
29
|
def []=(key, value)
|
10
30
|
AcidicJob.instrument(:set_context, key: key, value: value) do
|
11
31
|
AcidicJob::Value.upsert(
|
data/lib/acidic_job/errors.rb
CHANGED
@@ -28,9 +28,9 @@ module AcidicJob
|
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
+
# rubocop:disable Lint/MissingSuper
|
31
32
|
class ArgumentMismatchError < Error
|
32
33
|
def initialize(expected, existing)
|
33
|
-
super
|
34
34
|
@expected = expected
|
35
35
|
@existing = existing
|
36
36
|
end
|
@@ -46,7 +46,6 @@ module AcidicJob
|
|
46
46
|
|
47
47
|
class DefinitionMismatchError < Error
|
48
48
|
def initialize(expected, existing)
|
49
|
-
super
|
50
49
|
@expected = expected
|
51
50
|
@existing = existing
|
52
51
|
end
|
@@ -62,7 +61,6 @@ module AcidicJob
|
|
62
61
|
|
63
62
|
class UndefinedStepError < Error
|
64
63
|
def initialize(step)
|
65
|
-
super
|
66
64
|
@step = step
|
67
65
|
end
|
68
66
|
|
@@ -73,7 +71,6 @@ module AcidicJob
|
|
73
71
|
|
74
72
|
class SucceededStepError < Error
|
75
73
|
def initialize(step)
|
76
|
-
super
|
77
74
|
@step = step
|
78
75
|
end
|
79
76
|
|
@@ -84,7 +81,6 @@ module AcidicJob
|
|
84
81
|
|
85
82
|
class UndefinedMethodError < Error
|
86
83
|
def initialize(step)
|
87
|
-
super
|
88
84
|
@step = step
|
89
85
|
end
|
90
86
|
|
@@ -95,7 +91,6 @@ module AcidicJob
|
|
95
91
|
|
96
92
|
class InvalidMethodError < Error
|
97
93
|
def initialize(step)
|
98
|
-
super
|
99
94
|
@step = step
|
100
95
|
end
|
101
96
|
|
@@ -103,4 +98,27 @@ module AcidicJob
|
|
103
98
|
"step method cannot expect arguments: #{@step.inspect}"
|
104
99
|
end
|
105
100
|
end
|
101
|
+
|
102
|
+
class DoublePluginCallError < Error
|
103
|
+
def initialize(plugin, step)
|
104
|
+
@plugin_name = Module === plugin ? plugin.name : plugin.class.name # rubocop:disable Style/CaseEquality
|
105
|
+
@step = step
|
106
|
+
end
|
107
|
+
|
108
|
+
def message
|
109
|
+
"plugin `#{@plugin_name}` attempted to call step multiple times: #{@step.inspect}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class MissingPluginCallError < Error
|
114
|
+
def initialize(plugin, step)
|
115
|
+
@plugin_name = Module === plugin ? plugin.name : plugin.class.name # rubocop:disable Style/CaseEquality
|
116
|
+
@step = step
|
117
|
+
end
|
118
|
+
|
119
|
+
def message
|
120
|
+
"plugin `#{@plugin_name}` failed to call step: #{@step.inspect}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
# rubocop:enable Lint/MissingSuper
|
106
124
|
end
|
@@ -5,33 +5,33 @@ require "active_support/log_subscriber"
|
|
5
5
|
module AcidicJob
|
6
6
|
class LogSubscriber < ActiveSupport::LogSubscriber
|
7
7
|
def define_workflow(event)
|
8
|
-
debug formatted_event(event,
|
8
|
+
debug formatted_event(event, title: "Define workflow", **event.payload.slice(:job_class, :job_id))
|
9
9
|
end
|
10
10
|
|
11
11
|
def initialize_workflow(event)
|
12
|
-
debug formatted_event(event,
|
12
|
+
debug formatted_event(event, title: "Initialize workflow", **event.payload.slice(:steps))
|
13
13
|
end
|
14
14
|
|
15
15
|
def process_workflow(event)
|
16
|
-
debug formatted_event(event,
|
16
|
+
debug formatted_event(event, title: "Process workflow", **event.payload[:execution].slice(:id, :recover_to))
|
17
17
|
end
|
18
18
|
|
19
19
|
def process_step(event)
|
20
|
-
debug formatted_event(event,
|
20
|
+
debug formatted_event(event, title: "Process step", **event.payload)
|
21
21
|
end
|
22
22
|
|
23
23
|
def perform_step(event)
|
24
|
-
debug formatted_event(event,
|
24
|
+
debug formatted_event(event, title: "Perform step", **event.payload)
|
25
25
|
end
|
26
26
|
|
27
27
|
def record_entry(event)
|
28
|
-
debug formatted_event(event,
|
28
|
+
debug formatted_event(event, title: "Record entry", **event.payload.slice(:step, :action, :timestamp))
|
29
29
|
end
|
30
30
|
|
31
31
|
private
|
32
32
|
|
33
|
-
def formatted_event(event,
|
34
|
-
"AcidicJob-#{AcidicJob::VERSION} #{
|
33
|
+
def formatted_event(event, title:, **attributes)
|
34
|
+
"AcidicJob-#{AcidicJob::VERSION} #{title} (#{event.duration.round(1)}ms) #{formatted_attributes(**attributes)}"
|
35
35
|
end
|
36
36
|
|
37
37
|
def formatted_attributes(**attributes)
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AcidicJob
|
4
|
+
class PluginContext
|
5
|
+
PLUGIN_INACTIVE = :__ACIDIC_JOB_PLUGIN_INACTIVE__
|
6
|
+
|
7
|
+
def initialize(plugin, job, execution, step_definition)
|
8
|
+
@plugin = plugin
|
9
|
+
@job = job
|
10
|
+
@execution = execution
|
11
|
+
@step_definition = step_definition
|
12
|
+
end
|
13
|
+
|
14
|
+
def definition
|
15
|
+
@step_definition.fetch(@plugin.keyword.to_s, PLUGIN_INACTIVE)
|
16
|
+
end
|
17
|
+
|
18
|
+
def current_step
|
19
|
+
@step_definition["does"]
|
20
|
+
end
|
21
|
+
|
22
|
+
def inactive?
|
23
|
+
definition == PLUGIN_INACTIVE
|
24
|
+
end
|
25
|
+
|
26
|
+
def entries_for_action(action)
|
27
|
+
@execution.entries.for_action(plugin_action(action))
|
28
|
+
end
|
29
|
+
|
30
|
+
def record!(step:, action:, timestamp:, **kwargs)
|
31
|
+
@execution.record!(
|
32
|
+
step: step,
|
33
|
+
action: plugin_action(action),
|
34
|
+
timestamp: timestamp,
|
35
|
+
**kwargs
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
def enqueue_job(...)
|
40
|
+
@job.enqueue(...)
|
41
|
+
end
|
42
|
+
|
43
|
+
def halt_step!
|
44
|
+
@job.halt_step!
|
45
|
+
end
|
46
|
+
|
47
|
+
def plugin_action(action)
|
48
|
+
"#{@plugin.keyword}/#{action}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AcidicJob
|
4
|
+
module Plugins
|
5
|
+
module TransactionalStep
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def keyword
|
9
|
+
:transactional
|
10
|
+
end
|
11
|
+
|
12
|
+
# transactional: true
|
13
|
+
# transactional: false
|
14
|
+
# transactional: { on: Model }
|
15
|
+
def validate(input)
|
16
|
+
return input if input in true | false
|
17
|
+
|
18
|
+
raise ArgumentError.new("argument must be boolean or hash") unless input in Hash
|
19
|
+
raise ArgumentError.new("argument hash must have `on` key") unless input in Hash[on:]
|
20
|
+
raise ArgumentError.new("`on` key must have module value") unless input in Hash[on: Module]
|
21
|
+
|
22
|
+
input
|
23
|
+
end
|
24
|
+
|
25
|
+
def around_step(context, &block)
|
26
|
+
return yield if context.definition == false
|
27
|
+
|
28
|
+
model = if context.definition == true
|
29
|
+
AcidicJob::Execution
|
30
|
+
else
|
31
|
+
context.definition["on"].constantize
|
32
|
+
end
|
33
|
+
|
34
|
+
model.transaction(&block)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/acidic_job/version.rb
CHANGED
data/lib/acidic_job/workflow.rb
CHANGED
@@ -4,33 +4,31 @@ require "active_job"
|
|
4
4
|
|
5
5
|
module AcidicJob
|
6
6
|
module Workflow
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
private_constant :NO_OP_WRAPPER, :REPEAT_STEP, :HALT_STEP
|
7
|
+
REPEAT_STEP = :__ACIDIC_JOB_REPEAT_STEP_SIGNAL__
|
8
|
+
HALT_STEP = :__ACIDIC_JOB_HALT_STEP_SIGNAL__
|
9
|
+
private_constant :REPEAT_STEP, :HALT_STEP
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
def execute_workflow(unique_by:, &block)
|
11
|
+
def execute_workflow(unique_by:, with: [Plugins::TransactionalStep], &block)
|
12
|
+
@__acidic_job_plugins__ = with
|
15
13
|
serialized_job = serialize
|
16
14
|
|
17
15
|
workflow_definition = AcidicJob.instrument(:define_workflow, **serialized_job) do
|
18
|
-
raise RedefiningWorkflowError if defined? @
|
16
|
+
raise RedefiningWorkflowError if defined? @__acidic_job_builder__
|
19
17
|
|
20
|
-
@
|
18
|
+
@__acidic_job_builder__ = Builder.new(@__acidic_job_plugins__)
|
21
19
|
|
22
20
|
raise UndefinedWorkflowBlockError unless block_given?
|
23
21
|
raise InvalidWorkflowBlockError if block.arity != 1
|
24
22
|
|
25
|
-
block.call @
|
23
|
+
block.call @__acidic_job_builder__
|
26
24
|
|
27
|
-
raise MissingStepsError if @
|
25
|
+
raise MissingStepsError if @__acidic_job_builder__.steps.empty?
|
28
26
|
|
29
27
|
# convert the array of steps into a hash of recovery_points and next steps
|
30
|
-
@
|
28
|
+
@__acidic_job_builder__.define_workflow
|
31
29
|
end
|
32
30
|
|
33
|
-
AcidicJob.instrument(:initialize_workflow,
|
31
|
+
AcidicJob.instrument(:initialize_workflow, definition: workflow_definition) do
|
34
32
|
transaction_args = case ::ActiveRecord::Base.connection.adapter_name.downcase.to_sym
|
35
33
|
# SQLite doesn't support `serializable` transactions
|
36
34
|
when :sqlite
|
@@ -38,9 +36,9 @@ module AcidicJob
|
|
38
36
|
else
|
39
37
|
{ isolation: :serializable }
|
40
38
|
end
|
41
|
-
idempotency_key = Digest::SHA256.hexdigest(JSON.
|
39
|
+
idempotency_key = Digest::SHA256.hexdigest(JSON.fast_generate([self.class.name, unique_by], strict: true))
|
42
40
|
|
43
|
-
@
|
41
|
+
@__acidic_job_execution__ = ::ActiveRecord::Base.transaction(**transaction_args) do
|
44
42
|
record = Execution.find_by(idempotency_key: idempotency_key)
|
45
43
|
|
46
44
|
if record.present?
|
@@ -62,41 +60,52 @@ module AcidicJob
|
|
62
60
|
last_run_at: Time.current
|
63
61
|
)
|
64
62
|
else
|
63
|
+
starting_point = if workflow_definition.key?("steps")
|
64
|
+
workflow_definition["steps"].keys.first
|
65
|
+
else
|
66
|
+
# TODO: add deprecation warning
|
67
|
+
workflow_definition.keys.first
|
68
|
+
end
|
69
|
+
|
65
70
|
record = Execution.create!(
|
66
71
|
idempotency_key: idempotency_key,
|
67
72
|
serialized_job: serialized_job,
|
68
73
|
definition: workflow_definition,
|
69
|
-
recover_to:
|
74
|
+
recover_to: starting_point
|
70
75
|
)
|
71
76
|
end
|
72
77
|
|
73
78
|
record
|
74
79
|
end
|
75
80
|
end
|
76
|
-
@
|
81
|
+
@__acidic_job_context__ ||= Context.new(@__acidic_job_execution__)
|
77
82
|
|
78
|
-
AcidicJob.instrument(:process_workflow, execution: @
|
83
|
+
AcidicJob.instrument(:process_workflow, execution: @__acidic_job_execution__.attributes) do
|
79
84
|
# if the workflow record is already marked as finished, immediately return its result
|
80
|
-
return true if @
|
85
|
+
return true if @__acidic_job_execution__.finished?
|
81
86
|
|
82
87
|
loop do
|
83
|
-
break if @
|
88
|
+
break if @__acidic_job_execution__.finished?
|
84
89
|
|
85
|
-
current_step = @
|
90
|
+
current_step = @__acidic_job_execution__.recover_to
|
86
91
|
|
87
|
-
if not @
|
92
|
+
if not @__acidic_job_execution__.defined?(current_step) # rubocop:disable Style/Not
|
88
93
|
raise UndefinedStepError.new(current_step)
|
89
94
|
end
|
90
95
|
|
91
|
-
step_definition = @
|
96
|
+
step_definition = @__acidic_job_execution__.definition_for(current_step)
|
92
97
|
AcidicJob.instrument(:process_step, **step_definition) do
|
93
98
|
recover_to = catch(:halt) { take_step(step_definition) }
|
94
99
|
case recover_to
|
95
100
|
when HALT_STEP
|
96
|
-
@
|
101
|
+
@__acidic_job_execution__.record!(
|
102
|
+
step: step_definition.fetch("does"),
|
103
|
+
action: :halted,
|
104
|
+
timestamp: Time.now
|
105
|
+
)
|
97
106
|
return true
|
98
107
|
else
|
99
|
-
@
|
108
|
+
@__acidic_job_execution__.update!(recover_to: recover_to)
|
100
109
|
end
|
101
110
|
end
|
102
111
|
end
|
@@ -114,11 +123,19 @@ module AcidicJob
|
|
114
123
|
def step_retrying?
|
115
124
|
step_name = caller_locations.first.label
|
116
125
|
|
117
|
-
if not @
|
126
|
+
if not @__acidic_job_execution__.defined?(step_name) # rubocop:disable Style/IfUnlessModifier, Style/Not
|
118
127
|
raise UndefinedStepError.new(step_name)
|
119
128
|
end
|
120
129
|
|
121
|
-
@
|
130
|
+
@__acidic_job_execution__.entries.where(step: step_name, action: "started").count > 1
|
131
|
+
end
|
132
|
+
|
133
|
+
def execution
|
134
|
+
@__acidic_job_execution__
|
135
|
+
end
|
136
|
+
|
137
|
+
def ctx
|
138
|
+
@__acidic_job_context__
|
122
139
|
end
|
123
140
|
|
124
141
|
private
|
@@ -127,20 +144,19 @@ module AcidicJob
|
|
127
144
|
curr_step = step_definition.fetch("does")
|
128
145
|
next_step = step_definition.fetch("then")
|
129
146
|
|
130
|
-
return next_step if @
|
147
|
+
return next_step if @__acidic_job_execution__.entries.exists?(step: curr_step, action: :succeeded)
|
131
148
|
|
132
|
-
step_method = performable_step_for(step_definition)
|
133
149
|
rescued_error = nil
|
134
150
|
begin
|
135
|
-
@
|
151
|
+
@__acidic_job_execution__.record!(step: curr_step, action: :started, timestamp: Time.now)
|
136
152
|
result = AcidicJob.instrument(:perform_step, **step_definition) do
|
137
|
-
|
153
|
+
perform_step_for(step_definition)
|
138
154
|
end
|
139
155
|
case result
|
140
156
|
when REPEAT_STEP
|
141
157
|
curr_step
|
142
158
|
else
|
143
|
-
@
|
159
|
+
@__acidic_job_execution__.record!(step: curr_step, action: :succeeded, timestamp: Time.now, result: result)
|
144
160
|
next_step
|
145
161
|
end
|
146
162
|
rescue StandardError => e
|
@@ -149,7 +165,7 @@ module AcidicJob
|
|
149
165
|
ensure
|
150
166
|
if rescued_error
|
151
167
|
begin
|
152
|
-
@
|
168
|
+
@__acidic_job_execution__.record!(
|
153
169
|
step: curr_step,
|
154
170
|
action: :errored,
|
155
171
|
timestamp: Time.now,
|
@@ -160,26 +176,47 @@ module AcidicJob
|
|
160
176
|
# We're already inside an error condition, so swallow any additional
|
161
177
|
# errors from here and just send them to logs.
|
162
178
|
logger.error(
|
163
|
-
"Failed to store exception at step #{curr_step} for execution ##{@
|
179
|
+
"Failed to store exception at step #{curr_step} for execution ##{@__acidic_job_execution__.id}: #{e}."
|
164
180
|
)
|
165
181
|
end
|
166
182
|
end
|
167
183
|
end
|
168
184
|
end
|
169
185
|
|
170
|
-
def
|
186
|
+
def perform_step_for(step_definition)
|
171
187
|
step_name = step_definition.fetch("does")
|
172
|
-
|
188
|
+
begin
|
189
|
+
step_method = method(step_name)
|
190
|
+
rescue NameError
|
191
|
+
raise UndefinedMethodError.new(step_name)
|
192
|
+
end
|
173
193
|
|
174
194
|
raise InvalidMethodError.new(step_name) unless step_method.arity.zero?
|
175
195
|
|
176
|
-
|
196
|
+
plugin_pipeline_callable = @__acidic_job_plugins__.reverse.reduce(step_method) do |callable, plugin|
|
197
|
+
context = PluginContext.new(plugin, self, @__acidic_job_execution__, step_definition)
|
177
198
|
|
178
|
-
|
179
|
-
|
199
|
+
if context.inactive?
|
200
|
+
callable
|
201
|
+
else
|
202
|
+
proc do
|
203
|
+
called = false
|
204
|
+
|
205
|
+
result = plugin.around_step(context) do
|
206
|
+
raise DoublePluginCallError.new(plugin, step_name) if called
|
207
|
+
|
208
|
+
called = true
|
209
|
+
callable.call
|
210
|
+
end
|
211
|
+
|
212
|
+
raise MissingPluginCallError.new(plugin, step_name) unless called
|
213
|
+
|
214
|
+
result
|
215
|
+
end
|
216
|
+
end
|
180
217
|
end
|
181
|
-
|
182
|
-
|
218
|
+
|
219
|
+
catch(:repeat) { plugin_pipeline_callable.call }
|
183
220
|
end
|
184
221
|
end
|
185
222
|
end
|
data/lib/acidic_job.rb
CHANGED
@@ -6,6 +6,8 @@ require_relative "acidic_job/errors"
|
|
6
6
|
require_relative "acidic_job/builder"
|
7
7
|
require_relative "acidic_job/context"
|
8
8
|
require_relative "acidic_job/arguments"
|
9
|
+
require_relative "acidic_job/plugin_context"
|
10
|
+
require_relative "acidic_job/plugins/transactional_step"
|
9
11
|
require_relative "acidic_job/log_subscriber"
|
10
12
|
require_relative "acidic_job/workflow"
|
11
13
|
|
@@ -15,13 +17,13 @@ module AcidicJob
|
|
15
17
|
extend self
|
16
18
|
|
17
19
|
DEFAULT_LOGGER = ActiveSupport::Logger.new($stdout)
|
18
|
-
FINISHED_RECOVERY_POINT = "
|
20
|
+
FINISHED_RECOVERY_POINT = "__ACIDIC_JOB_WORKFLOW_FINISHED__"
|
19
21
|
|
20
22
|
mattr_accessor :logger, default: DEFAULT_LOGGER
|
21
23
|
mattr_accessor :connects_to
|
22
24
|
|
23
25
|
def instrument(channel, **options, &block)
|
24
|
-
ActiveSupport::Notifications.instrument("#{channel}.acidic_job", **options, &block)
|
26
|
+
ActiveSupport::Notifications.instrument("#{channel}.acidic_job", **options.deep_symbolize_keys, &block)
|
25
27
|
end
|
26
28
|
|
27
29
|
ActiveSupport.run_load_hooks(:acidic_job, self)
|
@@ -7,24 +7,25 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version
|
|
7
7
|
t.datetime :locked_at, null: true
|
8
8
|
t.string :recover_to, null: true
|
9
9
|
t.json :definition, null: true, default: "{}"
|
10
|
+
|
10
11
|
t.timestamps
|
11
12
|
end
|
12
13
|
|
13
14
|
create_table :acidic_job_entries do |t|
|
14
15
|
t.references :execution, null: false, foreign_key: { to_table: :acidic_job_executions }
|
15
|
-
t.string
|
16
|
-
t.string
|
17
|
-
t.datetime
|
18
|
-
t.json :
|
16
|
+
t.string :step, null: false
|
17
|
+
t.string :action, null: false
|
18
|
+
t.datetime :timestamp, null: false
|
19
|
+
t.json :data, null: true, default: "{}"
|
19
20
|
|
20
21
|
t.timestamps
|
21
22
|
end
|
22
|
-
add_index :acidic_job_entries, [:execution_id, :step]
|
23
|
+
add_index :acidic_job_entries, [:execution_id, :step, :action]
|
23
24
|
|
24
25
|
create_table :acidic_job_values do |t|
|
25
26
|
t.references :execution, null: false, foreign_key: { to_table: :acidic_job_executions }
|
26
|
-
t.string
|
27
|
-
t.json
|
27
|
+
t.string :key, null: false
|
28
|
+
t.json :value, null: false, default: "{}"
|
28
29
|
|
29
30
|
t.timestamps
|
30
31
|
end
|
metadata
CHANGED
@@ -1,15 +1,28 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acidic_job
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.
|
4
|
+
version: 1.0.0.rc3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- fractaledmind
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-05-16 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: json
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: 2.7.0
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 2.7.0
|
13
26
|
- !ruby/object:Gem::Dependency
|
14
27
|
name: activejob
|
15
28
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,20 +79,34 @@ dependencies:
|
|
66
79
|
- - ">="
|
67
80
|
- !ruby/object:Gem::Version
|
68
81
|
version: '7.1'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: actionmailer
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '7.1'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '7.1'
|
69
96
|
- !ruby/object:Gem::Dependency
|
70
97
|
name: chaotic_job
|
71
98
|
requirement: !ruby/object:Gem::Requirement
|
72
99
|
requirements:
|
73
100
|
- - ">="
|
74
101
|
- !ruby/object:Gem::Version
|
75
|
-
version: 0
|
102
|
+
version: '0'
|
76
103
|
type: :development
|
77
104
|
prerelease: false
|
78
105
|
version_requirements: !ruby/object:Gem::Requirement
|
79
106
|
requirements:
|
80
107
|
- - ">="
|
81
108
|
- !ruby/object:Gem::Version
|
82
|
-
version: 0
|
109
|
+
version: '0'
|
83
110
|
- !ruby/object:Gem::Dependency
|
84
111
|
name: combustion
|
85
112
|
requirement: !ruby/object:Gem::Requirement
|
@@ -199,6 +226,7 @@ executables: []
|
|
199
226
|
extensions: []
|
200
227
|
extra_rdoc_files: []
|
201
228
|
files:
|
229
|
+
- ".codacy.yml"
|
202
230
|
- ".github/FUNDING.yml"
|
203
231
|
- ".github/workflows/main.yml"
|
204
232
|
- ".gitignore"
|
@@ -232,6 +260,8 @@ files:
|
|
232
260
|
- lib/acidic_job/engine.rb
|
233
261
|
- lib/acidic_job/errors.rb
|
234
262
|
- lib/acidic_job/log_subscriber.rb
|
263
|
+
- lib/acidic_job/plugin_context.rb
|
264
|
+
- lib/acidic_job/plugins/transactional_step.rb
|
235
265
|
- lib/acidic_job/serializers/exception_serializer.rb
|
236
266
|
- lib/acidic_job/serializers/job_serializer.rb
|
237
267
|
- lib/acidic_job/serializers/new_record_serializer.rb
|
@@ -249,7 +279,6 @@ metadata:
|
|
249
279
|
source_code_uri: https://github.com/fractaledmind/acidic_job
|
250
280
|
changelog_uri: https://github.com/fractaledmind/acidic_job/CHANGELOG.md
|
251
281
|
rubygems_mfa_required: 'true'
|
252
|
-
post_install_message:
|
253
282
|
rdoc_options: []
|
254
283
|
require_paths:
|
255
284
|
- lib
|
@@ -264,8 +293,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
264
293
|
- !ruby/object:Gem::Version
|
265
294
|
version: '0'
|
266
295
|
requirements: []
|
267
|
-
rubygems_version: 3.
|
268
|
-
signing_key:
|
296
|
+
rubygems_version: 3.6.3
|
269
297
|
specification_version: 4
|
270
298
|
summary: Idempotent operations for Rails apps, built on top of ActiveJob.
|
271
299
|
test_files: []
|