acidic_job 0.4.0 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 82e7abf5e57a4a6b68691e3c6c69ee8a6bbc2eabcedb69a3ef29e91ac0e99e8c
4
- data.tar.gz: b73584561b8a558529b2e057e449896dedfff2c21ab859dacf82b070f75c7910
3
+ metadata.gz: e4041a344bd53943f281696cafdd0b0a7fa05969b33ac849475f39efd9333018
4
+ data.tar.gz: 73a7b1175df2b8e8b7ce6e89cbc13bce208e575b662a31ea03f11ed66fbd0961
5
5
  SHA512:
6
- metadata.gz: 6e42251e87faff4790ecfba572eca0dd70251fc58a4293b906560b526efa4bc14db00e01ee0e90c43a0d3f3dc355b36b8db3a4be5129529c071711f3a0b2506f
7
- data.tar.gz: 167aa9f07dd033a5a3646edcff171a8dffea485d9cbb943c230526214c6d4eaa68761418321cc4e7795a0326361699181bd142377516ceadaa430d286628aea5
6
+ metadata.gz: bb042126f12c372d0d3767d785d095aa68b48bb71faa10866fa7de6216b47ff7e885ef55611b10cd1d65f9d53aff515a7723fc429ee3095dc3b6ac749e0f761d
7
+ data.tar.gz: 8cbf5b168781b8ea1125382b998492e6f6d38cf1b46dc8c766b432e93f19e786547dc5dfc8f5437b5623d33970d67d090857abdcd58a5eb5e80804fc682efd36
data/Gemfile CHANGED
@@ -26,3 +26,5 @@ gem "database_cleaner"
26
26
  gem "simplecov"
27
27
 
28
28
  gem "pry"
29
+
30
+ gem "sidekiq"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- acidic_job (0.4.0)
4
+ acidic_job (0.5.3)
5
5
  activerecord (>= 4.0.0)
6
6
  activesupport
7
7
 
@@ -39,6 +39,7 @@ GEM
39
39
  builder (3.2.4)
40
40
  coderay (1.1.3)
41
41
  concurrent-ruby (1.1.9)
42
+ connection_pool (2.2.5)
42
43
  crass (1.0.6)
43
44
  database_cleaner (2.0.1)
44
45
  database_cleaner-active_record (~> 2.0.0)
@@ -86,6 +87,7 @@ GEM
86
87
  thor (~> 1.0)
87
88
  rainbow (3.0.0)
88
89
  rake (13.0.4)
90
+ redis (4.4.0)
89
91
  regexp_parser (2.1.1)
90
92
  rexml (3.2.5)
91
93
  rubocop (1.18.3)
@@ -104,6 +106,10 @@ GEM
104
106
  rubocop-rake (0.6.0)
105
107
  rubocop (~> 1.0)
106
108
  ruby-progressbar (1.11.0)
109
+ sidekiq (6.2.2)
110
+ connection_pool (>= 2.2.2)
111
+ rack (~> 2.0)
112
+ redis (>= 4.2.0)
107
113
  simplecov (0.21.2)
108
114
  docile (~> 1.1)
109
115
  simplecov-html (~> 0.11)
@@ -133,6 +139,7 @@ DEPENDENCIES
133
139
  rubocop (~> 1.7)
134
140
  rubocop-minitest
135
141
  rubocop-rake
142
+ sidekiq
136
143
  simplecov
137
144
  sqlite3
138
145
 
