acidic_job 0.1.3 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +4 -0
- data/Gemfile.lock +52 -1
- data/README.md +58 -5
- data/acidic_job.gemspec +2 -0
- data/bin/console +1 -0
- data/lib/acidic_job.rb +65 -78
- data/lib/acidic_job/no_op.rb +5 -3
- data/lib/acidic_job/recovery_point.rb +9 -7
- data/lib/acidic_job/response.rb +8 -6
- data/lib/acidic_job/version.rb +1 -1
- data/lib/generators/acidic_job_generator.rb +37 -0
- data/lib/generators/templates/migration.rb +17 -0
- metadata +32 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed6991438cd55a757d0c3606591dd3e39cf470a2fca7486afea1d9efbb948aa6
|
4
|
+
data.tar.gz: d5f078c2111b538ebc7b16df70b2986fa7e1c958716806ad4778c81817b9d3f4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 24aa6c22959133bd68f3947bd15a742b6b314e7fb174b45fede5b598a85a63f04c2ba95a96e53ad3167fbfa1b9c9dcb981c7d59786be87d36520bab825255605
|
7
|
+
data.tar.gz: 02fad1948a7a17b81c2a6be0e48c271563fc7ce84a8de5ec5b84d3e1bb2ca65dbe6e838c4f4eb3c05eb2497424fe33fb5ea0e2c8e1001861ec75cb0c835a153a
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,12 +1,26 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
acidic_job (0.1
|
4
|
+
acidic_job (0.2.1)
|
5
|
+
activerecord (>= 4.0.0)
|
5
6
|
activesupport
|
6
7
|
|
7
8
|
GEM
|
8
9
|
remote: https://rubygems.org/
|
9
10
|
specs:
|
11
|
+
actionpack (6.1.3.2)
|
12
|
+
actionview (= 6.1.3.2)
|
13
|
+
activesupport (= 6.1.3.2)
|
14
|
+
rack (~> 2.0, >= 2.0.9)
|
15
|
+
rack-test (>= 0.6.3)
|
16
|
+
rails-dom-testing (~> 2.0)
|
17
|
+
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
18
|
+
actionview (6.1.3.2)
|
19
|
+
activesupport (= 6.1.3.2)
|
20
|
+
builder (~> 3.1)
|
21
|
+
erubi (~> 1.4)
|
22
|
+
rails-dom-testing (~> 2.0)
|
23
|
+
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
10
24
|
activejob (6.1.3.2)
|
11
25
|
activesupport (= 6.1.3.2)
|
12
26
|
globalid (>= 0.3.6)
|
@@ -22,7 +36,9 @@ GEM
|
|
22
36
|
tzinfo (~> 2.0)
|
23
37
|
zeitwerk (~> 2.3)
|
24
38
|
ast (2.4.2)
|
39
|
+
builder (3.2.4)
|
25
40
|
concurrent-ruby (1.1.9)
|
41
|
+
crass (1.0.6)
|
26
42
|
database_cleaner (2.0.1)
|
27
43
|
database_cleaner-active_record (~> 2.0.0)
|
28
44
|
database_cleaner-active_record (2.0.1)
|
@@ -30,14 +46,40 @@ GEM
|
|
30
46
|
database_cleaner-core (~> 2.0.0)
|
31
47
|
database_cleaner-core (2.0.1)
|
32
48
|
docile (1.4.0)
|
49
|
+
erubi (1.10.0)
|
33
50
|
globalid (0.4.2)
|
34
51
|
activesupport (>= 4.2.0)
|
35
52
|
i18n (1.8.10)
|
36
53
|
concurrent-ruby (~> 1.0)
|
54
|
+
loofah (2.12.0)
|
55
|
+
crass (~> 1.0.2)
|
56
|
+
nokogiri (>= 1.5.9)
|
57
|
+
method_source (1.0.0)
|
58
|
+
mini_portile2 (2.6.1)
|
37
59
|
minitest (5.14.4)
|
60
|
+
nokogiri (1.12.3)
|
61
|
+
mini_portile2 (~> 2.6.1)
|
62
|
+
racc (~> 1.4)
|
63
|
+
nokogiri (1.12.3-x86_64-darwin)
|
64
|
+
racc (~> 1.4)
|
38
65
|
parallel (1.20.1)
|
39
66
|
parser (3.0.1.1)
|
40
67
|
ast (~> 2.4.1)
|
68
|
+
racc (1.5.2)
|
69
|
+
rack (2.2.3)
|
70
|
+
rack-test (1.1.0)
|
71
|
+
rack (>= 1.0, < 3)
|
72
|
+
rails-dom-testing (2.0.3)
|
73
|
+
activesupport (>= 4.2.0)
|
74
|
+
nokogiri (>= 1.6)
|
75
|
+
rails-html-sanitizer (1.4.1)
|
76
|
+
loofah (~> 2.3)
|
77
|
+
railties (6.1.3.2)
|
78
|
+
actionpack (= 6.1.3.2)
|
79
|
+
activesupport (= 6.1.3.2)
|
80
|
+
method_source
|
81
|
+
rake (>= 0.8.7)
|
82
|
+
thor (~> 1.0)
|
41
83
|
rainbow (3.0.0)
|
42
84
|
rake (13.0.4)
|
43
85
|
regexp_parser (2.1.1)
|
@@ -53,6 +95,10 @@ GEM
|
|
53
95
|
unicode-display_width (>= 1.4.0, < 3.0)
|
54
96
|
rubocop-ast (1.7.0)
|
55
97
|
parser (>= 3.0.1.1)
|
98
|
+
rubocop-minitest (0.14.0)
|
99
|
+
rubocop (>= 0.90, < 2.0)
|
100
|
+
rubocop-rake (0.6.0)
|
101
|
+
rubocop (~> 1.0)
|
56
102
|
ruby-progressbar (1.11.0)
|
57
103
|
simplecov (0.21.2)
|
58
104
|
docile (~> 1.1)
|
@@ -61,12 +107,14 @@ GEM
|
|
61
107
|
simplecov-html (0.12.3)
|
62
108
|
simplecov_json_formatter (0.1.3)
|
63
109
|
sqlite3 (1.4.2)
|
110
|
+
thor (1.1.0)
|
64
111
|
tzinfo (2.0.4)
|
65
112
|
concurrent-ruby (~> 1.0)
|
66
113
|
unicode-display_width (2.0.0)
|
67
114
|
zeitwerk (2.4.2)
|
68
115
|
|
69
116
|
PLATFORMS
|
117
|
+
ruby
|
70
118
|
x86_64-darwin-17
|
71
119
|
|
72
120
|
DEPENDENCIES
|
@@ -75,8 +123,11 @@ DEPENDENCIES
|
|
75
123
|
activerecord (~> 6.1.3.2)
|
76
124
|
database_cleaner
|
77
125
|
minitest (~> 5.0)
|
126
|
+
railties (>= 4.0)
|
78
127
|
rake (~> 13.0)
|
79
128
|
rubocop (~> 1.7)
|
129
|
+
rubocop-minitest
|
130
|
+
rubocop-rake
|
80
131
|
simplecov
|
81
132
|
sqlite3
|
82
133
|
|
data/README.md
CHANGED
@@ -1,8 +1,25 @@
|
|
1
1
|
# AcidicJob
|
2
2
|
|
3
|
-
|
3
|
+
### Idempotent operations for Rails apps, built on top of ActiveJob.
|
4
4
|
|
5
|
-
|
5
|
+
At the conceptual heart of basically any software are "operations"—the discrete actions the software performs. Rails provides a powerful abstraction layer for building operations in the form of `ActiveJob`. With `ActiveJob`, we can easily trigger from other Ruby code throughout our Rails application (controller actions, model methods, model callbacks, etc.); we can run operations both synchronously (blocking execution and then returning its response to the caller) and asychronously (non-blocking and the caller doesn't know its response); and we can also retry a specific operation if needed seamlessly.
|
6
|
+
|
7
|
+
However, in order to ensure that our operational jobs are _robust_, we need to ensure that they are properly [idempotent and transactional](https://github.com/mperham/sidekiq/wiki/Best-Practices#2-make-your-job-idempotent-and-transactional). As stated in the [GitLab Sidekiq Style Guide](https://docs.gitlab.com/ee/development/sidekiq_style_guide.html#idempotent-jobs):
|
8
|
+
|
9
|
+
>As a general rule, a worker can be considered idempotent if:
|
10
|
+
> * It can safely run multiple times with the same arguments.
|
11
|
+
> * Application side-effects are expected to happen only once (or side-effects of a second run do not have an effect).
|
12
|
+
|
13
|
+
This is, of course, far easier said than done. Thus, `AcidicJob`.
|
14
|
+
|
15
|
+
`AcidicJob` provides a framework to help you make your operational jobs atomic ⚛️, consistent 🤖, isolated 🕴🏼, and durable ⛰️. Its conceptual framework is directly inspired by a truly wonderful loosely collected series of articles written by Brandur Leach, which together lay out core techniques and principles required to make an HTTP API properly ACIDic:
|
16
|
+
|
17
|
+
1. https://brandur.org/acid
|
18
|
+
2. https://brandur.org/http-transactions
|
19
|
+
3. https://brandur.org/job-drain
|
20
|
+
4. https://brandur.org/idempotency-keys
|
21
|
+
|
22
|
+
`AcidicJob` brings these techniques and principles into the world of a standard Rails application.
|
6
23
|
|
7
24
|
## Installation
|
8
25
|
|
@@ -16,13 +33,49 @@ And then execute:
|
|
16
33
|
|
17
34
|
$ bundle install
|
18
35
|
|
19
|
-
Or install
|
36
|
+
Or simply execute to install the gem yourself:
|
20
37
|
|
21
|
-
$
|
38
|
+
$ bundle add acidic_job
|
39
|
+
|
40
|
+
Then, use the following command to copy over the AcidicJobKey migration.
|
41
|
+
|
42
|
+
```
|
43
|
+
rails generate acidic_job
|
44
|
+
```
|
22
45
|
|
23
46
|
## Usage
|
24
47
|
|
25
|
-
|
48
|
+
`AcidicJob` is a concern that you `include` into your operation jobs which provides two public methods to help you make your jobs idempotent and robust—`idempotently` and `step`. You can see them "in action" in the example job below:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
class RideCreateJob < ActiveJob::Base
|
52
|
+
include AcidicJob
|
53
|
+
|
54
|
+
def perform(ride_params)
|
55
|
+
idempotently with: { user: current_user, params: ride_params, ride: nil } do
|
56
|
+
step :create_ride_and_audit_record
|
57
|
+
step :create_stripe_charge
|
58
|
+
step :send_receipt
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_ride_and_audit_record
|
63
|
+
# ...
|
64
|
+
end
|
65
|
+
|
66
|
+
def create_stripe_charge
|
67
|
+
# ...
|
68
|
+
end
|
69
|
+
|
70
|
+
def send_receipt
|
71
|
+
# ...
|
72
|
+
end
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
`idempotently` takes only the `with:` named parameter and a block where you define the steps of this operation. `step` simply takes the name of a method available in the job. That's all!
|
77
|
+
|
78
|
+
So, how does `AcidicJob` make this operation idempotent and robust then? In simplest form, `AcidicJob` creates an "idempotency key" record for each job run, where it stores information about that job run, like the parameters passed in and the step the job is on. It then wraps each of your step methods in a database transaction to ensure that each step in the operation is transactionally secure. Finally, it handles a variety of edge-cases and error conditions for you as well. But, basically, by explicitly breaking your operation into steps and storing a record of each job run and updating its current step as it runs, we level up the `ActiveJob` retry mechanism to ensure that we don't retry already finished steps if something goes wrong and the job has to retry. Then, by wrapping each step in a transaction, we ensure each individual step is ACIDic. Taken together, these two strategies help us to ensure that our operational jobs are both idempotent and ACIDic.
|
26
79
|
|
27
80
|
## Development
|
28
81
|
|
data/acidic_job.gemspec
CHANGED
@@ -28,6 +28,8 @@ Gem::Specification.new do |spec|
|
|
28
28
|
spec.require_paths = ["lib"]
|
29
29
|
|
30
30
|
spec.add_dependency "activesupport"
|
31
|
+
spec.add_dependency "activerecord", ">= 4.0.0"
|
32
|
+
spec.add_development_dependency "railties", ">= 4.0"
|
31
33
|
|
32
34
|
# For more information and examples about making a new gem, checkout our
|
33
35
|
# guide at: https://bundler.io/guides/creating_gem.html
|
data/bin/console
CHANGED
data/lib/acidic_job.rb
CHANGED
@@ -6,14 +6,8 @@ require_relative "acidic_job/recovery_point"
|
|
6
6
|
require_relative "acidic_job/response"
|
7
7
|
require "active_support/concern"
|
8
8
|
|
9
|
-
# rubocop:disable Metrics/ModuleLength, Style/Documentation, Metrics/AbcSize, Metrics/MethodLength
|
9
|
+
# rubocop:disable Metrics/ModuleLength, Style/Documentation, Metrics/AbcSize, Metrics/MethodLength
|
10
10
|
module AcidicJob
|
11
|
-
class IdempotencyKeyRequired < StandardError; end
|
12
|
-
|
13
|
-
class MissingRequiredAttribute < StandardError; end
|
14
|
-
|
15
|
-
class IdempotencyKeyTooShort < StandardError; end
|
16
|
-
|
17
11
|
class MismatchedIdempotencyKeyAndJobArguments < StandardError; end
|
18
12
|
|
19
13
|
class LockedIdempotencyKey < StandardError; end
|
@@ -24,6 +18,33 @@ module AcidicJob
|
|
24
18
|
|
25
19
|
class SerializedTransactionConflict < StandardError; end
|
26
20
|
|
21
|
+
class Key < ActiveRecord::Base
|
22
|
+
RECOVERY_POINT_FINISHED = "FINISHED"
|
23
|
+
|
24
|
+
self.table_name = "acidic_job_keys"
|
25
|
+
|
26
|
+
serialize :job_args, Hash
|
27
|
+
serialize :error_object
|
28
|
+
|
29
|
+
validates :job_name, presence: true
|
30
|
+
validates :job_args, presence: true
|
31
|
+
validates :idempotency_key, presence: true
|
32
|
+
validates :last_run_at, presence: true
|
33
|
+
validates :recovery_point, presence: true
|
34
|
+
|
35
|
+
def finished?
|
36
|
+
recovery_point == RECOVERY_POINT_FINISHED
|
37
|
+
end
|
38
|
+
|
39
|
+
def succeeded?
|
40
|
+
finished? && !failed?
|
41
|
+
end
|
42
|
+
|
43
|
+
def failed?
|
44
|
+
error_object.present?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
27
48
|
extend ActiveSupport::Concern
|
28
49
|
|
29
50
|
included do
|
@@ -37,48 +58,34 @@ module AcidicJob
|
|
37
58
|
# retry_on ActiveRecord::SerializationFailure
|
38
59
|
end
|
39
60
|
|
40
|
-
class_methods do
|
41
|
-
def required(*names)
|
42
|
-
required_attributes.push(*names)
|
43
|
-
end
|
44
|
-
|
45
|
-
def required_attributes
|
46
|
-
return @required_attributes if instance_variable_defined?(:@required_attributes)
|
47
|
-
|
48
|
-
@required_attributes = []
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
61
|
# Number of seconds passed which we consider a held idempotency key lock to be
|
53
|
-
# defunct and eligible to be locked again by a different
|
62
|
+
# defunct and eligible to be locked again by a different job run. We try to
|
54
63
|
# unlock keys on our various failure conditions, but software is buggy, and
|
55
64
|
# this might not happen 100% of the time, so this is a hedge against it.
|
56
65
|
IDEMPOTENCY_KEY_LOCK_TIMEOUT = 90
|
57
66
|
|
58
|
-
# To try and enforce some level of required randomness in an idempotency key,
|
59
|
-
# we require a minimum length. This of course is a poor approximate, and in
|
60
|
-
# real life you might want to consider trying to measure actual entropy with
|
61
|
-
# something like the Shannon entropy equation.
|
62
|
-
IDEMPOTENCY_KEY_MIN_LENGTH = 20
|
63
|
-
|
64
67
|
# &block
|
65
|
-
def idempotently(
|
68
|
+
def idempotently(with:) # &block
|
66
69
|
# set accessors for each argument passed in to ensure they are available
|
67
70
|
# to the step methods the job will have written
|
68
71
|
define_accessors_for_passed_arguments(with)
|
69
72
|
|
70
|
-
validate_passed_idempotency_key(key)
|
71
|
-
validate_passed_arguments(with)
|
72
|
-
|
73
73
|
# execute the block to gather the info on what phases are defined for this job
|
74
74
|
defined_steps = yield
|
75
|
+
# [:create_ride_and_audit_record, :create_stripe_charge, :send_receipt]
|
75
76
|
|
76
77
|
# convert the array of steps into a hash of recovery_points and callable actions
|
77
78
|
phases = define_atomic_phases(defined_steps)
|
79
|
+
# { create_ride_and_audit_record: <#Method >, ... }
|
78
80
|
|
79
|
-
# find or create an
|
81
|
+
# find or create an Key record (our idempotency key) to store all information about this job
|
80
82
|
# side-effect: will set the @key instance variable
|
81
|
-
|
83
|
+
#
|
84
|
+
# A key concept here is that if two requests try to insert or update within
|
85
|
+
# close proximity, one of the two will be aborted by Postgres because we're
|
86
|
+
# using a transaction with SERIALIZABLE isolation level. It may not look
|
87
|
+
# it, but this code is safe from races.
|
88
|
+
ensure_idempotency_key_record(job_id, with, defined_steps.first)
|
82
89
|
|
83
90
|
# if the key record is already marked as finished, immediately return its result
|
84
91
|
return @key.succeeded? if @key.finished?
|
@@ -89,7 +96,7 @@ module AcidicJob
|
|
89
96
|
recovery_point = @key.recovery_point.to_sym
|
90
97
|
|
91
98
|
case recovery_point
|
92
|
-
when
|
99
|
+
when Key::RECOVERY_POINT_FINISHED.to_sym
|
93
100
|
break
|
94
101
|
else
|
95
102
|
raise UnknownRecoveryPoint unless phases.key? recovery_point
|
@@ -110,17 +117,14 @@ module AcidicJob
|
|
110
117
|
|
111
118
|
private
|
112
119
|
|
113
|
-
def atomic_phase(key
|
120
|
+
def atomic_phase(key, proc = nil, &block)
|
114
121
|
error = false
|
115
122
|
phase_callable = (proc || block)
|
116
123
|
|
117
124
|
begin
|
118
|
-
|
119
|
-
ActiveRecord::Base.transaction(isolation: :read_uncommitted) do
|
125
|
+
key.with_lock do
|
120
126
|
phase_result = phase_callable.call
|
121
127
|
|
122
|
-
# TODO: why is this here?
|
123
|
-
key ||= @key
|
124
128
|
phase_result.call(key: key)
|
125
129
|
end
|
126
130
|
rescue StandardError => e
|
@@ -129,47 +133,48 @@ module AcidicJob
|
|
129
133
|
ensure
|
130
134
|
# If we're leaving under an error condition, try to unlock the idempotency
|
131
135
|
# key right away so that another request can try again.
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
puts "Failed to unlock key #{key.id} because of #{e}."
|
139
|
-
end
|
136
|
+
begin
|
137
|
+
key.update_columns(locked_at: nil, error_object: error) if error.present?
|
138
|
+
rescue StandardError => e
|
139
|
+
# We're already inside an error condition, so swallow any additional
|
140
|
+
# errors from here and just send them to logs.
|
141
|
+
puts "Failed to unlock key #{key.id} because of #{e}."
|
140
142
|
end
|
141
143
|
end
|
142
144
|
end
|
143
145
|
|
144
|
-
def ensure_idempotency_key_record(key_val,
|
145
|
-
|
146
|
-
|
146
|
+
def ensure_idempotency_key_record(key_val, job_args, first_step)
|
147
|
+
isolation_level = case ActiveRecord::Base.connection.adapter_name.downcase.to_sym
|
148
|
+
when :sqlite
|
149
|
+
:read_uncommitted
|
150
|
+
else
|
151
|
+
:serializable
|
152
|
+
end
|
153
|
+
|
154
|
+
ActiveRecord::Base.transaction(isolation: isolation_level) do
|
155
|
+
@key = Key.find_by(idempotency_key: key_val)
|
147
156
|
|
148
157
|
if @key
|
149
158
|
# Programs enqueuing multiple jobs with different parameters but the
|
150
159
|
# same idempotency key is a bug.
|
151
|
-
raise MismatchedIdempotencyKeyAndJobArguments if @key.job_args !=
|
160
|
+
raise MismatchedIdempotencyKeyAndJobArguments if @key.job_args != job_args.as_json
|
152
161
|
|
153
162
|
# Only acquire a lock if the key is unlocked or its lock has expired
|
154
163
|
# because the original job was long enough ago.
|
155
164
|
raise LockedIdempotencyKey if @key.locked_at && @key.locked_at > Time.current - IDEMPOTENCY_KEY_LOCK_TIMEOUT
|
156
165
|
|
157
|
-
# Lock the key and update latest run unless the job is already
|
158
|
-
# finished.
|
166
|
+
# Lock the key and update latest run unless the job is already finished.
|
159
167
|
@key.update!(last_run_at: Time.current, locked_at: Time.current) unless @key.finished?
|
160
168
|
else
|
161
|
-
@key =
|
169
|
+
@key = Key.create!(
|
162
170
|
idempotency_key: key_val,
|
163
171
|
locked_at: Time.current,
|
164
172
|
last_run_at: Time.current,
|
165
173
|
recovery_point: first_step,
|
166
174
|
job_name: self.class.name,
|
167
|
-
job_args:
|
175
|
+
job_args: job_args.as_json
|
168
176
|
)
|
169
177
|
end
|
170
|
-
|
171
|
-
# no response and no need to set a recovery point
|
172
|
-
NoOp.new
|
173
178
|
end
|
174
179
|
end
|
175
180
|
|
@@ -184,33 +189,15 @@ module AcidicJob
|
|
184
189
|
true
|
185
190
|
end
|
186
191
|
|
187
|
-
def validate_passed_idempotency_key(key)
|
188
|
-
raise IdempotencyKeyRequired if key.nil?
|
189
|
-
raise IdempotencyKeyTooShort if key.length < IDEMPOTENCY_KEY_MIN_LENGTH
|
190
|
-
|
191
|
-
true
|
192
|
-
end
|
193
|
-
|
194
|
-
def validate_passed_arguments(attributes)
|
195
|
-
missing_attributes = self.class.required_attributes.select do |required_attribute|
|
196
|
-
attributes[required_attribute].nil?
|
197
|
-
end
|
198
|
-
|
199
|
-
return if missing_attributes.empty?
|
200
|
-
|
201
|
-
raise MissingRequiredAttribute,
|
202
|
-
"The following required job parameters are missing: #{missing_attributes.to_sentence}"
|
203
|
-
end
|
204
|
-
|
205
192
|
def define_atomic_phases(defined_steps)
|
206
|
-
defined_steps <<
|
193
|
+
defined_steps << Key::RECOVERY_POINT_FINISHED
|
207
194
|
|
208
195
|
{}.tap do |phases|
|
209
196
|
defined_steps.each_cons(2).map do |enter_method, exit_method|
|
210
197
|
phases[enter_method] = lambda do
|
211
198
|
method(enter_method).call
|
212
199
|
|
213
|
-
if exit_method ==
|
200
|
+
if exit_method.to_s == Key::RECOVERY_POINT_FINISHED
|
214
201
|
Response.new
|
215
202
|
else
|
216
203
|
RecoveryPoint.new(exit_method)
|
@@ -220,4 +207,4 @@ module AcidicJob
|
|
220
207
|
end
|
221
208
|
end
|
222
209
|
end
|
223
|
-
# rubocop:enable Metrics/ModuleLength, Style/Documentation, Metrics/AbcSize, Metrics/MethodLength
|
210
|
+
# rubocop:enable Metrics/ModuleLength, Style/Documentation, Metrics/AbcSize, Metrics/MethodLength
|
data/lib/acidic_job/no_op.rb
CHANGED
@@ -2,14 +2,16 @@
|
|
2
2
|
|
3
3
|
# Represents an action to set a new recovery point. One possible option for a
|
4
4
|
# return from an #atomic_phase block.
|
5
|
-
|
6
|
-
|
5
|
+
module AcidicJob
|
6
|
+
class RecoveryPoint
|
7
|
+
attr_accessor :name
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
def initialize(name)
|
10
|
+
self.name = name
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
-
|
13
|
+
def call(key:)
|
14
|
+
key.update_column(:recovery_point, name)
|
15
|
+
end
|
14
16
|
end
|
15
17
|
end
|
data/lib/acidic_job/response.rb
CHANGED
@@ -3,11 +3,13 @@
|
|
3
3
|
# Represents an action to set a new API response (which will be stored onto an
|
4
4
|
# idempotency key). One possible option for a return from an #atomic_phase
|
5
5
|
# block.
|
6
|
-
|
7
|
-
|
8
|
-
key
|
9
|
-
|
10
|
-
|
11
|
-
|
6
|
+
module AcidicJob
|
7
|
+
class Response
|
8
|
+
def call(key:)
|
9
|
+
key.update!(
|
10
|
+
locked_at: nil,
|
11
|
+
recovery_point: Key::RECOVERY_POINT_FINISHED
|
12
|
+
)
|
13
|
+
end
|
12
14
|
end
|
13
15
|
end
|
data/lib/acidic_job/version.rb
CHANGED
@@ -0,0 +1,37 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
require "rails/generators/active_record"
|
3
|
+
|
4
|
+
# This generator adds a migration for the {FriendlyId::History
|
5
|
+
# FriendlyId::History} addon.
|
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.
|
9
|
+
argument :name, type: :string, default: "random_name"
|
10
|
+
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
12
|
+
|
13
|
+
def self.next_migration_number(_path)
|
14
|
+
if instance_variable_defined?("@prev_migration_nr")
|
15
|
+
@prev_migration_nr += 1
|
16
|
+
else
|
17
|
+
@prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
|
18
|
+
end
|
19
|
+
|
20
|
+
@prev_migration_nr.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
# Copies the migration template to db/migrate.
|
24
|
+
def copy_files
|
25
|
+
migration_template "migration.rb",
|
26
|
+
"db/migrate/create_acidic_job_keys.rb"
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
def migration_class
|
31
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
32
|
+
ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"]
|
33
|
+
else
|
34
|
+
ActiveRecord::Migration
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateAcidicJobKeys < <%= migration_class %>
|
2
|
+
def change
|
3
|
+
create_table :acidic_job_keys do |t|
|
4
|
+
t.string :idempotency_key, null: false
|
5
|
+
t.string :job_name, null: false
|
6
|
+
t.text :job_args, null: false
|
7
|
+
t.datetime :last_run_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
8
|
+
t.datetime :locked_at, null: true
|
9
|
+
t.string :recovery_point, null: false
|
10
|
+
t.text :error_object
|
11
|
+
t.timestamps
|
12
|
+
|
13
|
+
t.index %i[idempotency_key job_name job_args], unique: true,
|
14
|
+
name: "idx_acidic_job_keys_on_idempotency_key_n_job_name_n_job_args"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
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.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- fractaledmind
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-08-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -24,6 +24,34 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activerecord
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 4.0.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 4.0.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: railties
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '4.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '4.0'
|
27
55
|
description: Idempotent operations for Rails apps, built on top of ActiveJob.
|
28
56
|
email:
|
29
57
|
- stephen.margheim@gmail.com
|
@@ -48,6 +76,8 @@ files:
|
|
48
76
|
- lib/acidic_job/recovery_point.rb
|
49
77
|
- lib/acidic_job/response.rb
|
50
78
|
- lib/acidic_job/version.rb
|
79
|
+
- lib/generators/acidic_job_generator.rb
|
80
|
+
- lib/generators/templates/migration.rb
|
51
81
|
homepage: https://github.com/fractaledmind/acidic_job
|
52
82
|
licenses:
|
53
83
|
- MIT
|