acidic_job 0.3.1 → 0.5.2

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: 8aa8aee9c23bc58be992813f139864afdb057442ee9c359263f3ff45029e3f28
4
- data.tar.gz: 9dbb8017fcbf91ce5cbcc5c96968e3b880340c1e1cc7a8c36cb11e744d6b405d
3
+ metadata.gz: 7b6e76d892b0953452781a38155801969acf50d81897fb78645b561280620ddc
4
+ data.tar.gz: 6640c17792135307d21d20f05b49136512368ff84ed748fb2b1ad98d552fc8fb
5
5
  SHA512:
6
- metadata.gz: 4bb2298f924a2aa2d5db31c7c05201188872333c36dd3e0bafb598ed087ec9a686695c27b1cdd2d33f067f2bbb47178c5bc700201406db856392997ef6605dbc
7
- data.tar.gz: f842586dc2d669196ce58bcbc279bc2b967fd0c8973d296323622c993e8d08422ce65f55b85cf96407aa4a668af15dea005fdf3f8917e520ca020c2851dd4bfa
6
+ metadata.gz: c489d047845c6fcd27482dfe1a3c0cae89a326cd3c81d4816ca9758c7db9e0edd09d954e719eedcc0d8b2a689137e21486482571bd05b7518af795152fc4bc61
7
+ data.tar.gz: ddc07f0514be3ad3f8fdec4ffd4bf350a039a4ad95f6b28984f1cb73f29d51ed3511df94eb628f8f336b81d4edae3d9b1c9af3a8b55ab5ac08efc015775c9249
data/.rubocop.yml CHANGED
@@ -12,3 +12,6 @@ Style/StringLiteralsInInterpolation:
12
12
 
13
13
  Layout/LineLength:
14
14
  Max: 120
15
+
16
+ Style/Documentation:
17
+ Enabled: false
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.3.1)
4
+ acidic_job (0.5.2)
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/acidic_job.gemspec CHANGED
@@ -27,8 +27,8 @@ 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 "activesupport"
31
30
  spec.add_dependency "activerecord", ">= 4.0.0"
31
+ spec.add_dependency "activesupport"
32
32
  spec.add_development_dependency "railties", ">= 4.0"
33
33
 
34
34
  # For more information and examples about making a new gem, checkout our
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
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ module AcidicJob
6
+ class Key < ActiveRecord::Base
7
+ RECOVERY_POINT_FINISHED = "FINISHED"
8
+
9
+ self.table_name = "acidic_job_keys"
10
+
11
+ serialize :error_object
12
+ serialize :job_args
13
+
14
+ validates :idempotency_key, presence: true, uniqueness: { scope: %i[job_name job_args] }
15
+ validates :job_name, presence: true
16
+ validates :last_run_at, presence: true
17
+ validates :recovery_point, presence: true
18
+
19
+ def finished?
20
+ recovery_point == RECOVERY_POINT_FINISHED
21
+ end
22
+
23
+ def succeeded?
24
+ finished? && !failed?
25
+ end
26
+
27
+ def failed?
28
+ error_object.present?
29
+ end
30
+ end
31
+ end
@@ -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
@@ -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,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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AcidicJob
4
- VERSION = "0.3.1"
4
+ VERSION = "0.5.2"
5
5
  end
data/lib/acidic_job.rb CHANGED
@@ -1,60 +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"
8
+ require_relative "acidic_job/key"
9
+ require_relative "acidic_job/staged"
10
+ require_relative "acidic_job/perform_wrapper"
11
+ require_relative "acidic_job/perform_transactionally_extension"
7
12
  require "active_support/concern"
8
13
 
9
- # rubocop:disable Metrics/ModuleLength, Style/Documentation, Metrics/AbcSize, Metrics/MethodLength
14
+ # rubocop:disable Metrics/ModuleLength, Metrics/AbcSize, Metrics/MethodLength
10
15
  module AcidicJob
11
- class MismatchedIdempotencyKeyAndJobArguments < StandardError; end
12
-
13
- class LockedIdempotencyKey < StandardError; end
14
-
15
- class UnknownRecoveryPoint < StandardError; end
16
-
17
- class UnknownAtomicPhaseType < StandardError; end
18
-
19
- class SerializedTransactionConflict < StandardError; end
20
-
21
- class Key < ActiveRecord::Base
22
- RECOVERY_POINT_FINISHED = "FINISHED"
23
-
24
- self.table_name = "acidic_job_keys"
25
-
26
- serialize :error_object
27
- serialize :job_args
28
-
29
- validates :idempotency_key, presence: true, uniqueness: {scope: [:job_name, :job_args]}
30
- validates :job_name, presence: true
31
- validates :last_run_at, presence: true
32
- validates :recovery_point, presence: true
16
+ extend ActiveSupport::Concern
33
17
 
