acidic_job 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c51b3ed20bebb6322ab050f122c83d863fe16436e302b61f91eea84fd9cc117
4
- data.tar.gz: 7f65a1d6c99d23930c4730557388833b3ebd277117394b18d6665978bf35119d
3
+ metadata.gz: bcdd5a6f2496a5764d0d0b07f1263653a299d2dd2594ac051393278466f59bd2
4
+ data.tar.gz: 25de84d0345eb47d2c1866b0f7e6824278214aeac1e212adad3504d9682c70a8
5
5
  SHA512:
6
- metadata.gz: 3a1910297ba003ca354ede4dd5312d12af4f6662d8cb0eca451f4f01e51ba54d80f3f27632785d2ba7c69d5504da042861d122c7a81356d19ca04046097928cd
7
- data.tar.gz: 6d57ce3ec53d5ed22bfd91f284611c505400ad7f524f0fbfe0bec4fbf335e94fd4c91781248980342d127186c5973dfed9cd32011c2e6b2e1c6ea72531247f87
6
+ metadata.gz: 64ddc179ec70f82ebce87438daad5d50a199daee773fd453f7dff27b21c26af5f7a35fe96f7167405b5a24eb275af6e9519cbe53018194ad9388ae0e2c276412
7
+ data.tar.gz: a960d42269f80fedcd3c913ce7398a8c456759c07c58710533476f3648e38acde98921bdc93a4d26be8fa4405852d20ffbd0685802c114d87e5b9a6650941f59
data/Gemfile.lock CHANGED
@@ -1,12 +1,26 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- acidic_job (0.1.5)
4
+ acidic_job (0.2.0)
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)
@@ -65,6 +107,7 @@ GEM
65
107
  simplecov-html (0.12.3)
66
108
  simplecov_json_formatter (0.1.3)
67
109
  sqlite3 (1.4.2)
110
+ thor (1.1.0)
68
111
  tzinfo (2.0.4)
69
112
  concurrent-ruby (~> 1.0)
70
113
  unicode-display_width (2.0.0)
@@ -80,6 +123,7 @@ DEPENDENCIES
80
123
  activerecord (~> 6.1.3.2)
81
124
  database_cleaner
82
125
  minitest (~> 5.0)
126
+ railties (>= 4.0)
83
127
  rake (~> 13.0)
84
128
  rubocop (~> 1.7)
85
129
  rubocop-minitest
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ### Idempotent operations for Rails apps, built on top of ActiveJob.
4
4
 
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 seemlessly.
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
6
 
7
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
8
 
@@ -19,7 +19,7 @@ This is, of course, far easier said than done. Thus, `AcidicJob`.
19
19
  3. https://brandur.org/job-drain
20
20
  4. https://brandur.org/idempotency-keys
21
21
 
22
- `AcididJob` brings these techniques and principles into the world of a standard Rails application.
22
+ `AcidicJob` brings these techniques and principles into the world of a standard Rails application.
23
23
 
24
24
  ## Installation
25
25
 
@@ -40,7 +40,7 @@ Or install it yourself as:
40
40
  Then, use the following command to copy over the AcidicJobKey migration.
41
41
 
42
42
  ```
43
- rails generate acidic_job:key
43
+ rails generate acidic_job
44
44
  ```
45
45
 
46
46
  ## Usage
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/lib/acidic_job.rb CHANGED
@@ -8,12 +8,6 @@ require "active_support/concern"
8
8
 
9
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
@@ -38,31 +32,32 @@ module AcidicJob
38
32
  end
39
33
 
40
34
  # Number of seconds passed which we consider a held idempotency key lock to be
41
- # defunct and eligible to be locked again by a different API call. We try to
35
+ # defunct and eligible to be locked again by a different job run. We try to
42
36
  # unlock keys on our various failure conditions, but software is buggy, and
43
37
  # this might not happen 100% of the time, so this is a hedge against it.
44
38
  IDEMPOTENCY_KEY_LOCK_TIMEOUT = 90
45
39
 
46
- # To try and enforce some level of required randomness in an idempotency key,
47
- # we require a minimum length. This of course is a poor approximate, and in
48
- # real life you might want to consider trying to measure actual entropy with
49
- # something like the Shannon entropy equation.
50
- IDEMPOTENCY_KEY_MIN_LENGTH = 20
51
-
52
40
  # &block
53
- def idempotently(with:)
41
+ def idempotently(with:) # &block
54
42
  # set accessors for each argument passed in to ensure they are available
55
43
  # to the step methods the job will have written
56
44
  define_accessors_for_passed_arguments(with)
57
45
 
58
46
  # execute the block to gather the info on what phases are defined for this job
59
47
  defined_steps = yield
48
+ # [:create_ride_and_audit_record, :create_stripe_charge, :send_receipt]
60
49
 
61
50
  # convert the array of steps into a hash of recovery_points and callable actions
