acidic_job 0.2.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +6 -1
- data/acidic_job.gemspec +11 -11
- data/lib/acidic_job/key.rb +29 -0
- data/lib/acidic_job/staging.rb +19 -0
- data/lib/acidic_job/version.rb +1 -1
- data/lib/acidic_job.rb +25 -38
- data/lib/generators/acidic_job_generator.rb +10 -7
- data/lib/generators/templates/{migration.rb → migration.rb.erb} +7 -3
- metadata +11 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 82e7abf5e57a4a6b68691e3c6c69ee8a6bbc2eabcedb69a3ef29e91ac0e99e8c
|
4
|
+
data.tar.gz: b73584561b8a558529b2e057e449896dedfff2c21ab859dacf82b070f75c7910
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6e42251e87faff4790ecfba572eca0dd70251fc58a4293b906560b526efa4bc14db00e01ee0e90c43a0d3f3dc355b36b8db3a4be5129529c071711f3a0b2506f
|
7
|
+
data.tar.gz: 167aa9f07dd033a5a3646edcff171a8dffea485d9cbb943c230526214c6d4eaa68761418321cc4e7795a0326361699181bd142377516ceadaa430d286628aea5
|
data/.rubocop.yml
CHANGED
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.4.0)
|
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/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,12 +23,12 @@ 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
|
-
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
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AcidicJob
|
4
|
+
class Key < ActiveRecord::Base
|
5
|
+
RECOVERY_POINT_FINISHED = "FINISHED"
|
6
|
+
|
7
|
+
self.table_name = "acidic_job_keys"
|
8
|
+
|
9
|
+
serialize :error_object
|
10
|
+
serialize :job_args
|
11
|
+
|
12
|
+
validates :idempotency_key, presence: true, uniqueness: { scope: %i[job_name job_args] }
|
13
|
+
validates :job_name, presence: true
|
14
|
+
validates :last_run_at, presence: true
|
15
|
+
validates :recovery_point, presence: true
|
16
|
+
|
17
|
+
def finished?
|
18
|
+
recovery_point == RECOVERY_POINT_FINISHED
|
19
|
+
end
|
20
|
+
|
21
|
+
def succeeded?
|
22
|
+
finished? && !failed?
|
23
|
+
end
|
24
|
+
|
25
|
+
def failed?
|
26
|
+
error_object.present?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,19 @@
|
|
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
|
data/lib/acidic_job/version.rb
CHANGED
data/lib/acidic_job.rb
CHANGED
@@ -4,9 +4,11 @@ require_relative "acidic_job/version"
|
|
4
4
|
require_relative "acidic_job/no_op"
|
5
5
|
require_relative "acidic_job/recovery_point"
|
6
6
|
require_relative "acidic_job/response"
|
7
|
+
require_relative "acidic_job/key"
|
8
|
+
require_relative "acidic_job/staging"
|
7
9
|
require "active_support/concern"
|
8
10
|
|
9
|
-
# rubocop:disable Metrics/ModuleLength,
|
11
|
+
# rubocop:disable Metrics/ModuleLength, Metrics/AbcSize, Metrics/MethodLength
|
10
12
|
module AcidicJob
|
11
13
|
class MismatchedIdempotencyKeyAndJobArguments < StandardError; end
|
12
14
|
|
@@ -18,44 +20,27 @@ module AcidicJob
|
|
18
20
|
|
19
21
|
class SerializedTransactionConflict < StandardError; end
|
20
22
|
|
21
|
-
|
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
|
23
|
+
extend ActiveSupport::Concern
|
38
24
|
|
39
|
-
|
40
|
-
|
41
|
-
end
|
25
|
+
module ActiveJobExtension
|
26
|
+
extend ActiveSupport::Concern
|
42
27
|
|
43
|
-
|
44
|
-
|
28
|
+
class_methods do
|
29
|
+
def perform_transactionally(*args)
|
30
|
+
AcidicJob::Staging.create!(
|
31
|
+
serialized_params: job_or_instantiate(*args).serialize
|
32
|
+
)
|
33
|
+
end
|
45
34
|
end
|
46
35
|
end
|
47
36
|
|
48
|
-
extend ActiveSupport::Concern
|
49
|
-
|
50
37
|
included do
|
51
38
|
attr_reader :key
|
52
39
|
|
53
|
-
#
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
# retry_on LockedIdempotencyKey
|
58
|
-
# retry_on ActiveRecord::SerializationFailure
|
40
|
+
# Extend ActiveJob only once it has been loaded
|
41
|
+
ActiveSupport.on_load(:active_job) do
|
42
|
+
send(:include, ActiveJobExtension)
|
43
|
+
end
|
59
44
|
end
|
60
45
|
|
61
46
|
# Number of seconds passed which we consider a held idempotency key lock to be
|
@@ -65,7 +50,8 @@ module AcidicJob
|
|
65
50
|
IDEMPOTENCY_KEY_LOCK_TIMEOUT = 90
|
66
51
|
|
67
52
|
# &block
|
68
|
-
|
53
|
+
# &block
|
54
|
+
def idempotently(with:)
|
69
55
|
# set accessors for each argument passed in to ensure they are available
|
70
56
|
# to the step methods the job will have written
|
71
57
|
define_accessors_for_passed_arguments(with)
|
@@ -85,7 +71,7 @@ module AcidicJob
|
|
85
71
|
# close proximity, one of the two will be aborted by Postgres because we're
|
86
72
|
# using a transaction with SERIALIZABLE isolation level. It may not look
|
87
73
|
# it, but this code is safe from races.
|
88
|
-
ensure_idempotency_key_record(job_id,
|
74
|
+
ensure_idempotency_key_record(job_id, defined_steps.first)
|
89
75
|
|
90
76
|
# if the key record is already marked as finished, immediately return its result
|
91
77
|
return @key.succeeded? if @key.finished?
|
@@ -143,13 +129,14 @@ module AcidicJob
|
|
143
129
|
end
|
144
130
|
end
|
145
131
|
|
146
|
-
def ensure_idempotency_key_record(key_val,
|
132
|
+
def ensure_idempotency_key_record(key_val, first_step)
|
147
133
|
isolation_level = case ActiveRecord::Base.connection.adapter_name.downcase.to_sym
|
148
134
|
when :sqlite
|
149
135
|
:read_uncommitted
|
150
136
|
else
|
151
137
|
:serializable
|
152
138
|
end
|
139
|
+
serialized_job_info = serialize
|
153
140
|
|
154
141
|
ActiveRecord::Base.transaction(isolation: isolation_level) do
|
155
142
|
@key = Key.find_by(idempotency_key: key_val)
|
@@ -157,7 +144,7 @@ module AcidicJob
|
|
157
144
|
if @key
|
158
145
|
# Programs enqueuing multiple jobs with different parameters but the
|
159
146
|
# same idempotency key is a bug.
|
160
|
-
raise MismatchedIdempotencyKeyAndJobArguments if @key.job_args !=
|
147
|
+
raise MismatchedIdempotencyKeyAndJobArguments if @key.job_args != serialized_job_info["arguments"]
|
161
148
|
|
162
149
|
# Only acquire a lock if the key is unlocked or its lock has expired
|
163
150
|
# because the original job was long enough ago.
|
@@ -171,8 +158,8 @@ module AcidicJob
|
|
171
158
|
locked_at: Time.current,
|
172
159
|
last_run_at: Time.current,
|
173
160
|
recovery_point: first_step,
|
174
|
-
job_name:
|
175
|
-
job_args:
|
161
|
+
job_name: serialized_job_info["job_class"],
|
162
|
+
job_args: serialized_job_info["arguments"]
|
176
163
|
)
|
177
164
|
end
|
178
165
|
end
|
@@ -207,4 +194,4 @@ module AcidicJob
|
|
207
194
|
end
|
208
195
|
end
|
209
196
|
end
|
210
|
-
# rubocop:enable Metrics/ModuleLength,
|
197
|
+
# rubocop:enable Metrics/ModuleLength, Metrics/AbcSize, Metrics/MethodLength
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "rails/generators"
|
2
4
|
require "rails/generators/active_record"
|
3
5
|
|
@@ -22,16 +24,17 @@ class AcidicJobGenerator < ActiveRecord::Generators::Base
|
|
22
24
|
|
23
25
|
# Copies the migration template to db/migrate.
|
24
26
|
def copy_files
|
25
|
-
migration_template "migration.rb",
|
27
|
+
migration_template "migration.rb.erb",
|
26
28
|
"db/migrate/create_acidic_job_keys.rb"
|
27
29
|
end
|
28
30
|
|
29
31
|
protected
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
32
|
+
|
33
|
+
def migration_class
|
34
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
35
|
+
ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"]
|
36
|
+
else
|
37
|
+
ActiveRecord::Migration
|
36
38
|
end
|
39
|
+
end
|
37
40
|
end
|
@@ -3,15 +3,19 @@ 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"
|
16
|
+
|
17
|
+
create_table :acidic_job_stagings do |t|
|
18
|
+
t.text :serialized_params, null: false
|
15
19
|
end
|
16
20
|
end
|
17
21
|
end
|
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.
|
4
|
+
version: 0.4.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-
|
11
|
+
date: 2021-09-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: activerecord
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
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:
|
26
|
+
version: 4.0.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: activesupport
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
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:
|
40
|
+
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: railties
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -72,12 +72,14 @@ files:
|
|
72
72
|
- bin/console
|
73
73
|
- bin/setup
|
74
74
|
- lib/acidic_job.rb
|
75
|
+
- lib/acidic_job/key.rb
|
75
76
|
- lib/acidic_job/no_op.rb
|
76
77
|
- lib/acidic_job/recovery_point.rb
|
77
78
|
- lib/acidic_job/response.rb
|
79
|
+
- lib/acidic_job/staging.rb
|
78
80
|
- lib/acidic_job/version.rb
|
79
81
|
- lib/generators/acidic_job_generator.rb
|
80
|
-
- lib/generators/templates/migration.rb
|
82
|
+
- lib/generators/templates/migration.rb.erb
|
81
83
|
homepage: https://github.com/fractaledmind/acidic_job
|
82
84
|
licenses:
|
83
85
|
- MIT
|