acidic_job 0.2.0 → 0.3.1
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 +6 -1
- data/README.md +2 -2
- data/acidic_job.gemspec +10 -10
- data/lib/acidic_job/response.rb +1 -1
- data/lib/acidic_job/version.rb +1 -1
- data/lib/acidic_job.rb +41 -14
- data/lib/generators/acidic_job_generator.rb +8 -7
- data/lib/generators/templates/migration.rb +4 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8aa8aee9c23bc58be992813f139864afdb057442ee9c359263f3ff45029e3f28
|
4
|
+
data.tar.gz: 9dbb8017fcbf91ce5cbcc5c96968e3b880340c1e1cc7a8c36cb11e744d6b405d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4bb2298f924a2aa2d5db31c7c05201188872333c36dd3e0bafb598ed087ec9a686695c27b1cdd2d33f067f2bbb47178c5bc700201406db856392997ef6605dbc
|
7
|
+
data.tar.gz: f842586dc2d669196ce58bcbc279bc2b967fd0c8973d296323622c993e8d08422ce65f55b85cf96407aa4a668af15dea005fdf3f8917e520ca020c2851dd4bfa
|
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.3.1)
|
5
5
|
activerecord (>= 4.0.0)
|
6
6
|
activesupport
|
7
7
|
|
@@ -37,6 +37,7 @@ GEM
|
|
37
37
|
zeitwerk (~> 2.3)
|
38
38
|
ast (2.4.2)
|
39
39
|
builder (3.2.4)
|
40
|
+
coderay (1.1.3)
|
40
41
|
concurrent-ruby (1.1.9)
|
41
42
|
crass (1.0.6)
|
42
43
|
database_cleaner (2.0.1)
|
@@ -65,6 +66,9 @@ GEM
|
|
65
66
|
parallel (1.20.1)
|
66
67
|
parser (3.0.1.1)
|
67
68
|
ast (~> 2.4.1)
|
69
|
+
pry (0.14.1)
|
70
|
+
coderay (~> 1.1)
|
71
|
+
method_source (~> 1.0)
|
68
72
|
racc (1.5.2)
|
69
73
|
rack (2.2.3)
|
70
74
|
rack-test (1.1.0)
|
@@ -123,6 +127,7 @@ DEPENDENCIES
|
|
123
127
|
activerecord (~> 6.1.3.2)
|
124
128
|
database_cleaner
|
125
129
|
minitest (~> 5.0)
|
130
|
+
pry
|
126
131
|
railties (>= 4.0)
|
127
132
|
rake (~> 13.0)
|
128
133
|
rubocop (~> 1.7)
|
data/README.md
CHANGED
@@ -33,9 +33,9 @@ And then execute:
|
|
33
33
|
|
34
34
|
$ bundle install
|
35
35
|
|
36
|
-
Or install
|
36
|
+
Or simply execute to install the gem yourself:
|
37
37
|
|
38
|
-
$
|
38
|
+
$ bundle add acidic_job
|
39
39
|
|
40
40
|
Then, use the following command to copy over the AcidicJobKey migration.
|
41
41
|
|
data/acidic_job.gemspec
CHANGED
@@ -3,15 +3,15 @@
|
|
3
3
|
require_relative "lib/acidic_job/version"
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
|
-
spec.name
|
7
|
-
spec.version
|
8
|
-
spec.authors
|
9
|
-
spec.email
|
6
|
+
spec.name = "acidic_job"
|
7
|
+
spec.version = AcidicJob::VERSION
|
8
|
+
spec.authors = ["fractaledmind"]
|
9
|
+
spec.email = ["stephen.margheim@gmail.com"]
|
10
10
|
|
11
|
-
spec.summary
|
12
|
-
spec.description
|
13
|
-
spec.homepage
|
14
|
-
spec.license
|
11
|
+
spec.summary = "Idempotent operations for Rails apps, built on top of ActiveJob."
|
12
|
+
spec.description = "Idempotent operations for Rails apps, built on top of ActiveJob."
|
13
|
+
spec.homepage = "https://github.com/fractaledmind/acidic_job"
|
14
|
+
spec.license = "MIT"
|
15
15
|
spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
|
16
16
|
|
17
17
|
spec.metadata["homepage_uri"] = spec.homepage
|
@@ -23,8 +23,8 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
24
24
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
25
25
|
end
|
26
|
-
spec.bindir
|
27
|
-
spec.executables
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
28
28
|
spec.require_paths = ["lib"]
|
29
29
|
|
30
30
|
spec.add_dependency "activesupport"
|
data/lib/acidic_job/response.rb
CHANGED
data/lib/acidic_job/version.rb
CHANGED
data/lib/acidic_job.rb
CHANGED
@@ -18,6 +18,32 @@ module AcidicJob
|
|
18
18
|
|
19
19
|
class SerializedTransactionConflict < StandardError; end
|
20
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
|
33
|
+
|
34
|
+
def finished?
|
35
|
+
recovery_point == RECOVERY_POINT_FINISHED
|
36
|
+
end
|
37
|
+
|
38
|
+
def succeeded?
|
39
|
+
finished? && !failed?
|
40
|
+
end
|
41
|
+
|
42
|
+
def failed?
|
43
|
+
error_object.present?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
21
47
|
extend ActiveSupport::Concern
|
22
48
|
|
23
49
|
included do
|
@@ -51,14 +77,14 @@ module AcidicJob
|
|
51
77
|
phases = define_atomic_phases(defined_steps)
|
52
78
|
# { create_ride_and_audit_record: <#Method >, ... }
|
53
79
|
|
54
|
-
# find or create an
|
80
|
+
# find or create an Key record (our idempotency key) to store all information about this job
|
55
81
|
# side-effect: will set the @key instance variable
|
56
82
|
#
|
57
83
|
# A key concept here is that if two requests try to insert or update within
|
58
84
|
# close proximity, one of the two will be aborted by Postgres because we're
|
59
85
|
# using a transaction with SERIALIZABLE isolation level. It may not look
|
60
86
|
# it, but this code is safe from races.
|
61
|
-
ensure_idempotency_key_record(job_id,
|
87
|
+
ensure_idempotency_key_record(job_id, defined_steps.first)
|
62
88
|
|
63
89
|
# if the key record is already marked as finished, immediately return its result
|
64
90
|
return @key.succeeded? if @key.finished?
|
@@ -69,7 +95,7 @@ module AcidicJob
|
|
69
95
|
recovery_point = @key.recovery_point.to_sym
|
70
96
|
|
71
97
|
case recovery_point
|
72
|
-
when
|
98
|
+
when Key::RECOVERY_POINT_FINISHED.to_sym
|
73
99
|
break
|
74
100
|
else
|
75
101
|
raise UnknownRecoveryPoint unless phases.key? recovery_point
|
@@ -100,7 +126,7 @@ module AcidicJob
|
|
100
126
|
|
101
127
|
phase_result.call(key: key)
|
102
128
|
end
|
103
|
-
rescue
|
129
|
+
rescue => e
|
104
130
|
error = e
|
105
131
|
raise e
|
106
132
|
ensure
|
@@ -108,7 +134,7 @@ module AcidicJob
|
|
108
134
|
# key right away so that another request can try again.
|
109
135
|
begin
|
110
136
|
key.update_columns(locked_at: nil, error_object: error) if error.present?
|
111
|
-
rescue
|
137
|
+
rescue => e
|
112
138
|
# We're already inside an error condition, so swallow any additional
|
113
139
|
# errors from here and just send them to logs.
|
114
140
|
puts "Failed to unlock key #{key.id} because of #{e}."
|
@@ -116,21 +142,22 @@ module AcidicJob
|
|
116
142
|
end
|
117
143
|
end
|
118
144
|
|
119
|
-
def ensure_idempotency_key_record(key_val,
|
145
|
+
def ensure_idempotency_key_record(key_val, first_step)
|
120
146
|
isolation_level = case ActiveRecord::Base.connection.adapter_name.downcase.to_sym
|
121
147
|
when :sqlite
|
122
148
|
:read_uncommitted
|
123
149
|
else
|
124
150
|
:serializable
|
125
|
-
|
151
|
+
end
|
152
|
+
serialized_job_info = serialize
|
126
153
|
|
127
154
|
ActiveRecord::Base.transaction(isolation: isolation_level) do
|
128
|
-
@key =
|
155
|
+
@key = Key.find_by(idempotency_key: key_val)
|
129
156
|
|
130
157
|
if @key
|
131
158
|
# Programs enqueuing multiple jobs with different parameters but the
|
132
159
|
# same idempotency key is a bug.
|
133
|
-
raise MismatchedIdempotencyKeyAndJobArguments if @key.job_args !=
|
160
|
+
raise MismatchedIdempotencyKeyAndJobArguments if @key.job_args != serialized_job_info["arguments"]
|
134
161
|
|
135
162
|
# Only acquire a lock if the key is unlocked or its lock has expired
|
136
163
|
# because the original job was long enough ago.
|
@@ -139,13 +166,13 @@ module AcidicJob
|
|
139
166
|
# Lock the key and update latest run unless the job is already finished.
|
140
167
|
@key.update!(last_run_at: Time.current, locked_at: Time.current) unless @key.finished?
|
141
168
|
else
|
142
|
-
@key =
|
169
|
+
@key = Key.create!(
|
143
170
|
idempotency_key: key_val,
|
144
171
|
locked_at: Time.current,
|
145
172
|
last_run_at: Time.current,
|
146
173
|
recovery_point: first_step,
|
147
|
-
job_name:
|
148
|
-
job_args:
|
174
|
+
job_name: serialized_job_info["job_class"],
|
175
|
+
job_args: serialized_job_info["arguments"]
|
149
176
|
)
|
150
177
|
end
|
151
178
|
end
|
@@ -163,14 +190,14 @@ module AcidicJob
|
|
163
190
|
end
|
164
191
|
|
165
192
|
def define_atomic_phases(defined_steps)
|
166
|
-
defined_steps <<
|
193
|
+
defined_steps << Key::RECOVERY_POINT_FINISHED
|
167
194
|
|
168
195
|
{}.tap do |phases|
|
169
196
|
defined_steps.each_cons(2).map do |enter_method, exit_method|
|
170
197
|
phases[enter_method] = lambda do
|
171
198
|
method(enter_method).call
|
172
199
|
|
173
|
-
if exit_method.to_s ==
|
200
|
+
if exit_method.to_s == Key::RECOVERY_POINT_FINISHED
|
174
201
|
Response.new
|
175
202
|
else
|
176
203
|
RecoveryPoint.new(exit_method)
|
@@ -23,15 +23,16 @@ class AcidicJobGenerator < ActiveRecord::Generators::Base
|
|
23
23
|
# Copies the migration template to db/migrate.
|
24
24
|
def copy_files
|
25
25
|
migration_template "migration.rb",
|
26
|
-
|
26
|
+
"db/migrate/create_acidic_job_keys.rb"
|
27
27
|
end
|
28
28
|
|
29
29
|
protected
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
30
|
+
|
31
|
+
def migration_class
|
32
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
33
|
+
ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"]
|
34
|
+
else
|
35
|
+
ActiveRecord::Migration
|
36
36
|
end
|
37
|
+
end
|
37
38
|
end
|
@@ -3,15 +3,16 @@ class CreateAcidicJobKeys < <%= migration_class %>
|
|
3
3
|
create_table :acidic_job_keys do |t|
|
4
4
|
t.string :idempotency_key, null: false
|
5
5
|
t.string :job_name, null: false
|
6
|
-
t.text :job_args, null:
|
6
|
+
t.text :job_args, null: true
|
7
7
|
t.datetime :last_run_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
8
8
|
t.datetime :locked_at, null: true
|
9
9
|
t.string :recovery_point, null: false
|
10
10
|
t.text :error_object
|
11
11
|
t.timestamps
|
12
12
|
|
13
|
-
t.index %i[idempotency_key job_name job_args],
|
14
|
-
|
13
|
+
t.index %i[idempotency_key job_name job_args],
|
14
|
+
unique: true,
|
15
|
+
name: "idx_acidic_job_keys_on_idempotency_key_n_job_name_n_job_args"
|
15
16
|
end
|
16
17
|
end
|
17
18
|
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.
|
4
|
+
version: 0.3.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-09-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|