acidic_job 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/Gemfile.lock +8 -1
- data/blog_post.md +28 -0
- data/lib/acidic_job/errors.rb +15 -0
- data/lib/acidic_job/key.rb +2 -0
- data/lib/acidic_job/perform_transactionally_extension.rb +31 -0
- data/lib/acidic_job/perform_wrapper.rb +21 -0
- data/lib/acidic_job/recovery_point.rb +1 -0
- data/lib/acidic_job/response.rb +2 -1
- data/lib/acidic_job/staged.rb +31 -0
- data/lib/acidic_job/version.rb +1 -1
- data/lib/acidic_job.rb +40 -39
- data/lib/generators/acidic_job_generator.rb +10 -6
- data/lib/generators/templates/{migration.rb.erb → create_acidic_job_keys_migration.rb.erb} +0 -3
- data/lib/generators/templates/create_staged_acidic_jobs_migration.rb.erb +9 -0
- data/slides.md +65 -0
- metadata +10 -4
- data/lib/acidic_job/staging.rb +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 943a9bc87b07b1e4a2ec7d0a83ffa8a23917124386cfc601780f95d36c62d23a
|
4
|
+
data.tar.gz: 2f534e9ebd0d533ae4b7572fb1da1451df9a1a2ed68fc231bc4f4de323de9e84
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 59f4ecc2e773d72bbef2d386a223e9859ee6f14c657e706c593e7a3f4c994c86aae885ef0b2993cbb916a1380daf3231882255685b6ffa360ffc4bf5dee591d8
|
7
|
+
data.tar.gz: 2a8d540fabb8528a9c305b9d7cf7fe12588c14760557f08f4c380dfb6b45fdafec1232767234809101d6aa0e8f44f8ac895b120664dec8e533d180900ad06c6f
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
acidic_job (0.
|
4
|
+
acidic_job (0.5.0)
|
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,15 @@
|
|
1
|
+
module AcidicJob
|
2
|
+
class Error < StandardError; end
|
3
|
+
|
4
|
+
class MismatchedIdempotencyKeyAndJobArguments < Error; end
|
5
|
+
|
6
|
+
class LockedIdempotencyKey < Error; end
|
7
|
+
|
8
|
+
class UnknownRecoveryPoint < Error; end
|
9
|
+
|
10
|
+
class UnknownAtomicPhaseType < Error; end
|
11
|
+
|
12
|
+
class SerializedTransactionConflict < Error; end
|
13
|
+
|
14
|
+
class UnknownJobAdapter < Error; end
|
15
|
+
end
|
data/lib/acidic_job/key.rb
CHANGED
@@ -0,0 +1,31 @@
|
|
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
|
+
def perform_transactionally(*args)
|
11
|
+
attributes = if self < ActiveJob::Base
|
12
|
+
{
|
13
|
+
adapter: "activejob",
|
14
|
+
job_name: self.name,
|
15
|
+
job_args: job_or_instantiate(*args).serialize
|
16
|
+
}
|
17
|
+
elsif self.include? Sidekiq::Worker
|
18
|
+
{
|
19
|
+
adapter: "sidekiq",
|
20
|
+
job_name: self.name,
|
21
|
+
job_args: args
|
22
|
+
}
|
23
|
+
else
|
24
|
+
raise UnknownJobAdapter
|
25
|
+
end
|
26
|
+
|
27
|
+
AcidicJob::Staged.create!(attributes)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
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
|
data/lib/acidic_job/response.rb
CHANGED
@@ -0,0 +1,31 @@
|
|
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
|
+
if adapter == "activejob"
|
19
|
+
job = ActiveJob::Base.deserialize(job_args)
|
20
|
+
job.enqueue
|
21
|
+
elsif adapter == "sidekiq"
|
22
|
+
Sidekiq::Client.push("class" => job_name, "args" => job_args)
|
23
|
+
else
|
24
|
+
raise UnknownJobAdapter.new(adapter: adapter)
|
25
|
+
end
|
26
|
+
|
27
|
+
# TODO: ensure successful enqueuing before deletion
|
28
|
+
delete
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/acidic_job/version.rb
CHANGED
data/lib/acidic_job.rb
CHANGED
@@ -1,46 +1,29 @@
|
|
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/
|
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
|
27
|
-
|
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
|
35
|
-
end
|
36
|
-
|
37
18
|
included do
|
38
19
|
attr_reader :key
|
20
|
+
attr_accessor :arguments_for_perform
|
39
21
|
|
40
|
-
# Extend ActiveJob
|
41
|
-
|
42
|
-
|
43
|
-
|
22
|
+
# Extend ActiveJob with `perform_transactionally` class method
|
23
|
+
include PerformTransactionallyExtension
|
24
|
+
|
25
|
+
# Ensure our `perform` method always runs first to gather parameters
|
26
|
+
prepend PerformWrapper
|
44
27
|
end
|
45
28
|
|
46
29
|
# Number of seconds passed which we consider a held idempotency key lock to be
|
@@ -49,8 +32,7 @@ module AcidicJob
|
|
49
32
|
# this might not happen 100% of the time, so this is a hedge against it.
|
50
33
|
IDEMPOTENCY_KEY_LOCK_TIMEOUT = 90
|
51
34
|
|
52
|
-
#
|
53
|
-
# &block
|
35
|
+
# takes a block
|
54
36
|
def idempotently(with:)
|
55
37
|
# set accessors for each argument passed in to ensure they are available
|
56
38
|
# to the step methods the job will have written
|
@@ -71,7 +53,7 @@ module AcidicJob
|
|
71
53
|
# close proximity, one of the two will be aborted by Postgres because we're
|
72
54
|
# using a transaction with SERIALIZABLE isolation level. It may not look
|
73
55
|
# it, but this code is safe from races.
|
74
|
-
ensure_idempotency_key_record(
|
56
|
+
ensure_idempotency_key_record(idempotency_key_value, defined_steps.first)
|
75
57
|
|
76
58
|
# if the key record is already marked as finished, immediately return its result
|
77
59
|
return @key.succeeded? if @key.finished?
|
@@ -101,10 +83,16 @@ module AcidicJob
|
|
101
83
|
@_steps
|
102
84
|
end
|
103
85
|
|
86
|
+
def safely_finish_acidic_job
|
87
|
+
# Short circuits execution by sending execution right to 'finished'.
|
88
|
+
# So, ends the job "successfully"
|
89
|
+
AcidicJob::Response.new
|
90
|
+
end
|
91
|
+
|
104
92
|
private
|
105
93
|
|
106
94
|
def atomic_phase(key, proc = nil, &block)
|
107
|
-
|
95
|
+
rescued_error = false
|
108
96
|
phase_callable = (proc || block)
|
109
97
|
|
110
98
|
begin
|
@@ -114,13 +102,15 @@ module AcidicJob
|
|
114
102
|
phase_result.call(key: key)
|
115
103
|
end
|
116
104
|
rescue StandardError => e
|
117
|
-
|
105
|
+
rescued_error = e
|
118
106
|
raise e
|
119
107
|
ensure
|
108
|
+
return unless rescued_error
|
109
|
+
|
120
110
|
# If we're leaving under an error condition, try to unlock the idempotency
|
121
|
-
# key right away so that another request can try again.
|
111
|
+
# key right away so that another request can try again.3
|
122
112
|
begin
|
123
|
-
key.update_columns(locked_at: nil, error_object:
|
113
|
+
key.update_columns(locked_at: nil, error_object: rescued_error)
|
124
114
|
rescue StandardError => e
|
125
115
|
# We're already inside an error condition, so swallow any additional
|
126
116
|
# errors from here and just send them to logs.
|
@@ -136,7 +126,6 @@ module AcidicJob
|
|
136
126
|
else
|
137
127
|
:serializable
|
138
128
|
end
|
139
|
-
serialized_job_info = serialize
|
140
129
|
|
141
130
|
ActiveRecord::Base.transaction(isolation: isolation_level) do
|
142
131
|
@key = Key.find_by(idempotency_key: key_val)
|
@@ -144,11 +133,15 @@ module AcidicJob
|
|
144
133
|
if @key
|
145
134
|
# Programs enqueuing multiple jobs with different parameters but the
|
146
135
|
# same idempotency key is a bug.
|
147
|
-
|
136
|
+
if @key.job_args != @arguments_for_perform
|
137
|
+
raise MismatchedIdempotencyKeyAndJobArguments
|
138
|
+
end
|
148
139
|
|
149
140
|
# Only acquire a lock if the key is unlocked or its lock has expired
|
150
141
|
# because the original job was long enough ago.
|
151
|
-
|
142
|
+
if @key.locked_at && @key.locked_at > Time.current - IDEMPOTENCY_KEY_LOCK_TIMEOUT
|
143
|
+
raise LockedIdempotencyKey
|
144
|
+
end
|
152
145
|
|
153
146
|
# Lock the key and update latest run unless the job is already finished.
|
154
147
|
@key.update!(last_run_at: Time.current, locked_at: Time.current) unless @key.finished?
|
@@ -158,8 +151,8 @@ module AcidicJob
|
|
158
151
|
locked_at: Time.current,
|
159
152
|
last_run_at: Time.current,
|
160
153
|
recovery_point: first_step,
|
161
|
-
job_name:
|
162
|
-
job_args:
|
154
|
+
job_name: self.class.name,
|
155
|
+
job_args: @arguments_for_perform
|
163
156
|
)
|
164
157
|
end
|
165
158
|
end
|
@@ -193,5 +186,13 @@ module AcidicJob
|
|
193
186
|
end
|
194
187
|
end
|
195
188
|
end
|
189
|
+
|
190
|
+
def idempotency_key_value
|
191
|
+
return job_id if defined?(job_id) && !job_id.nil?
|
192
|
+
return jid if defined?(jid) && !jid.nil?
|
193
|
+
|
194
|
+
require 'securerandom'
|
195
|
+
SecureRandom.hex
|
196
|
+
end
|
196
197
|
end
|
197
198
|
# 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
|
10
|
-
#
|
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
|
27
|
-
migration_template "
|
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
|
@@ -13,9 +13,6 @@ class CreateAcidicJobKeys < <%= migration_class %>
|
|
13
13
|
t.index %i[idempotency_key job_name job_args],
|
14
14
|
unique: true,
|
15
15
|
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
16
|
end
|
20
17
|
end
|
21
18
|
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
|
+
version: 0.5.0
|
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-
|
11
|
+
date: 2021-09-28 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/
|
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/
|
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
|
data/lib/acidic_job/staging.rb
DELETED
@@ -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
|