acidic_job 0.1.5 → 0.3.0

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: 1c51b3ed20bebb6322ab050f122c83d863fe16436e302b61f91eea84fd9cc117
4
- data.tar.gz: 7f65a1d6c99d23930c4730557388833b3ebd277117394b18d6665978bf35119d
3
+ metadata.gz: 528d362023c8d2547054566f13d65556e057285124ffb05f8bc4c6007848b5cb
4
+ data.tar.gz: 05d08959ff8b674b48966ce4b879fd8c290d658a20c24b6af27e0c1fa8150452
5
5
  SHA512:
6
- metadata.gz: 3a1910297ba003ca354ede4dd5312d12af4f6662d8cb0eca451f4f01e51ba54d80f3f27632785d2ba7c69d5504da042861d122c7a81356d19ca04046097928cd
7
- data.tar.gz: 6d57ce3ec53d5ed22bfd91f284611c505400ad7f524f0fbfe0bec4fbf335e94fd4c91781248980342d127186c5973dfed9cd32011c2e6b2e1c6ea72531247f87
6
+ metadata.gz: 68bdc77c5e23e4fa680ca9ec72e00cf62cc1bcb2fc133b539d4c17f68b8926da9edd19f895ff92a0a379db567aee9ee949f0ade58eee0ef8b38edd61d28b5c19
7
+ data.tar.gz: b352b70b51cf7f3244a057dc5d6b2a89ede6e9b317346dd0fd0d54e9115ffff84a86670ac27c3bd45dc8b8f28d535de9af308fbd1b6be8dbd6eec76a7d740485
data/Gemfile CHANGED
@@ -24,3 +24,5 @@ gem "sqlite3"
24
24
  gem "database_cleaner"
25
25
 
26
26
  gem "simplecov"
27
+
28
+ gem "pry"
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.3.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,10 @@ GEM
22
36
  tzinfo (~> 2.0)
23
37
  zeitwerk (~> 2.3)
24
38
  ast (2.4.2)
39
+ builder (3.2.4)
40
+ coderay (1.1.3)
25
41
  concurrent-ruby (1.1.9)
42
+ crass (1.0.6)
26
43
  database_cleaner (2.0.1)
27
44
  database_cleaner-active_record (~> 2.0.0)
28
45
  database_cleaner-active_record (2.0.1)
@@ -30,14 +47,43 @@ GEM
30
47
  database_cleaner-core (~> 2.0.0)
31
48
  database_cleaner-core (2.0.1)
32
49
  docile (1.4.0)
50
+ erubi (1.10.0)
33
51
  globalid (0.4.2)
34
52
  activesupport (>= 4.2.0)
35
53
  i18n (1.8.10)
36
54
  concurrent-ruby (~> 1.0)
55
+ loofah (2.12.0)
56
+ crass (~> 1.0.2)
57
+ nokogiri (>= 1.5.9)
58
+ method_source (1.0.0)
59
+ mini_portile2 (2.6.1)
37
60
  minitest (5.14.4)
61
+ nokogiri (1.12.3)
62
+ mini_portile2 (~> 2.6.1)
63
+ racc (~> 1.4)
64
+ nokogiri (1.12.3-x86_64-darwin)
65
+ racc (~> 1.4)
38
66
  parallel (1.20.1)
39
67
  parser (3.0.1.1)
40
68
  ast (~> 2.4.1)
69
+ pry (0.14.1)
70
+ coderay (~> 1.1)
71
+ method_source (~> 1.0)
72
+ racc (1.5.2)
73
+ rack (2.2.3)
74
+ rack-test (1.1.0)
75
+ rack (>= 1.0, < 3)
76
+ rails-dom-testing (2.0.3)
77
+ activesupport (>= 4.2.0)
78
+ nokogiri (>= 1.6)
79
+ rails-html-sanitizer (1.4.1)
80
+ loofah (~> 2.3)
81
+ railties (6.1.3.2)
82
+ actionpack (= 6.1.3.2)
83
+ activesupport (= 6.1.3.2)
84
+ method_source
85
+ rake (>= 0.8.7)
86
+ thor (~> 1.0)
41
87
  rainbow (3.0.0)
42
88
  rake (13.0.4)
43
89
  regexp_parser (2.1.1)
@@ -65,6 +111,7 @@ GEM
65
111
  simplecov-html (0.12.3)
66
112
  simplecov_json_formatter (0.1.3)
67
113
  sqlite3 (1.4.2)
114
+ thor (1.1.0)
68
115
  tzinfo (2.0.4)
69
116
  concurrent-ruby (~> 1.0)
70
117
  unicode-display_width (2.0.0)
@@ -80,6 +127,8 @@ DEPENDENCIES
80
127
  activerecord (~> 6.1.3.2)
81
128
  database_cleaner
82
129
  minitest (~> 5.0)
130
+ pry
131
+ railties (>= 4.0)
83
132
  rake (~> 13.0)
84
133
  rubocop (~> 1.7)
85
134
  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
 
@@ -33,14 +33,14 @@ And then execute:
33
33
 
34
34
  $ bundle install
35
35
 
36
- Or install it yourself as:
36
+ Or simply execute to install the gem yourself:
37
37
 
38
- $ gem install acidic_job
38
+ $ bundle add acidic_job
39
39
 
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
@@ -3,15 +3,15 @@
3
3
  require_relative "lib/acidic_job/version"
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.name = "acidic_job"
7
- spec.version = AcidicJob::VERSION
8
- spec.authors = ["fractaledmind"]
9
- spec.email = ["stephen.margheim@gmail.com"]
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 = "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"
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,11 +23,13 @@ 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 = "exe"
27
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
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"
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
@@ -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.5"
4
+ VERSION = "0.3.0"
5
5
  end
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,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 :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 :job_args, 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
@@ -38,32 +59,33 @@ module AcidicJob
38
59
  end