62
51
  phases = define_atomic_phases(defined_steps)
52
+ # { create_ride_and_audit_record: <#Method >, ... }
63
53
 
64
- # find or create an AcidicJobKey record to store all information about this job
54
+ # find or create an AcidicJobKey record (our idempotency key) to store all information about this job
65
55
  # side-effect: will set the @key instance variable
56
+ #
57
+ # A key concept here is that if two requests try to insert or update within
58
+ # close proximity, one of the two will be aborted by Postgres because we're
59
+ # using a transaction with SERIALIZABLE isolation level. It may not look
60
+ # it, but this code is safe from races.
66
61
  ensure_idempotency_key_record(job_id, with, defined_steps.first)
67
62
 
68
63
  # if the key record is already marked as finished, immediately return its result
@@ -74,7 +69,7 @@ module AcidicJob
74
69
  recovery_point = @key.recovery_point.to_sym
75
70
 
76
71
  case recovery_point
77
- when :FINISHED
72
+ when AcidicJobKey::RECOVERY_POINT_FINISHED.to_sym
78
73
  break
79
74
  else
80
75
  raise UnknownRecoveryPoint unless phases.key? recovery_point
@@ -122,13 +117,12 @@ module AcidicJob
122
117
  end
123
118
 
124
119
  def ensure_idempotency_key_record(key_val, job_args, first_step)
125
- # isolation_level = case ActiveRecord::Base.connection.adapter_name.downcase.to_sym
126
- # when :sqlite
127
- # :read_uncommitted
128
- # else # :nocov:
129
- # :serializable # :nocov:
130
- # end
131
- isolation_level = :read_uncommitted
120
+ isolation_level = case ActiveRecord::Base.connection.adapter_name.downcase.to_sym
121
+ when :sqlite
122
+ :read_uncommitted
123
+ else
124
+ :serializable
125
+ end
132
126
 
133
127
  ActiveRecord::Base.transaction(isolation: isolation_level) do
134
128
  @key = AcidicJobKey.find_by(idempotency_key: key_val)
@@ -142,8 +136,7 @@ module AcidicJob
142
136
  # because the original job was long enough ago.
143
137
  raise LockedIdempotencyKey if @key.locked_at && @key.locked_at > Time.current - IDEMPOTENCY_KEY_LOCK_TIMEOUT
144
138
 
145
- # Lock the key and update latest run unless the job is already
146
- # finished.
139
+ # Lock the key and update latest run unless the job is already finished.
147
140
  @key.update!(last_run_at: Time.current, locked_at: Time.current) unless @key.finished?
148
141
  else
149
142
  @key = AcidicJobKey.create!(
@@ -170,14 +163,14 @@ module AcidicJob
170
163
  end
171
164
 
172
165
  def define_atomic_phases(defined_steps)
173
- defined_steps << :FINISHED
166
+ defined_steps << AcidicJobKey::RECOVERY_POINT_FINISHED
174
167
 
175
168
  {}.tap do |phases|
176
169
  defined_steps.each_cons(2).map do |enter_method, exit_method|
177
170
  phases[enter_method] = lambda do
178
171
  method(enter_method).call
179
172
 
180
- if exit_method == :FINISHED
173
+ if exit_method.to_s == AcidicJobKey::RECOVERY_POINT_FINISHED
181
174
  Response.new
182
175
  else
183
176
  RecoveryPoint.new(exit_method)
@@ -2,8 +2,10 @@
2
2
 
3
3
  # Represents an action to perform a no-op. One possible option for a return
4
4
  # from an #atomic_phase block.
5
- class NoOp
6
- def call(_key)
7
- # no-op
5
+ module AcidicJob
6
+ class NoOp
7
+ def call(_key)
8
+ # no-op
9
+ end
8
10
  end
9
11
  end
@@ -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
- class RecoveryPoint
6
- attr_accessor :name
5
+ module AcidicJob
6
+ class RecoveryPoint
7
+ attr_accessor :name
7
8
 
8
- def initialize(name)
9
- self.name = name
10
- end
9
+ def initialize(name)
10
+ self.name = name
11
+ end
11
12
 
12
- def call(key:)
13
- key.update(recovery_point: name)
13
+ def call(key:)
14
+ key.update_column(:recovery_point, name)
15
+ end
14
16
  end
15
17
  end
@@ -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
- class Response
7
- def call(key:)
8
- key.update!(
9
- locked_at: nil,
10
- recovery_point: :FINISHED
11
- )
6
+ module AcidicJob
7
+ class Response
8
+ def call(key:)
9
+ key.update!(
10
+ locked_at: nil,
11
+ recovery_point: AcidicJobKey::RECOVERY_POINT_FINISHED
12
+ )
13
+ end
12
14
  end
13
15
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AcidicJob
4
- VERSION = "0.1.5"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -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_acidid_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.5
4
+ version: 0.2.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-07-09 00:00:00.000000000 Z
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