acidic_job 0.1.4 → 0.2.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: fb1de28508c8a662b772a9b9f8fb4046d8c2a64e242826ec92c1ecc820dbc18b
4
- data.tar.gz: a0db4307ec7d7c971d9a5e1ff30ccb2243afa9ea35a7f17fe91f8c81d775a52b
3
+ metadata.gz: efbbe68a5d9822e76723f8724743f58b6566706937fa1e939f1fba67ccb8fece
4
+ data.tar.gz: 48a2cc89d6c9cc0bc13dc95abcce456b5be78bb15ae583e68fd9c72b86ed466a
5
5
  SHA512:
6
- metadata.gz: 0e39981533a09f2f197f8c047d057328be88c5a34666bd8520500a805bee7fb4d86ac74fdebe673cd67fe92c3faff072a3d62eef79e4e14b21ffdfb05b292e2b
7
- data.tar.gz: b2eb0c3c2fe92b27fc065fc0a450261743b3b6b397c2212e1b46ed4773074342c64c8433930b71f4125a71b9c692f5baa1d29b3ec9d9fc6ac13905c1fe4e2d2f
6
+ metadata.gz: 5f095c6a40548b07d4216e8939f52b533778071e4eef2df4090e35f681766a391805a394966d39a3e809e6d1c48094bbb39e768e986401a847882fe8b61ce5ef
7
+ data.tar.gz: e05b1a3f238492038c40e1a3d51110c52f7062b0143b12d78ac3aace525ea225a13db139f951bc6cf46cc04b14429beaa55c99b0f772c806014b746b88e14897
data/Gemfile.lock CHANGED
@@ -1,12 +1,26 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- acidic_job (0.1.4)
4
+ acidic_job (0.2.2)
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
@@ -1,8 +1,25 @@
1
1
  # AcidicJob
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/acidic_job`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ ### Idempotent operations for Rails apps, built on top of ActiveJob.
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
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 it yourself as:
36
+ Or simply execute to install the gem yourself:
20
37
 
21
- $ gem install acidic_job
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
- TODO: Write usage instructions here
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/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
@@ -24,6 +18,32 @@ 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 :error_object
27
+
28
+ validates :job_name, presence: true
29
+ validates :job_args, presence: true
30
+ validates :idempotency_key, 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
+
27
47
  extend ActiveSupport::Concern
28
48
 
29
49
  included do
@@ -37,47 +57,34 @@ module AcidicJob
37
57
  # retry_on ActiveRecord::SerializationFailure
38
58
  end
39
59
 
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
60
  # 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 API call. We try to
61
+ # defunct and eligible to be locked again by a different job run. We try to
54
62
  # unlock keys on our various failure conditions, but software is buggy, and
55
63
  # this might not happen 100% of the time, so this is a hedge against it.
56
64
  IDEMPOTENCY_KEY_LOCK_TIMEOUT = 90
57
65
 
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
66
  # &block
65
- def idempotently(with:)
67
+ def idempotently(with:) # &block
66
68
  # set accessors for each argument passed in to ensure they are available
67
69
  # to the step methods the job will have written
68
70
  define_accessors_for_passed_arguments(with)
69
71
 
70
- validate_passed_arguments(with)
71
-
72
72
  # execute the block to gather the info on what phases are defined for this job
73
73
  defined_steps = yield
74
+ # [:create_ride_and_audit_record, :create_stripe_charge, :send_receipt]
74
75
 
75
76
  # convert the array of steps into a hash of recovery_points and callable actions
76
77
  phases = define_atomic_phases(defined_steps)
78
+ # { create_ride_and_audit_record: <#Method >, ... }
77
79
 
78
- # find or create an AcidicJobKey record to store all information about this job
80
+ # find or create an Key record (our idempotency key) to store all information about this job
79
81
  # side-effect: will set the @key instance variable
80
- ensure_idempotency_key_record(job_id, with[:params], defined_steps.first)
82
+ #
83
+ # A key concept here is that if two requests try to insert or update within
84
+ # close proximity, one of the two will be aborted by Postgres because we're
85
+ # using a transaction with SERIALIZABLE isolation level. It may not look
86
+ # it, but this code is safe from races.
87
+ ensure_idempotency_key_record(job_id, with, defined_steps.first)
81
88
 
82
89
  # if the key record is already marked as finished, immediately return its result
83
90
  return @key.succeeded? if @key.finished?
@@ -88,7 +95,7 @@ module AcidicJob
88
95
  recovery_point = @key.recovery_point.to_sym
89
96
 
90
97
  case recovery_point
91
- when :FINISHED
98
+ when Key::RECOVERY_POINT_FINISHED.to_sym
92
99
  break
93
100
  else
94
101
  raise UnknownRecoveryPoint unless phases.key? recovery_point
@@ -135,38 +142,36 @@ module AcidicJob
135
142
  end
136
143
  end
137
144
 
138
- def ensure_idempotency_key_record(key_val, params, first_step)
139
- # isolation_level = case ActiveRecord::Base.connection.adapter_name.downcase.to_sym
140
- # when :sqlite
141
- # :read_uncommitted
142
- # else # :nocov:
143
- # :serializable # :nocov:
144
- # end
145
- isolation_level = :read_uncommitted
145
+ def ensure_idempotency_key_record(key_val, job_args, first_step)
146
+ isolation_level = case ActiveRecord::Base.connection.adapter_name.downcase.to_sym
147
+ when :sqlite
148
+ :read_uncommitted
149
+ else
150
+ :serializable
151
+ end
146
152
 
147
153
  ActiveRecord::Base.transaction(isolation: isolation_level) do
148
- @key = AcidicJobKey.find_by(idempotency_key: key_val)
154
+ @key = Key.find_by(idempotency_key: key_val)
149
155
 
150
156
  if @key
151
157
  # Programs enqueuing multiple jobs with different parameters but the
152
158
  # same idempotency key is a bug.
153
- raise MismatchedIdempotencyKeyAndJobArguments if @key.job_args != params.as_json
159
+ raise MismatchedIdempotencyKeyAndJobArguments if @key.job_args != job_args.deep_stringify_keys.inspect
154
160
 
155
161
  # Only acquire a lock if the key is unlocked or its lock has expired
156
162
  # because the original job was long enough ago.
157
163
  raise LockedIdempotencyKey if @key.locked_at && @key.locked_at > Time.current - IDEMPOTENCY_KEY_LOCK_TIMEOUT
158
164
 
159
- # Lock the key and update latest run unless the job is already
160
- # finished.
165
+ # Lock the key and update latest run unless the job is already finished.
161
166
  @key.update!(last_run_at: Time.current, locked_at: Time.current) unless @key.finished?
162
167
  else
163
- @key = AcidicJobKey.create!(
168
+ @key = Key.create!(
164
169
  idempotency_key: key_val,
165
170
  locked_at: Time.current,
166
171
  last_run_at: Time.current,
167
172
  recovery_point: first_step,
168
173
  job_name: self.class.name,
169
- job_args: params.as_json
174
+ job_args: job_args.inspect
170
175
  )
171
176
  end
172
177
  end
@@ -183,26 +188,15 @@ module AcidicJob
183
188
  true
184
189
  end
185
190
 
186
- def validate_passed_arguments(attributes)
187
- missing_attributes = self.class.required_attributes.select do |required_attribute|
188
- attributes[required_attribute].nil?
189
- end
190
-
191
- return if missing_attributes.empty?
192
-
193
- raise MissingRequiredAttribute,
194
- "The following required job parameters are missing: #{missing_attributes.to_sentence}"
195
- end
196
-
197
191
  def define_atomic_phases(defined_steps)
198
- defined_steps << :FINISHED
192
+ defined_steps << Key::RECOVERY_POINT_FINISHED
199
193
 
200
194
  {}.tap do |phases|
201
195
  defined_steps.each_cons(2).map do |enter_method, exit_method|
202
196
  phases[enter_method] = lambda do
203
197
  method(enter_method).call
204
198
 
205
- if exit_method == :FINISHED
199
+ if exit_method.to_s == Key::RECOVERY_POINT_FINISHED
206
200
  Response.new
207
201
  else
208
202
  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: Key::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.4"
4
+ VERSION = "0.2.2"
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_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
4
+ version: 0.2.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-07-08 00:00:00.000000000 Z
11
+ date: 2021-08-20 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