data/blog_post.md ADDED
@@ -0,0 +1,28 @@
1
+ # ACIDic Operations in Rails
2
+
3
+ At the conceptual heart of basically any software are "operations"—the discrete actions the software performs. At the horizon of basically any software is the goal to make that sofware _robust_. Typically, one makes a software system robust by making each of its operations robust. Moreover, typically, robustness in software is considered as the software being "ACIDic"—atomic, consistent, isolated, durable.
4
+
5
+ In a loosely collected series of articles, Brandur Leach lays out the core techniques and principles required to make an HTTP API properly ACIDic:
6
+
7
+ 1. https://brandur.org/acid
8
+ 2. https://brandur.org/http-transactions
9
+ 3. https://brandur.org/job-drain
10
+ 4. https://brandur.org/idempotency-keys
11
+
12
+ With these techniques and principles in mind, our challenge is bring them into the world of a standard Rails application. This will require us to conceptually map the concepts of an HTTP request, an API server action, and an HTTP response into the world of a running Rails process.
13
+
14
+ We can begin to make this mapping by observing that an API server action is a specific instantiation of the general concept of an "operation". Like all operations, it has a "trigger" (the HTTP request) and a "response" (the HTTP response). So, what we need is a foundation upon which to build our Rails "operations".
15
+
16
+ In order to help us find that tool, let us consider the necessary characteristics we need. We need something that we can easily trigger from other Ruby code throughout our Rails application (controller actions, model methods, model callbacks, etc.). It should also be able to be run both synchronously (blocking execution and then returning its response to the caller) and asychronously (non-blocking and the caller doesn't know its response). It should then also be able to retry a specific operation (in much the way that an API consumer can "retry an operation" by hitting the same endpoint with the same request).
17
+
18
+ As we lay out these characteristics, I imagine your mind is going where mine went—`ActiveJob` gives us a solid foundation upon which we can build "ACIDic" operations.
19
+
20
+ So, our challenge to build tooling which will allow us to make "operational" jobs _robust_.
21
+
22
+ What we need primarily is to be able to make our jobs *idempotent*, and one of the simplest yet still most powerful tools for making an operation idempotent is the idempotency key. As laid out in the article linked above, an idempotency key is a record that we store in our database to uniquely identify a particular execution of an operation and a related "recovery point" for where we are in the process of that operation.
23
+
24
+
25
+
26
+
27
+
28
+
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AcidicJob
4
+ class Error < StandardError; end
5
+
6
+ class MismatchedIdempotencyKeyAndJobArguments < Error; end
7
+
8
+ class LockedIdempotencyKey < Error; end
9
+
10
+ class UnknownRecoveryPoint < Error; end
11
+
12
+ class UnknownAtomicPhaseType < Error; end
13
+
14
+ class SerializedTransactionConflict < Error; end
15
+
16
+ class UnknownJobAdapter < Error; end
17
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_record"
4
+
3
5
  module AcidicJob
4
6
  class Key < ActiveRecord::Base
5
7
  RECOVERY_POINT_FINISHED = "FINISHED"
@@ -8,6 +10,7 @@ module AcidicJob
8
10
 
9
11
  serialize :error_object
10
12
  serialize :job_args
13
+ store :attr_accessors
11
14
 
12
15
  validates :idempotency_key, presence: true, uniqueness: { scope: %i[job_name job_args] }
13
16
  validates :job_name, presence: true
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module AcidicJob
6
+ module PerformTransactionallyExtension
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ # rubocop:disable Metrics/MethodLength
11
+ def perform_transactionally(*args)
12
+ attributes = if self < ActiveJob::Base
13
+ {
14
+ adapter: "activejob",
15
+ job_name: name,
16
+ job_args: job_or_instantiate(*args).serialize
17
+ }
18
+ elsif include? Sidekiq::Worker
19
+ {
20
+ adapter: "sidekiq",
21
+ job_name: name,
22
+ job_args: args
23
+ }
24
+ else
25
+ raise UnknownJobAdapter
26
+ end
27
+
28
+ AcidicJob::Staged.create!(attributes)
29
+ end
30
+ # rubocop:enable Metrics/MethodLength
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AcidicJob
4
+ module PerformWrapper
5
+ def perform(*args, **kwargs)
6
+ # store arguments passed into `perform` so that we can later persist
7
+ # them to `AcidicJob::Key#job_args` for both ActiveJob and Sidekiq::Worker
8
+ @arguments_for_perform = if args.any? && kwargs.any?
9
+ args + [kwargs]
10
+ elsif args.any? && kwargs.none?
11
+ args
12
+ elsif args.none? && kwargs.any?
13
+ [kwargs]
14
+ else
15
+ []
16
+ end
17
+
18
+ super
19
+ end
20
+ end
21
+ end
@@ -11,6 +11,7 @@ module AcidicJob
11
11
  end
12
12
 
13
13
  def call(key:)
14
+ # Skip AR callbacks as there are none on the model
14
15
  key.update_column(:recovery_point, name)
15
16
  end
16
17
  end
@@ -6,7 +6,8 @@
6
6
  module AcidicJob
7
7
  class Response
8
8
  def call(key:)
9
- key.update!(
9
+ # Skip AR callbacks as there are none on the model
10
+ key.update_columns(
10
11
  locked_at: nil,
11
12
  recovery_point: Key::RECOVERY_POINT_FINISHED
12
13
  )
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ module AcidicJob
6
+ class Staged < ActiveRecord::Base
7
+ self.table_name = "staged_acidic_jobs"
8
+
9
+ validates :adapter, presence: true
10
+ validates :job_name, presence: true
11
+ validates :job_args, presence: true
12
+
13
+ serialize :job_args
14
+
15
+ after_create_commit :enqueue_job
16
+
17
+ def enqueue_job
18
+ case adapter
19
+ when "activejob"
20
+ job = ActiveJob::Base.deserialize(job_args)
21
+ job.enqueue
22
+ when "sidekiq"
23
+ Sidekiq::Client.push("class" => job_name, "args" => job_args)
24
+ else
25
+ raise UnknownJobAdapter.new(adapter: adapter)
26
+ end
27
+
28
+ # TODO: ensure successful enqueuing before deletion
29
+ delete
30
+ end
31
+ end
32
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AcidicJob
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.3"
5
5
  end
data/lib/acidic_job.rb CHANGED
@@ -1,45 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "acidic_job/version"
4
+ require_relative "acidic_job/errors"
4
5
  require_relative "acidic_job/no_op"
5
6
  require_relative "acidic_job/recovery_point"
6
7
  require_relative "acidic_job/response"
7
8
  require_relative "acidic_job/key"
8
- require_relative "acidic_job/staging"
9
+ require_relative "acidic_job/staged"
10
+ require_relative "acidic_job/perform_wrapper"
11
+ require_relative "acidic_job/perform_transactionally_extension"
9
12
  require "active_support/concern"
10
13
 
11
14
  # rubocop:disable Metrics/ModuleLength, Metrics/AbcSize, Metrics/MethodLength
12
15
  module AcidicJob
13
- class MismatchedIdempotencyKeyAndJobArguments < StandardError; end
14
-
15
- class LockedIdempotencyKey < StandardError; end
16
-
17
- class UnknownRecoveryPoint < StandardError; end
18
-
19
- class UnknownAtomicPhaseType < StandardError; end
20
-
21
- class SerializedTransactionConflict < StandardError; end
22
-
23
16
  extend ActiveSupport::Concern
24
17
 
25
- module ActiveJobExtension
26
- extend ActiveSupport::Concern
18
+ def self.wire_everything_up(klass)
19
+ klass.attr_reader :key
20
+ klass.attr_accessor :arguments_for_perform
27
21
 
28
- class_methods do
29
- def perform_transactionally(*args)
30
- AcidicJob::Staging.create!(
31
- serialized_params: job_or_instantiate(*args).serialize
32
- )
33
- end
34
- end
22
+ # Extend ActiveJob with `perform_transactionally` class method
23
+ klass.include PerformTransactionallyExtension
24
+
25
+ # Ensure our `perform` method always runs first to gather parameters
26
+ klass.prepend PerformWrapper
35
27
  end
36
28
 
37
29
  included do
38
- attr_reader :key
30
+ AcidicJob.wire_everything_up(self)
31
+ end
39
32
 
40
- # Extend ActiveJob only once it has been loaded
41
- ActiveSupport.on_load(:active_job) do
42
- send(:include, ActiveJobExtension)
33
+ class_methods do
34
+ def inherited(subclass)
35
+ AcidicJob.wire_everything_up(subclass)
36
+ super
43
37
  end
44
38
  end
45
39
 
@@ -49,13 +43,8 @@ module AcidicJob
49
43
  # this might not happen 100% of the time, so this is a hedge against it.
50
44
  IDEMPOTENCY_KEY_LOCK_TIMEOUT = 90
51
45
 
52
- # &block
53
- # &block
46
+ # takes a block
54
47
  def idempotently(with:)
55
- # set accessors for each argument passed in to ensure they are available
56
- # to the step methods the job will have written
57
- define_accessors_for_passed_arguments(with)
58
-
59
48
  # execute the block to gather the info on what phases are defined for this job
60
49
  defined_steps = yield
61
50
  # [:create_ride_and_audit_record, :create_stripe_charge, :send_receipt]
@@ -71,13 +60,17 @@ module AcidicJob
71
60
  # close proximity, one of the two will be aborted by Postgres because we're
72
61
  # using a transaction with SERIALIZABLE isolation level. It may not look
73
62
  # it, but this code is safe from races.
74
- ensure_idempotency_key_record(job_id, defined_steps.first)
63
+ ensure_idempotency_key_record(idempotency_key_value, defined_steps.first)
75
64
 
76
65
  # if the key record is already marked as finished, immediately return its result
77
66
  return @key.succeeded? if @key.finished?
78
67
 
68
+ # set accessors for each argument passed in to ensure they are available
69
+ # to the step methods the job will have written
70
+ define_accessors_for_passed_arguments(with, @key)
71
+
79
72
  # otherwise, we will enter a loop to process each required step of the job
80
- 100.times do
73
+ phases.size.times do
81
74
  # our `phases` hash uses Symbols for keys
82
75
  recovery_point = @key.recovery_point.to_sym
83
76
 
@@ -101,10 +94,16 @@ module AcidicJob
101
94
  @_steps
102
95
  end
103
96
 
97
+ def safely_finish_acidic_job
98
+ # Short circuits execution by sending execution right to 'finished'.
99
+ # So, ends the job "successfully"
100
+ AcidicJob::Response.new
101
+ end
102
+
104
103
  private
105
104
 
106
105
  def atomic_phase(key, proc = nil, &block)
107
- error = false
106
+ rescued_error = false
108
107
  phase_callable = (proc || block)
109
108
 
110
109
  begin
@@ -114,17 +113,19 @@ module AcidicJob
114
113
  phase_result.call(key: key)
115
114
  end
116
115
  rescue StandardError => e
117
- error = e
116
+ rescued_error = e
118
117
  raise e
119
118
  ensure
120
- # If we're leaving under an error condition, try to unlock the idempotency
121
- # key right away so that another request can try again.
122
- begin
123
- key.update_columns(locked_at: nil, error_object: error) if error.present?
124
- rescue StandardError => e
125
- # We're already inside an error condition, so swallow any additional
126
- # errors from here and just send them to logs.
127
- puts "Failed to unlock key #{key.id} because of #{e}."
119
+ if rescued_error
120
+ # If we're leaving under an error condition, try to unlock the idempotency
121
+ # key right away so that another request can try again.3
122
+ begin
123
+ key.update_columns(locked_at: nil, error_object: rescued_error)
124
+ rescue StandardError => e
125
+ # We're already inside an error condition, so swallow any additional
126
+ # errors from here and just send them to logs.
127
+ puts "Failed to unlock key #{key.id} because of #{e}."
128
+ end
128
129
  end
129
130
  end
130
131
  end
@@ -136,7 +137,6 @@ module AcidicJob
136
137
  else
137
138
  :serializable
138
139
  end
139
- serialized_job_info = serialize
140
140
 
141
141
  ActiveRecord::Base.transaction(isolation: isolation_level) do
142
142
  @key = Key.find_by(idempotency_key: key_val)
@@ -144,7 +144,7 @@ module AcidicJob
144
144
  if @key
145
145
  # Programs enqueuing multiple jobs with different parameters but the
146
146
  # same idempotency key is a bug.
147
- raise MismatchedIdempotencyKeyAndJobArguments if @key.job_args != serialized_job_info["arguments"]
147
+ raise MismatchedIdempotencyKeyAndJobArguments if @key.job_args != @arguments_for_perform
148
148
 
149
149
  # Only acquire a lock if the key is unlocked or its lock has expired
150
150
  # because the original job was long enough ago.
@@ -158,19 +158,31 @@ module AcidicJob
158
158
  locked_at: Time.current,
159
159
  last_run_at: Time.current,
160
160
  recovery_point: first_step,
161
- job_name: serialized_job_info["job_class"],
162
- job_args: serialized_job_info["arguments"]
161
+ job_name: self.class.name,
162
+ job_args: @arguments_for_perform
163
163
  )
164
164
  end
165
165
  end
166
166
  end
167
167
 
168
- def define_accessors_for_passed_arguments(passed_arguments)
168
+ def define_accessors_for_passed_arguments(passed_arguments, key)
169
+ # first, ensure that `Key#attr_accessors` is populated with initial values
170
+ key.update_column(:attr_accessors, passed_arguments)
171
+
169
172
  passed_arguments.each do |accessor, value|
170
173
  # the reader method may already be defined
171
174
  self.class.attr_reader accessor unless respond_to?(accessor)
172
175
  # but we should always update the value to match the current value
173
176
  instance_variable_set("@#{accessor}", value)
177
+ # and we overwrite the setter to ensure any updates to an accessor update the `Key` stored value
178
+ # Note: we must define the singleton method on the instance to avoid overwriting setters on other
179
+ # instances of the same class
180
+ define_singleton_method("#{accessor}=") do |current_value|
181
+ instance_variable_set("@#{accessor}", current_value)
182
+ key.attr_accessors[accessor] = current_value
183
+ key.save!(validate: false)
184
+ current_value
185
+ end
174
186
  end
175
187
 
176
188
  true
@@ -193,5 +205,13 @@ module AcidicJob
193
205
  end
194
206
  end
195
207
  end
208
+
209
+ def idempotency_key_value
210
+ return job_id if defined?(job_id) && !job_id.nil?
211
+ return jid if defined?(jid) && !jid.nil?
212
+
213
+ require "securerandom"
214
+ SecureRandom.hex
215
+ end
196
216
  end
197
217
  # rubocop:enable Metrics/ModuleLength, Metrics/AbcSize, Metrics/MethodLength
@@ -3,11 +3,10 @@
3
3
  require "rails/generators"
4
4
  require "rails/generators/active_record"
5
5
 
6
- # This generator adds a migration for the {FriendlyId::History
7
- # FriendlyId::History} addon.
8
6
  class AcidicJobGenerator < ActiveRecord::Generators::Base
9
- # ActiveRecord::Generators::Base inherits from Rails::Generators::NamedBase which requires a NAME parameter for the
10
- # new table name. Our generator always uses 'acidic_job_keys', so we just set a random name here.
7
+ # ActiveRecord::Generators::Base inherits from Rails::Generators::NamedBase
8
+ # which requires a NAME parameter for the new table name.
9
+ # Our generator always uses "acidic_job_keys", so we just set a random name here.
11
10
  argument :name, type: :string, default: "random_name"
12
11
 
13
12
  source_root File.expand_path("templates", __dir__)
@@ -23,11 +22,16 @@ class AcidicJobGenerator < ActiveRecord::Generators::Base
23
22
  end
24
23
 
25
24
  # Copies the migration template to db/migrate.
26
- def copy_files
27
- migration_template "migration.rb.erb",
25
+ def copy_acidic_job_keys_migration_files
26
+ migration_template "create_acidic_job_keys_migration.rb.erb",
28
27
  "db/migrate/create_acidic_job_keys.rb"
29
28
  end
30
29
 
30
+ def copy_staged_acidic_jobs_migration_files
31
+ migration_template "create_staged_acidic_jobs_migration.rb.erb",
32
+ "db/migrate/create_staged_acidic_jobs.rb"
33
+ end
34
+
31
35
  protected
32
36
 
33
37
  def migration_class
@@ -8,14 +8,12 @@ class CreateAcidicJobKeys < <%= migration_class %>
8
8
  t.datetime :locked_at, null: true
9
9
  t.string :recovery_point, null: false
10
10
  t.text :error_object
11
+ t.text :attr_accessors
11
12
  t.timestamps
12
13
 
13
14
  t.index %i[idempotency_key job_name job_args],
14
15
  unique: true,
15
16
  name: "idx_acidic_job_keys_on_idempotency_key_n_job_name_n_job_args"
16
-
17
- create_table :acidic_job_stagings do |t|
18
- t.text :serialized_params, null: false
19
17
  end
20
18
  end
21
19
  end
@@ -0,0 +1,9 @@
1
+ class CreateStagedAcidicJobs < <%= migration_class %>
2
+ def change
3
+ create_table :staged_acidic_jobs do |t|
4
+ t.string :adapter, null: false
5
+ t.string :job_name, null: false
6
+ t.text :job_args, null: true
7
+ end
8
+ end
9
+ end
data/slides.md ADDED
@@ -0,0 +1,65 @@
1
+ # ACIDic Jobs
2
+
3
+ ## A bit about me
4
+
5
+ - programming in Ruby for 6 years
6
+ - working for test IO / EPAM
7
+ - consulting for RCRDSHP
8
+ - building Smokestack QA on the side
9
+
10
+ ## Jobs are essential
11
+
12
+ - job / operation / work
13
+ - in every company, with every app, jobs are essential. Why?
14
+ - jobs are what your app *does*, expressed as a distinct unit
15
+ - jobs can be called from anywhere, run sync or async, and have retry mechanisms built-in
16
+
17
+ ## Jobs are internal API endpoints
18
+
19
+ - Like API endpoints, both are discrete units of work
20
+ - Like API endpoints, we should expect failure
21
+ - Like API endpoints, we should expect retries
22
+ - Like API endpoints, we should expect concurrency
23
+ - this symmetry allows us to port much of the wisdom built up over decades of building robust APIs to our app job infrastructure
24
+
25
+ ## ACIDic APIs
26
+
27
+ In a loosely collected series of articles, Brandur Leach lays out the core techniques and principles required to make an HTTP API properly ACIDic:
28
+
29
+ 1. https://brandur.org/acid
30
+ 2. https://brandur.org/http-transactions
31
+ 3. https://brandur.org/job-drain
32
+ 4. https://brandur.org/idempotency-keys
33
+
34
+ His central points can be summarized as follows:
35
+
36
+ - "ACID databases are one of the most important tools in existence for ensuring maintainability and data correctness in big production systems"
37
+ - "for a common idempotent HTTP request, requests should map to backend transactions at 1:1"
38
+ - "We can dequeue jobs gracefully by using a transactionally-staged job drain."
39
+ - "Implementations that need to make synchronous changes in foreign state (i.e. outside of a local ACID store) are somewhat more difficult to design. ... To guarantee idempotency on this type of endpoint we’ll need to introduce idempotency keys."
40
+
41
+ Key concepts:
42
+
43
+ - foreign state mutations
44
+ - The reason that the local vs. foreign distinction matters is that unlike a local set of operations where we can leverage an ACID store to roll back a result that we didn’t like, once we make our first foreign state mutation, we’re committed one way or another
45
+ - "An atomic phase is a set of local state mutations that occur in transactions between foreign state mutations."
46
+ - "A recovery point is a name of a check point that we get to after having successfully executed any atomic phase or foreign state mutation"
47
+ - "transactionally-staged job drain"
48
+ - "With this pattern, jobs aren’t immediately sent to the job queue. Instead, they’re staged in a table within the relational database itself, and the ACID properties of the running transaction keep them invisible until they’re ready to be worked. A secondary enqueuer process reads the table and sends any jobs it finds to the job queue before removing their rows."
49
+
50
+
51
+ https://github.com/mperham/sidekiq/wiki/Best-Practices#2-make-your-job-idempotent-and-transactional
52
+
53
+ 2. Make your job idempotent and transactional
54
+
55
+ Idempotency means that your job can safely execute multiple times. For instance, with the error retry functionality, your job might be half-processed, throw an error, and then be re-executed over and over until it successfully completes. Let's say you have a job which voids a credit card transaction and emails the user to let them know the charge has been refunded:
56
+
57
+ ```ruby
58
+ def perform(card_charge_id)
59
+ charge = CardCharge.find(card_charge_id)
60
+ charge.void_transaction
61
+ Emailer.charge_refunded(charge).deliver
62
+ end
63
+ ```
64
+
65
+ What happens when the email fails to render due to a bug? Will the void_transaction method handle the case where a charge has already been refunded? You can use a database transaction to ensure data changes are rolled back if there is an error or you can write your code to be resilient in the face of errors. Just remember that Sidekiq will execute your job at least once, not exactly once.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acidic_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - fractaledmind
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-09-06 00:00:00.000000000 Z
11
+ date: 2021-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -71,15 +71,21 @@ files:
71
71
  - acidic_job.gemspec
72
72
  - bin/console
73
73
  - bin/setup
74
+ - blog_post.md
74
75
  - lib/acidic_job.rb
76
+ - lib/acidic_job/errors.rb
75
77
  - lib/acidic_job/key.rb
76
78
  - lib/acidic_job/no_op.rb
79
+ - lib/acidic_job/perform_transactionally_extension.rb
80
+ - lib/acidic_job/perform_wrapper.rb
77
81
  - lib/acidic_job/recovery_point.rb
78
82
  - lib/acidic_job/response.rb
79
- - lib/acidic_job/staging.rb
83
+ - lib/acidic_job/staged.rb
80
84
  - lib/acidic_job/version.rb
81
85
  - lib/generators/acidic_job_generator.rb
82
- - lib/generators/templates/migration.rb.erb
86
+ - lib/generators/templates/create_acidic_job_keys_migration.rb.erb
87
+ - lib/generators/templates/create_staged_acidic_jobs_migration.rb.erb
88
+ - slides.md
83
89
  homepage: https://github.com/fractaledmind/acidic_job
84
90
  licenses:
85
91
  - MIT
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module AcidicJob
4
- class Staging < ActiveRecord::Base
5
- self.table_name = "acidic_job_stagings"
6
-
7
- validates :serialized_params, presence: true
8
-
9
- serialize :serialized_params
10
-
11
- after_create_commit :enqueue_job
12
-
13
- def enqueue_job
14
- job = ActiveJob::Base.deserialize(serialized_params)
15
- job.enqueue
16
- delete
17
- end
18
- end
19
- end