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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.codacy.yml +4 -0
  3. data/.github/FUNDING.yml +13 -0
  4. data/.github/workflows/main.yml +12 -15
  5. data/.gitignore +3 -1
  6. data/.rubocop.yml +50 -5
  7. data/.ruby-version +1 -0
  8. data/Gemfile.lock +134 -193
  9. data/README.md +164 -246
  10. data/TODO +77 -0
  11. data/acidic_job.gemspec +10 -10
  12. data/app/models/acidic_job/entry.rb +19 -0
  13. data/app/models/acidic_job/execution.rb +50 -0
  14. data/app/models/acidic_job/record.rb +11 -0
  15. data/app/models/acidic_job/value.rb +7 -0
  16. data/bin/console +5 -2
  17. data/bin/test_all +26 -0
  18. data/gemfiles/rails_7.0.gemfile +4 -1
  19. data/gemfiles/rails_7.1.gemfile +11 -0
  20. data/gemfiles/rails_7.2.gemfile +11 -0
  21. data/gemfiles/rails_8.0.gemfile +11 -0
  22. data/lib/acidic_job/arguments.rb +31 -0
  23. data/lib/acidic_job/builder.rb +29 -0
  24. data/lib/acidic_job/context.rb +46 -0
  25. data/lib/acidic_job/engine.rb +46 -0
  26. data/lib/acidic_job/errors.rb +87 -12
  27. data/lib/acidic_job/log_subscriber.rb +50 -0
  28. data/lib/acidic_job/serializers/exception_serializer.rb +31 -0
  29. data/lib/acidic_job/serializers/job_serializer.rb +27 -0
  30. data/lib/acidic_job/serializers/new_record_serializer.rb +25 -0
  31. data/lib/acidic_job/serializers/range_serializer.rb +28 -0
  32. data/lib/acidic_job/testing.rb +8 -12
  33. data/lib/acidic_job/version.rb +1 -1
  34. data/lib/acidic_job/workflow.rb +182 -0
  35. data/lib/acidic_job.rb +15 -284
  36. data/lib/generators/acidic_job/install_generator.rb +3 -3
  37. data/lib/generators/acidic_job/templates/create_acidic_job_tables_migration.rb.erb +33 -0
  38. metadata +51 -95
  39. data/.ruby_version +0 -1
  40. data/.tool-versions +0 -1
  41. data/gemfiles/rails_6.1.gemfile +0 -8
  42. data/lib/acidic_job/awaiting.rb +0 -102
  43. data/lib/acidic_job/extensions/action_mailer.rb +0 -29
  44. data/lib/acidic_job/extensions/active_job.rb +0 -40
  45. data/lib/acidic_job/extensions/noticed.rb +0 -54
  46. data/lib/acidic_job/extensions/sidekiq.rb +0 -111
  47. data/lib/acidic_job/finished_point.rb +0 -16
  48. data/lib/acidic_job/idempotency_key.rb +0 -82
  49. data/lib/acidic_job/perform_wrapper.rb +0 -22
  50. data/lib/acidic_job/recovery_point.rb +0 -18
  51. data/lib/acidic_job/rspec_configuration.rb +0 -31
  52. data/lib/acidic_job/run.rb +0 -100
  53. data/lib/acidic_job/serializer.rb +0 -163
  54. data/lib/acidic_job/staging.rb +0 -38
  55. data/lib/acidic_job/step.rb +0 -104
  56. data/lib/acidic_job/test_case.rb +0 -9
  57. data/lib/acidic_job/upgrade_service.rb +0 -118
  58. data/lib/generators/acidic_job/drop_tables_generator.rb +0 -26
  59. data/lib/generators/acidic_job/templates/create_acidic_job_runs_migration.rb.erb +0 -19
  60. data/lib/generators/acidic_job/templates/drop_acidic_job_keys_migration.rb.erb +0 -27