34
- def finished?
35
- recovery_point == RECOVERY_POINT_FINISHED
36
- end
18
+ def self.wire_everything_up(klass)
19
+ klass.attr_reader :key
20
+ klass.attr_accessor :arguments_for_perform
37
21
 
38
- def succeeded?
39
- finished? && !failed?
40
- end
22
+ # Extend ActiveJob with `perform_transactionally` class method
23
+ klass.include PerformTransactionallyExtension
41
24
 
42
- def failed?
43
- error_object.present?
44
- end
25
+ # Ensure our `perform` method always runs first to gather parameters
26
+ klass.prepend PerformWrapper
45
27
  end
46
28
 
47
- extend ActiveSupport::Concern
48
-
49
29
  included do
50
- attr_reader :key
51
-
52
- # discard_on MismatchedIdempotencyKeyAndJobArguments
53
- # discard_on UnknownRecoveryPoint
54
- # discard_on UnknownAtomicPhaseType
55
- # discard_on MissingRequiredAttribute
56
- # retry_on LockedIdempotencyKey
57
- # retry_on ActiveRecord::SerializationFailure
30
+ AcidicJob.wire_everything_up(self)
31
+ end
32
+
33
+ class_methods do
34
+ def inherited(subclass)
35
+ AcidicJob.wire_everything_up(subclass)
36
+ end
58
37
  end
59
38
 
60
39
  # Number of seconds passed which we consider a held idempotency key lock to be
@@ -63,8 +42,8 @@ module AcidicJob
63
42
  # this might not happen 100% of the time, so this is a hedge against it.
64
43
  IDEMPOTENCY_KEY_LOCK_TIMEOUT = 90
65
44
 
66
- # &block
67
- def idempotently(with:) # &block
45
+ # takes a block
46
+ def idempotently(with:)
68
47
  # set accessors for each argument passed in to ensure they are available
69
48
  # to the step methods the job will have written
70
49
  define_accessors_for_passed_arguments(with)
@@ -84,13 +63,13 @@ module AcidicJob
84
63
  # close proximity, one of the two will be aborted by Postgres because we're
85
64
  # using a transaction with SERIALIZABLE isolation level. It may not look
86
65
  # it, but this code is safe from races.
87
- ensure_idempotency_key_record(job_id, defined_steps.first)
66
+ ensure_idempotency_key_record(idempotency_key_value, defined_steps.first)
88
67
 
89
68
  # if the key record is already marked as finished, immediately return its result
90
69
  return @key.succeeded? if @key.finished?
91
70
 
92
71
  # otherwise, we will enter a loop to process each required step of the job
93
- 100.times do
72
+ phases.size.times do
94
73
  # our `phases` hash uses Symbols for keys
95
74
  recovery_point = @key.recovery_point.to_sym
96
75
 
@@ -114,10 +93,16 @@ module AcidicJob
114
93
  @_steps
115
94
  end
116
95
 
96
+ def safely_finish_acidic_job
97
+ # Short circuits execution by sending execution right to 'finished'.
98
+ # So, ends the job "successfully"
99
+ AcidicJob::Response.new
100
+ end
101
+
117
102
  private
118
103
 
119
104
  def atomic_phase(key, proc = nil, &block)
120
- error = false
105
+ rescued_error = false
121
106
  phase_callable = (proc || block)
122
107
 
123
108
  begin
@@ -126,15 +111,17 @@ module AcidicJob
126
111
 
127
112
  phase_result.call(key: key)
128
113
  end
129
- rescue => e
130
- error = e
114
+ rescue StandardError => e
115
+ rescued_error = e
131
116
  raise e
132
117
  ensure
118
+ return unless rescued_error
119
+
133
120
  # If we're leaving under an error condition, try to unlock the idempotency
134
- # key right away so that another request can try again.
121
+ # key right away so that another request can try again.3
135
122
  begin
136
- key.update_columns(locked_at: nil, error_object: error) if error.present?
137
- rescue => e
123
+ key.update_columns(locked_at: nil, error_object: rescued_error)
124
+ rescue StandardError => e
138
125
  # We're already inside an error condition, so swallow any additional
139
126
  # errors from here and just send them to logs.
140
127
  puts "Failed to unlock key #{key.id} because of #{e}."
@@ -148,8 +135,7 @@ module AcidicJob
148
135
  :read_uncommitted
149
136
  else
150
137
  :serializable
