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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea7524fb0e637ffdf7df124528a027f41f29891e2185e8776255c1b96ee3b00d
4
- data.tar.gz: f364ede10a8872ffdaa07127cd7db22ed2886ae90605aeeb9d3e35dc3a7b169a
3
+ metadata.gz: 8af9b244cf2343978f26322753e80c779c224ab6942c85de5a8fe05cf0ba55f2
4
+ data.tar.gz: 5f609ee36139400937e2a8b3cae42f721eb72f1abf6633ddbd275aa18c1b2242
5
5
  SHA512:
6
- metadata.gz: ffc3d7f6c3638c8a883421f7e2f82c84739c63165d110faf70c95cd77952dff804b6769c7ac637ec170935d6bf227fd04416b7a75f1b585994376f5fe4965feb
7
- data.tar.gz: ca8d0848c203914fd98fd2252689488b789b24c4ada0f082ba5a3a521463e24f66ec69e1240e087766d6091b2e9d0e5748aaeef81e9a69153b7c6489a0a6e29f
6
+ metadata.gz: 6becc8ed3a1c1c58e24d52753e53edb206854cec472ffb4438e4711ea2a8abcd5adae080c621ab7b8bd30eb386118a51012a4f446e997277da3e9b12e460c696
7
+ data.tar.gz: fe04f817a6c2cab6b2284f7ce0a79b44f452e1c490abac18e6385b801a7ca00860e0910286886da6d70270b508ca54fa3ae4c13369d539e47c3ea4d12e9316d8
data/.codacy.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ exclude_paths:
3
+ - "**/*.md"
4
+ - "LICENSE"
data/Gemfile.lock CHANGED
@@ -1,15 +1,23 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- acidic_job (1.0.0.rc1)
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.2.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.6.3)
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
- chaotic_job (>= 0.2.0)
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
- 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 be anything that `JSON.dump` can handle.
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", ">= 0.2.0"
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
- def started?
8
- action == "started"
9
- end
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 succeeded?
12
- action == "succeeded"
11
+ def self.most_recent
12
+ order(created_at: :desc).first
13
13
  end
14
14
 
15
- def errored?
16
- action == "errored"
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
- recover_to.to_s == FINISHED_RECOVERY_POINT
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
@@ -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, transactional: false)
12
- @steps << { "does" => method_name.to_s, "transactional" => transactional }
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
- {}.tap do |workflow|
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
@@ -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(
@@ -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, action: "Define workflow", **event.payload.slice("job_class", "job_id"))
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, action: "Initialize workflow", **event.payload.slice("steps"))
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, action: "Process workflow", **event.payload["execution"].slice("id", "recover_to"))
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, action: "Process step", **event.payload)
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, action: "Perform step", **event.payload)
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, action: "Record entry", **event.payload.slice(:step, :action, :timestamp))
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, action:, **attributes)
34
- "AcidicJob-#{AcidicJob::VERSION} #{action} (#{event.duration.round(1)}ms) #{formatted_attributes(**attributes)}"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AcidicJob
4
- VERSION = "1.0.0.rc1"
4
+ VERSION = "1.0.0.rc3"
5
5
  end
@@ -4,33 +4,31 @@ require "active_job"
4
4
 
5
5
  module AcidicJob
6
6
  module Workflow
7
- NO_OP_WRAPPER = proc { |&block| block.call }
8
- REPEAT_STEP = :REPEAT_STEP
9
- HALT_STEP = :HALT_STEP
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
- attr_reader :execution, :ctx
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? @_builder
16
+ raise RedefiningWorkflowError if defined? @__acidic_job_builder__
19
17
 
20
- @_builder = Builder.new
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 @_builder
23
+ block.call @__acidic_job_builder__
26
24
 
27
- raise MissingStepsError if @_builder.steps.empty?
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
- @_builder.define_workflow
28
+ @__acidic_job_builder__.define_workflow
31
29
  end
32
30
 
33
- AcidicJob.instrument(:initialize_workflow, "definition" => workflow_definition) do
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.dump([self.class.name, unique_by]))
39
+ idempotency_key = Digest::SHA256.hexdigest(JSON.fast_generate([self.class.name, unique_by], strict: true))
42
40
 
43
- @execution = ::ActiveRecord::Base.transaction(**transaction_args) do
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: workflow_definition.keys.first
74
+ recover_to: starting_point
70
75
  )
71
76
  end
72
77
 
73
78
  record
74
79
  end
75
80
  end
76
- @ctx ||= Context.new(@execution)
81
+ @__acidic_job_context__ ||= Context.new(@__acidic_job_execution__)
77
82
 
78
- AcidicJob.instrument(:process_workflow, execution: @execution.attributes) do
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 @execution.finished?
85
+ return true if @__acidic_job_execution__.finished?
81
86
 
82
87
  loop do
83
- break if @execution.finished?
88
+ break if @__acidic_job_execution__.finished?
84
89
 
85
- current_step = @execution.recover_to
90
+ current_step = @__acidic_job_execution__.recover_to
86
91
 
87
- if not @execution.definition.key?(current_step) # rubocop:disable Style/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 = @execution.definition[current_step]
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
- @execution.record!(step: step_definition.fetch("does"), action: :halted, timestamp: Time.now)
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
- @execution.update!(recover_to: recover_to)
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 @execution.definition.key?(step_name) # rubocop:disable Style/IfUnlessModifier, Style/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
- @execution.entries.where(step: step_name, action: "started").count > 1
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 @execution.entries.exists?(step: curr_step, action: :succeeded)
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
- @execution.record!(step: curr_step, action: :started, timestamp: Time.now)
151
+ @__acidic_job_execution__.record!(step: curr_step, action: :started, timestamp: Time.now)
136
152
  result = AcidicJob.instrument(:perform_step, **step_definition) do
137
- step_method.call
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
- @execution.record!(step: curr_step, action: :succeeded, timestamp: Time.now, result: result)
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
- @execution.record!(
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 ##{@execution.id} because of #{e}."
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 performable_step_for(step_definition)
186
+ def perform_step_for(step_definition)
171
187
  step_name = step_definition.fetch("does")
172
- step_method = method(step_name)
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
- wrapper = step_definition["transactional"] ? @execution.method(:with_lock) : NO_OP_WRAPPER
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
- proc do
179
- catch(:repeat) { wrapper.call { step_method.call } }
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
- rescue NameError
182
- raise UndefinedMethodError.new(step_name)
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 = "FINISHED"
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 :step, null: false
16
- t.string :action, null: false
17
- t.datetime :timestamp, null: false
18
- t.json :data
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 :key, null: false
27
- t.json :value, null: false, default: "{}"
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.rc1
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: 2024-11-17 00:00:00.000000000 Z
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.2.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.2.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.5.21
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: []