data/TODO ADDED
@@ -0,0 +1,77 @@
1
+ - [x] Track number of runs per execution (resolves #79)
2
+ This is accomplished with the new `entries` table, as proven by the `idempotency_check` feature, which only runs on a retry.
3
+
4
+ - [x] Store errors with more context and without full error history (resolves #80)
5
+ This is also accomplished with the new `entries` table, by storing a separate error entry for any raised errors.
6
+
7
+ - [x] All traversal over a collection added to the context within the workflow (resolves #81)
8
+ The new collection traversal logic resolves #81, as proven by the test case. You can traverse a key for a context value. For example,
9
+ ```ruby
10
+ execute :prepare_collection
11
+ traverse :collection, using: :process_item
12
+
13
+ def prepare_collection = @ctx[:collection] = 1..5
14
+ def process_item(item) = @processed_items << item
15
+ ```
16
+
17
+ - [x] Handle case where non-idempotent external write succeeds but recording fails (resolves #82)
18
+ The new `idempotency_check` option accomplishes this, by allowing a step making external IO to define a method that checks if the write was already successfully made when re-running a step in a retry.
19
+
20
+ - [x] Ensure any and all exceptions can be serialized and deserialized (resolves #83)
21
+ The original serializer was too naive and couldn't deserialize, for example, an `ActionView::Template::Error` which requires that `$!` be set when initializing the object.
22
+
23
+ The new `ExceptionSerializer` uses Ruby's built-in YAML encoding of exceptions to serialize and deserialize the objects, laying on Zlib to ensure compact byte storage.
24
+
25
+ - [x] Handle duplicate workflow step names (resolves #87)
26
+ Adding a `DuplicateStepError` exception raised when building the workflow definition resolved this issue.
27
+
28
+ - [ ] Add a `context` step type (resolves #89)
29
+ Use case: the workflow may or may not have the data it needs to proceed. If it doesn't, it should make an API call to fetch the data. The API call may take a while, so we should be able to handle that as well.
30
+
31
+ Example usage:
32
+ ```ruby
33
+ context :slack_author, fallback: :fetch_slack_author
34
+
35
+ def slack_author
36
+ Slack::Profile.find_by(uid: @user_uid)
37
+ end
38
+
39
+ def fetch_slack_author
40
+ api_response = Slack::Client.new().get_user(uid: @user_uid)
41
+ if api_response[:ok]
42
+ [Slack::ProcessProfileSlackJob.new(@installation, api_response[:user])]
43
+ else
44
+ raise DoNotRetryJob
45
+ end
46
+ end
47
+ ```
48
+
49
+ Psuedo code for implementation:
50
+ ```ruby
51
+ result = resolve_method(primary_method_name).call
52
+ if result
53
+ @ctx[primary_method_name] = result
54
+ else
55
+ fallback = resolve_method(fallback_method_name).call
56
+ case fallback
57
+ in ActiveJob::Base then awaits(fallback)
58
+ in Array[ActiveJob::Base] then awaits(fallback)
59
+ else @ctx[primary_method_name] = result
60
+ end
61
+ end
62
+ ```
63
+
64
+ - [ ] Add documentation on how to "migrate" a workflow (resolves #90)
65
+ The v1 alpha already includes a check that the persisted workflow definition matches the job's current definition. This ensures that no job tries to run with an outdated definition. However, we should document the proper way to update a workflow definition.
66
+
67
+ The process can only be additive, like a strong migration. First, you create a new job with a new name that is a clone of the original job. Make the necessary changes to the new job. Update your codebase to only enqueue the new job. Deploy this change, where both the new job and the old jobs exist, but the application only enqueues the new job. Once deployed, wait until all currently running instances of the old job complete (provide some docs on how to check this). Once all old job instances are complete, you can safely delete the old job and deploy that change. This process ensures that no job is running with an outdated definition.
68
+
69
+ - [ ] Automatically retry the serializable transaction for find/create the Run record (resolves #91)
70
+ Serializable transactions are prone to failure: https://stackoverflow.com/a/21715207/2884386
71
+
72
+ Currently, if the transaction fails, the job will fail. We need to build automatic retries into the gem, as this failure will naturally resolve. But, we should also add a limit to the number of retries to prevent infinite loops.
73
+
74
+ - [ ] Ensure the gem works with GoodJob (resolves #92 and #94)
75
+ In the current version, GoodJob can't handle messing with the `job_id` and retrying a failed job raises a NoMethodError: "undefined method `utc' for an instance of Float".
76
+
77
+ - [ ] Ensure users can transactionally enqueue other entities, like ActionMailer or Noticed objects
data/acidic_job.gemspec CHANGED
@@ -27,24 +27,24 @@ 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 "activerecord"
31
- spec.add_dependency "activesupport"
32
- spec.add_dependency "database_cleaner"
33
- spec.add_development_dependency "activejob"
30
+ spec.add_dependency "json", ">= 2.7.0" # see: https://github.com/ruby/json/pull/519
31
+ ">= 7.1".tap do |rails_version|
32
+ spec.add_dependency "activejob", rails_version
33
+ spec.add_dependency "activerecord", rails_version
34
+ spec.add_dependency "activesupport", rails_version
35
+ spec.add_dependency "railties", rails_version
36
+ spec.add_development_dependency "actionmailer", rails_version
37
+ end
38
+
39
+ spec.add_development_dependency "chaotic_job"
34
40
  spec.add_development_dependency "combustion"
35
41
  spec.add_development_dependency "minitest"
36
- spec.add_development_dependency "net-smtp"
37
- spec.add_development_dependency "noticed"
38
- spec.add_development_dependency "psych", "> 4.0"
39
- spec.add_development_dependency "railties"
40
42
  spec.add_development_dependency "rake"
41
43
  spec.add_development_dependency "rubocop"
42
44
  spec.add_development_dependency "rubocop-minitest"
43
45
  spec.add_development_dependency "rubocop-rake"
44
- spec.add_development_dependency "sidekiq"
45
46
  spec.add_development_dependency "simplecov"
46
47
  spec.add_development_dependency "sqlite3"
47
- spec.add_development_dependency "warning"
48
48
 
49
49
  # For more information and examples about making a new gem, checkout our
50
50
  # guide at: https://bundler.io/guides/creating_gem.html
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AcidicJob
4
+ class Entry < Record
5
+ belongs_to :execution, class_name: "AcidicJob::Execution"
6
+
7
+ def started?
8
+ action == "started"
9
+ end
10
+
11
+ def succeeded?
12
+ action == "succeeded"
13
+ end
14
+
15
+ def errored?
16
+ action == "errored"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AcidicJob
4
+ class Execution < Record
5
+ has_many :entries, class_name: "AcidicJob::Entry"
6
+ has_many :values, class_name: "AcidicJob::Value"
7
+
8
+ validates :idempotency_key, presence: true # uniqueness constraint is enforced at the database level
9
+ validates :serialized_job, presence: true
10
+
11
+ scope :finished, -> { where(recover_to: FINISHED_RECOVERY_POINT) }
12
+ scope :outstanding, lambda {
13
+ where.not(recover_to: FINISHED_RECOVERY_POINT).or(where(recover_to: [nil, ""]))
14
+ }
15
+
16
+ def record!(step:, action:, timestamp:, **kwargs)
17
+ AcidicJob.instrument(:record_entry, step: step, action: action, timestamp: timestamp, data: kwargs) do
18
+ entries.create!(
19
+ step: step,
20
+ action: action,
21
+ timestamp: timestamp,
22
+ data: kwargs.stringify_keys!
23
+ )
24
+ end
25
+ end
26
+
27
+ def context
28
+ @context ||= Context.new(self)
29
+ end
30
+
31
+ def finished?
32
+ recover_to.to_s == FINISHED_RECOVERY_POINT
33
+ end
34
+
35
+ def deserialized_job
36
+ serialized_job["job_class"].constantize.new.tap do |job|
37
+ job.deserialize(serialized_job)
38
+ end
39
+ end
40
+
41
+ def raw_arguments
42
+ JSON.parse(serialized_job_before_type_cast)["arguments"]
43
+ end
44
+
45
+ def enqueue_job
46
+ deserialized_job.enqueue
47
+ true
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AcidicJob
4
+ class Record < ActiveRecord::Base
5
+ self.abstract_class = true
6
+
7
+ connects_to(**AcidicJob.connects_to) if AcidicJob.connects_to
8
+ end
9
+ end
10
+
11
+ ActiveSupport.run_load_hooks :acidic_job_record, AcidicJob::Record
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AcidicJob
4
+ class Value < Record
5
+ belongs_to :execution, class_name: "AcidicJob::Execution"
6
+ end
7
+ end
data/bin/console CHANGED
@@ -2,13 +2,16 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "bundler/setup"
5
+ require "rails"
5
6
  require "acidic_job"
6
7
 
7
8
  # You can add fixtures and/or initialization code here to make experimenting
8
9
  # with your gem easier. You can also use a different console, if you like.
9
10
 
10
- require_relative "../test/support/setup"
11
- require_relative "../test/support/ride_create_job"
11
+ require "combustion"
12
+ require "sqlite3"
13
+ Combustion.path = "test/combustion"
14
+ Combustion.initialize! :active_record
12
15
 
13
16
  # (If you use this, don't forget to add pry to your Gemfile!)
14
17
  # require "pry"
data/bin/test_all ADDED
@@ -0,0 +1,26 @@
1
+ run_tests() {
2
+ if [ "$#" -ne 2 ]; then
3
+ echo "Usage: run_tests RUBY_VERSION RAILS_VERSION"
4
+ echo "Example: run_tests 3.0.7 7.1"
5
+ return 1
6
+ fi
7
+
8
+ local ruby="$1"
9
+ local gemfile="gemfiles/rails_$2.gemfile"
10
+
11
+ echo "**************************************************"
12
+ echo "Running tests with Ruby $1 and Rails $2..."
13
+ ASDF_RUBY_VERSION=$ruby BUNDLE_GEMFILE=$gemfile bundle check ||
14
+ ASDF_RUBY_VERSION=$ruby BUNDLE_GEMFILE=$gemfile bundle install &&
15
+ ASDF_RUBY_VERSION=$ruby BUNDLE_GEMFILE=$gemfile bundle exec rake
16
+ }
17
+
18
+ run_tests "3.0.7" "7.1"
19
+ run_tests "3.1.6" "7.1"
20
+ run_tests "3.1.6" "7.2"
21
+ run_tests "3.2.5" "7.1"
22
+ run_tests "3.2.5" "7.2"
23
+ run_tests "3.2.5" "8.0"
24
+ run_tests "3.3.5" "7.1"
25
+ run_tests "3.3.5" "7.2"
26
+ run_tests "3.3.5" "8.0"
@@ -2,7 +2,10 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "activemodel", "~> 7.0.0"
5
+ gem "activejob", "~> 7.0.0"
6
+ gem "activerecord", "~> 7.0.0"
7
+ gem "activesupport", "~> 7.0.0"
6
8
  gem "railties", "~> 7.0.0"
9
+ gem "sqlite3", "~> 1.7.3"
7
10
 
8
11
  gemspec path: "../"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activejob", "~> 7.1.0"
6
+ gem "activerecord", "~> 7.1.0"
7
+ gem "activesupport", "~> 7.1.0"
8
+ gem "railties", "~> 7.1.0"
9
+ gem "sqlite3", ">= 2.0.0"
10
+
11
+ gemspec path: "../"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activejob", "~> 7.2.0"
6
+ gem "activerecord", "~> 7.2.0"
7
+ gem "activesupport", "~> 7.2.0"
8
+ gem "railties", "~> 7.2.0"
9
+ gem "sqlite3", ">= 2.0.0"
10
+
11
+ gemspec path: "../"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activejob", "~> 8.0.0.rc1"
6
+ gem "activerecord", "~> 8.0.0.rc1"
7
+ gem "activesupport", "~> 8.0.0.rc1"
8
+ gem "railties", "~> 8.0.0.rc1"
9
+ gem "sqlite3", ">= 2.1.0"
10
+
11
+ gemspec path: "../"
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job/arguments"
4
+
5
+ module AcidicJob
6
+ module Arguments
7
+ include ::ActiveJob::Arguments
8
+
9
+ extend self
10
+
11
+ # `ActiveJob` will throw an error if it tries to deserialize a GlobalID record.
12
+ # However, this isn't the behavior that we want for our custom `ActiveRecord` serializer.
13
+ # Since `ActiveRecord` does _not_ reset instance record state to its pre-transactional state
14
+ # on a transaction ROLLBACK, we can have GlobalID entries in a serialized column that point to
15
+ # non-persisted records. This is ok. We should simply return `nil` for that portion of the
16
+ # serialized field.
17
+ def deserialize_global_id(hash)
18
+ ::GlobalID::Locator.locate hash[GLOBALID_KEY]
19
+ rescue ::ActiveRecord::RecordNotFound
20
+ nil
21
+ end
22
+
23
+ # In order to allow our `NewRecordSerializer` a chance to work, we need to ensure that
24
+ # ActiveJob's first attempt to serialize an ActiveRecord model doesn't throw an exception.
25
+ def convert_to_global_id_hash(argument)
26
+ { GLOBALID_KEY => argument.to_global_id.to_s }
27
+ rescue ::URI::GID::MissingModelIdError
28
+ ::ActiveJob::Serializers.serialize(argument)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AcidicJob
4
+ class Builder
5
+ attr_reader :steps
6
+
7
+ def initialize
8
+ @steps = []
9
+ end
10
+
11
+ def step(method_name, transactional: false)
12
+ @steps << { "does" => method_name.to_s, "transactional" => transactional }
13
+ @steps
14
+ end
15
+
16
+ def define_workflow
17
+ # [ { does: "step 1", transactional: true }, { does: "step 2", transactional: false }, ... ]
18
+ @steps << { "does" => FINISHED_RECOVERY_POINT }
19
+
20
+ {}.tap do |workflow|
21
+ @steps.each_cons(2).map do |enter_step, exit_step|
22
+ enter_name = enter_step["does"]
23
+ workflow[enter_name] = enter_step.merge("then" => exit_step["does"])
24
+ end
25
+ end
26
+ # { "step 1": { does: "step 1", transactional: true, then: "step 2" }, ... }
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AcidicJob
4
+ class Context
5
+ def initialize(execution)
6
+ @execution = execution
7
+ end
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
29
+ def []=(key, value)
30
+ AcidicJob.instrument(:set_context, key: key, value: value) do
31
+ AcidicJob::Value.upsert(
32
+ { execution_id: @execution.id,
33
+ key: key,
34
+ value: value },
35
+ unique_by: %i[execution_id key]
36
+ )
37
+ end
38
+ end
39
+
40
+ def [](key)
41
+ AcidicJob.instrument(:get_context, key: key) do
42
+ @execution.values.select(:value).find_by(key: key)&.value
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AcidicJob
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace AcidicJob
6
+
7
+ config.acidic_job = ActiveSupport::OrderedOptions.new
8
+
9
+ initializer "acidic_job.config" do
10
+ config.acidic_job.each do |name, value|
11
+ AcidicJob.public_send("#{name}=", value)
12
+ end
13
+ end
14
+
15
+ initializer "acidic_job.logger" do
16
+ ActiveSupport.on_load :acidic_job do
17
+ self.logger = ::Rails.logger if logger == AcidicJob::DEFAULT_LOGGER
18
+ end
19
+
20
+ AcidicJob::LogSubscriber.attach_to :acidic_job
21
+ end
22
+
23
+ initializer "acidic_job.active_job.extensions" do
24
+ ActiveSupport.on_load :active_job do
25
+ require "active_job/serializers"
26
+ require_relative "serializers/exception_serializer"
27
+ require_relative "serializers/new_record_serializer"
28
+ require_relative "serializers/job_serializer"
29
+ require_relative "serializers/range_serializer"
30
+
31
+ ActiveJob::Serializers.add_serializers(
32
+ Serializers::ExceptionSerializer,
33
+ Serializers::NewRecordSerializer,
34
+ Serializers::JobSerializer,
35
+ Serializers::RangeSerializer
36
+ )
37
+ end
38
+ end
39
+
40
+ # :nocov:
41
+ generators do
42
+ require "generators/acidic_job/install_generator"
43
+ end
44
+ # :nocov:
45
+ end
46
+ end
@@ -1,27 +1,102 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AcidicJob
4
- class Error < StandardError; end
4
+ class Error < StandardError
5
+ end
5
6
 
6
- class MismatchedIdempotencyKeyAndJobArguments < Error; end
7
+ class RedefiningWorkflowError < Error
8
+ def message
9
+ "can only call `execute_workflow` once within a job"
10
+ end
11
+ end
7
12
 
8
- class LockedIdempotencyKey < Error; end
13
+ class UndefinedWorkflowBlockError < Error
14
+ def message
15
+ "block must be passed to `execute_workflow`"
16
+ end
17
+ end
9
18
 
10
- class UnknownRecoveryPoint < Error; end
19
+ class InvalidWorkflowBlockError < Error
20
+ def message
21
+ "workflow builder must be yielded to the `execute_workflow` block"
22
+ end
23
+ end
11
24
 
12
- class UnknownAtomicPhaseType < Error; end
25
+ class MissingStepsError < Error
26
+ def message
27
+ "workflow must define at least one step"
28
+ end
29
+ end
13
30
 
14
- class SerializedTransactionConflict < Error; end
31
+ # rubocop:disable Lint/MissingSuper
32
+ class ArgumentMismatchError < Error
33
+ def initialize(expected, existing)
34
+ @expected = expected
35
+ @existing = existing
36
+ end
15
37
 
16
- class UnknownJobAdapter < Error; end
38
+ def message
39
+ <<~TXT
40
+ existing execution's arguments do not match
41
+ existing: #{@existing.inspect}
42
+ expected: #{@expected.inspect}
43
+ TXT
44
+ end
45
+ end
17
46
 
18
- class NoDefinedSteps < Error; end
47
+ class DefinitionMismatchError < Error
48
+ def initialize(expected, existing)
49
+ @expected = expected
50
+ @existing = existing
51
+ end
19
52
 
20
- class SidekiqBatchRequired < Error; end
53
+ def message
54
+ <<~TXT
55
+ existing execution's definition does not match
56
+ existing: #{@existing.inspect}
57
+ expected: #{@expected.inspect}
58
+ TXT
59
+ end
60
+ end
21
61
 
22
- class TooManyParametersForStepMethod < Error; end
62
+ class UndefinedStepError < Error
63
+ def initialize(step)
64
+ @step = step
65
+ end
23
66
 
24
- class TooManyParametersForParallelJob < Error; end
67
+ def message
68
+ "workflow does not reference this step: #{@step.inspect}"
69
+ end
70
+ end
25
71
 
26
- class UnknownSerializedJobIdentifier < Error; end
72
+ class SucceededStepError < Error
73
+ def initialize(step)
74
+ @step = step
75
+ end
76
+
77
+ def message
78
+ "workflow has already recorded this step as succeeded: #{@step.inspect}"
79
+ end
80
+ end
81
+
82
+ class UndefinedMethodError < Error
83
+ def initialize(step)
84
+ @step = step
85
+ end
86
+
87
+ def message
88
+ "Undefined step method: #{@step.inspect}"
89
+ end
90
+ end
91
+
92
+ class InvalidMethodError < Error
93
+ def initialize(step)
94
+ @step = step
95
+ end
96
+
97
+ def message
98
+ "step method cannot expect arguments: #{@step.inspect}"
99
+ end
100
+ end
101
+ # rubocop:enable Lint/MissingSuper
27
102
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/log_subscriber"
4
+
5
+ module AcidicJob
6
+ class LogSubscriber < ActiveSupport::LogSubscriber
7
+ def define_workflow(event)
8
+ debug formatted_event(event, action: "Define workflow", **event.payload.slice("job_class", "job_id"))
9
+ end
10
+
11
+ def initialize_workflow(event)
12
+ debug formatted_event(event, action: "Initialize workflow", **event.payload.slice("steps"))
13
+ end
14
+
15
+ def process_workflow(event)
16
+ debug formatted_event(event, action: "Process workflow", **event.payload["execution"].slice("id", "recover_to"))
17
+ end
18
+
19
+ def process_step(event)
20
+ debug formatted_event(event, action: "Process step", **event.payload)
21
+ end
22
+
23
+ def perform_step(event)
24
+ debug formatted_event(event, action: "Perform step", **event.payload)
25
+ end
26
+
27
+ def record_entry(event)
28
+ debug formatted_event(event, action: "Record entry", **event.payload.slice(:step, :action, :timestamp))
29
+ end
30
+
31
+ private
32
+
33
+ def formatted_event(event, action:, **attributes)
34
+ "AcidicJob-#{AcidicJob::VERSION} #{action} (#{event.duration.round(1)}ms) #{formatted_attributes(**attributes)}"
35
+ end
36
+
37
+ def formatted_attributes(**attributes)
38
+ attributes.map { |attr, value| "#{attr}: #{value.inspect}" }.join(", ")
39
+ end
40
+
41
+ def formatted_error(error)
42
+ [error.class, error.message].compact.join(" ")
43
+ end
44
+
45
+ # Use the logger configured for AcidicJob
46
+ def logger
47
+ AcidicJob.logger
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job/serializers/object_serializer"
4
+ require "zlib"
5
+ require "yaml"
6
+
7
+ module AcidicJob
8
+ module Serializers
9
+ class ExceptionSerializer < ::ActiveJob::Serializers::ObjectSerializer
10
+ def serialize(exception)
11
+ compressed = Zlib::Deflate.deflate(exception.to_yaml)
12
+
13
+ super("deflated_yaml" => compressed)
14
+ end
15
+
16
+ def deserialize(hash)
17
+ uncompressed = Zlib::Inflate.inflate(hash["deflated_yaml"])
18
+
19
+ if YAML.respond_to?(:unsafe_load)
20
+ YAML.unsafe_load(uncompressed)
21
+ else
22
+ YAML.load(uncompressed) # rubocop:disable Security/YAMLLoad
23
+ end
24
+ end
25
+
26
+ def serialize?(argument)
27
+ defined?(Exception) && argument.is_a?(Exception)
28
+ end
29
+ end
30
+ end
31
+ end