39
60
 
40
61
  # 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
62
+ # defunct and eligible to be locked again by a different job run. We try to
42
63
  # unlock keys on our various failure conditions, but software is buggy, and
43
64
  # this might not happen 100% of the time, so this is a hedge against it.
44
65
  IDEMPOTENCY_KEY_LOCK_TIMEOUT = 90
45
66
 
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
67
  # &block
53
- def idempotently(with:)
68
+ def idempotently(with:) # &block
54
69
  # set accessors for each argument passed in to ensure they are available
55
70
  # to the step methods the job will have written
56
71
  define_accessors_for_passed_arguments(with)
57
72
 
58
73
  # execute the block to gather the info on what phases are defined for this job
59
74
  defined_steps = yield
75
+ # [:create_ride_and_audit_record, :create_stripe_charge, :send_receipt]
60
76
 
61
77
  # convert the array of steps into a hash of recovery_points and callable actions
62
78
  phases = define_atomic_phases(defined_steps)
79
+ # { create_ride_and_audit_record: <#Method >, ... }
63
80
 
64
- # find or create an AcidicJobKey record to store all information about this job
81
+ # find or create an Key record (our idempotency key) to store all information about this job
65
82
  # side-effect: will set the @key instance variable
66
- ensure_idempotency_key_record(job_id, with, defined_steps.first)
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, defined_steps.first)
67
89
 
68
90
  # if the key record is already marked as finished, immediately return its result
69
91
  return @key.succeeded? if @key.finished?
@@ -74,7 +96,7 @@ module AcidicJob
74
96
  recovery_point = @key.recovery_point.to_sym
75
97
 
76
98
  case recovery_point
77
- when :FINISHED
99
+ when Key::RECOVERY_POINT_FINISHED.to_sym
78
100
  break
79
101
  else
80
102
  raise UnknownRecoveryPoint unless phases.key? recovery_point
@@ -105,7 +127,7 @@ module AcidicJob
105
127
 
106
128
  phase_result.call(key: key)
107
129
  end
108
- rescue StandardError => e
130
+ rescue => e
109
131
  error = e
110
132
  raise e
111
133
  ensure
@@ -113,7 +135,7 @@ module AcidicJob
113
135
  # key right away so that another request can try again.
114
136
  begin
115
137
  key.update_columns(locked_at: nil, error_object: error) if error.present?
116
- rescue StandardError => e
138
+ rescue => e
117
139
  # We're already inside an error condition, so swallow any additional
118
140
  # errors from here and just send them to logs.
119
141
  puts "Failed to unlock key #{key.id} because of #{e}."
@@ -121,38 +143,37 @@ module AcidicJob
121
143
  end
122
144
  end
123
145
 
124
- 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
146
+ def ensure_idempotency_key_record(key_val, 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
+ serialized_job_info = serialize
132
154
 
133
155
  ActiveRecord::Base.transaction(isolation: isolation_level) do
134
- @key = AcidicJobKey.find_by(idempotency_key: key_val)
156
+ @key = Key.find_by(idempotency_key: key_val)
135
157
 
136
158
  if @key
137
159
  # Programs enqueuing multiple jobs with different parameters but the
138
160
  # same idempotency key is a bug.
139
- raise MismatchedIdempotencyKeyAndJobArguments if @key.job_args != job_args.as_json
161
+ raise MismatchedIdempotencyKeyAndJobArguments if @key.job_args != serialized_job_info["arguments"]
140
162
 
141
163
  # Only acquire a lock if the key is unlocked or its lock has expired
142
164
  # because the original job was long enough ago.
143
165
  raise LockedIdempotencyKey if @key.locked_at && @key.locked_at > Time.current - IDEMPOTENCY_KEY_LOCK_TIMEOUT
144
166
 
145
- # Lock the key and update latest run unless the job is already
146
- # finished.
167
+ # Lock the key and update latest run unless the job is already finished.
147
168
  @key.update!(last_run_at: Time.current, locked_at: Time.current) unless @key.finished?
148
169
  else
149
- @key = AcidicJobKey.create!(
170
+ @key = Key.create!(
150
171
  idempotency_key: key_val,
151
172
  locked_at: Time.current,
152
173
  last_run_at: Time.current,
153
174
  recovery_point: first_step,
154
- job_name: self.class.name,
155
- job_args: job_args.as_json
175
+ job_name: serialized_job_info["job_class"],
176
+ job_args: serialized_job_info["arguments"]
156
177
  )
157
178
  end
158
179
  end
@@ -170,14 +191,14 @@ module AcidicJob
170
191
  end
171
192
 
172
193
  def define_atomic_phases(defined_steps)
173
- defined_steps << :FINISHED
194
+ defined_steps << Key::RECOVERY_POINT_FINISHED
174
195
 
175
196
  {}.tap do |phases|
176
197
  defined_steps.each_cons(2).map do |enter_method, exit_method|
177
198
  phases[enter_method] = lambda do
178
199
  method(enter_method).call
179
200
 
180
- if exit_method == :FINISHED
201
+ if exit_method.to_s == Key::RECOVERY_POINT_FINISHED
181
202
  Response.new
182
203
  else
183
204
  RecoveryPoint.new(exit_method)
@@ -0,0 +1,38 @@
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
+
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
+ end
37
+ end
38
+ end
@@ -0,0 +1,18 @@
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],
14
+ unique: true,
15
+ name: "idx_acidic_job_keys_on_idempotency_key_n_job_name_n_job_args"
16
+ end
17
+ end
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.1.5
4
+ version: 0.3.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-25 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