151
- end
152
- serialized_job_info = serialize
138
+ end
153
139
 
154
140
  ActiveRecord::Base.transaction(isolation: isolation_level) do
155
141
  @key = Key.find_by(idempotency_key: key_val)
@@ -157,11 +143,15 @@ module AcidicJob
157
143
  if @key
158
144
  # Programs enqueuing multiple jobs with different parameters but the
159
145
  # same idempotency key is a bug.
160
- raise MismatchedIdempotencyKeyAndJobArguments if @key.job_args != serialized_job_info["arguments"]
146
+ if @key.job_args != @arguments_for_perform
147
+ raise MismatchedIdempotencyKeyAndJobArguments
148
+ end
161
149
 
162
150
  # Only acquire a lock if the key is unlocked or its lock has expired
163
151
  # because the original job was long enough ago.
164
- raise LockedIdempotencyKey if @key.locked_at && @key.locked_at > Time.current - IDEMPOTENCY_KEY_LOCK_TIMEOUT
152
+ if @key.locked_at && @key.locked_at > Time.current - IDEMPOTENCY_KEY_LOCK_TIMEOUT
153
+ raise LockedIdempotencyKey
154
+ end
165
155
 
166
156
  # Lock the key and update latest run unless the job is already finished.
167
157
  @key.update!(last_run_at: Time.current, locked_at: Time.current) unless @key.finished?
@@ -171,8 +161,8 @@ module AcidicJob
171
161
  locked_at: Time.current,
172
162
  last_run_at: Time.current,
173
163
  recovery_point: first_step,
174
- job_name: serialized_job_info["job_class"],
175
- job_args: serialized_job_info["arguments"]
164
+ job_name: self.class.name,
165
+ job_args: @arguments_for_perform
176
166
  )
177
167
  end
178
168
  end
@@ -206,5 +196,13 @@ module AcidicJob
206
196
  end
207
197
  end
208
198
  end
199
+
200
+ def idempotency_key_value
201
+ return job_id if defined?(job_id) && !job_id.nil?
202
+ return jid if defined?(jid) && !jid.nil?
203
+
204
+ require 'securerandom'
205
+ SecureRandom.hex
206
+ end
209
207
  end
210
- # rubocop:enable Metrics/ModuleLength, Style/Documentation, Metrics/AbcSize, Metrics/MethodLength
208
+ # rubocop:enable Metrics/ModuleLength, Metrics/AbcSize, Metrics/MethodLength
@@ -1,11 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rails/generators"
2
4
  require "rails/generators/active_record"
3
5
 
4
- # This generator adds a migration for the {FriendlyId::History
5
- # FriendlyId::History} addon.
6
6
  class AcidicJobGenerator < ActiveRecord::Generators::Base
7
- # ActiveRecord::Generators::Base inherits from Rails::Generators::NamedBase which requires a NAME parameter for the
8
- # 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.
9
10
  argument :name, type: :string, default: "random_name"
10
11
 
11
12
  source_root File.expand_path("templates", __dir__)
@@ -21,9 +22,14 @@ class AcidicJobGenerator < ActiveRecord::Generators::Base
21
22
  end
22
23
 
23
24
  # Copies the migration template to db/migrate.
24
- def copy_files
25
- migration_template "migration.rb",
26
- "db/migrate/create_acidic_job_keys.rb"
25
+ def copy_acidic_job_keys_migration_files
26
+ migration_template "create_acidic_job_keys_migration.rb.erb",
27
+ "db/migrate/create_acidic_job_keys.rb"
28
+ end
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"
27
33
  end
28
34
 
29
35
  protected
@@ -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,43 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acidic_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.5.2
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-02 00:00:00.000000000 Z
11
+ date: 2021-09-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: activesupport
14
+ name: activerecord
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 4.0.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: 4.0.0
27
27
  - !ruby/object:Gem::Dependency
28
- name: activerecord
28
+ name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 4.0.0
33
+ version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 4.0.0
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: railties
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -71,13 +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
77
+ - lib/acidic_job/key.rb
75
78
  - lib/acidic_job/no_op.rb
79
+ - lib/acidic_job/perform_transactionally_extension.rb
80
+ - lib/acidic_job/perform_wrapper.rb
76
81
  - lib/acidic_job/recovery_point.rb
77
82
  - lib/acidic_job/response.rb
83
+ - lib/acidic_job/staged.rb
78
84
  - lib/acidic_job/version.rb
79
85
  - lib/generators/acidic_job_generator.rb
80
- - lib/generators/templates/migration.rb
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
81
89
  homepage: https://github.com/fractaledmind/acidic_job
82
90
  licenses:
83
91
  - MIT