acidic_job 0.1.2 → 0.2.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: 9681eb2a123d15b96ea8666da752886f1e8365f4be846cef4a2597299378ae37
4
- data.tar.gz: 83c1a7cafb42e833352f9c919d96d1d2e46e2e22830952dacb468102307f4bc0
3
+ metadata.gz: bcdd5a6f2496a5764d0d0b07f1263653a299d2dd2594ac051393278466f59bd2
4
+ data.tar.gz: 25de84d0345eb47d2c1866b0f7e6824278214aeac1e212adad3504d9682c70a8
5
5
  SHA512:
6
- metadata.gz: 1662be5778950bfd429798568aa115c294f992a9dbe530559e9f2f0093f5a767eee5667507f771148ec596a64c78be7a19d55b2965c52a1a94fdc6ae39d3562c
7
- data.tar.gz: ac8b78d17d4772af7ef4661e4d7aef3b2686c73485e3809ad72dad0ab5cd3c08e630b605a294adcf49f89766ebbbc6b28dd70d0ccbae559afabf3599a744e342
6
+ metadata.gz: 64ddc179ec70f82ebce87438daad5d50a199daee773fd453f7dff27b21c26af5f7a35fe96f7167405b5a24eb275af6e9519cbe53018194ad9388ae0e2c276412
7
+ data.tar.gz: a960d42269f80fedcd3c913ce7398a8c456759c07c58710533476f3648e38acde98921bdc93a4d26be8fa4405852d20ffbd0685802c114d87e5b9a6650941f59
data/Gemfile CHANGED
@@ -11,6 +11,10 @@ gem "minitest", "~> 5.0"
11
11
 
12
12
  gem "rubocop", "~> 1.7"
13
13
 
14
+ gem "rubocop-minitest"
15
+
16
+ gem "rubocop-rake"
17
+
14
18
  gem "activerecord", "~> 6.1.3.2"
15
19
 
16
20
  gem "activejob", "~> 6.1.3.2"
data/Gemfile.lock CHANGED
@@ -1,12 +1,26 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- acidic_job (0.1.2)
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)
@@ -53,6 +95,10 @@ GEM
53
95
  unicode-display_width (>= 1.4.0, < 3.0)
54
96
  rubocop-ast (1.7.0)
55
97
  parser (>= 3.0.1.1)
98
+ rubocop-minitest (0.14.0)
99
+ rubocop (>= 0.90, < 2.0)
100
+ rubocop-rake (0.6.0)
101
+ rubocop (~> 1.0)
56
102
  ruby-progressbar (1.11.0)
57
103
  simplecov (0.21.2)
58
104
  docile (~> 1.1)
@@ -61,12 +107,14 @@ GEM
61
107
  simplecov-html (0.12.3)
62
108
  simplecov_json_formatter (0.1.3)
63
109
  sqlite3 (1.4.2)
110
+ thor (1.1.0)
64
111
  tzinfo (2.0.4)
65
112
  concurrent-ruby (~> 1.0)
66
113
  unicode-display_width (2.0.0)
67
114
  zeitwerk (2.4.2)
68
115
 
69
116
  PLATFORMS
117
+ ruby
70
118
  x86_64-darwin-17
71
119
 
72
120
  DEPENDENCIES
@@ -75,8 +123,11 @@ DEPENDENCIES
75
123
  activerecord (~> 6.1.3.2)
76
124
  database_cleaner
77
125
  minitest (~> 5.0)
126
+ railties (>= 4.0)
78
127
  rake (~> 13.0)
79
128
  rubocop (~> 1.7)
129
+ rubocop-minitest
130
+ rubocop-rake
80
131
  simplecov
81
132
  sqlite3
82
133
 
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
 
@@ -20,9 +37,45 @@ Or install it yourself as:
20
37
 
21
38
  $ gem install acidic_job
22
39
 
40
+ Then, use the following command to copy over the AcidicJobKey migration.
41
+
42
+ ```
43
+ rails generate acidic_job
44
+ ```
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/bin/console CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  require "bundler/setup"
5
5
  require "acidic_job"
6
+ require_relative "../test/setup"
6
7
 
7
8
  # You can add fixtures and/or initialization code here to make experimenting
8
9
  # with your gem easier. You can also use a different console, if you like.
data/lib/acidic_job.rb CHANGED
@@ -6,14 +6,8 @@ require_relative "acidic_job/recovery_point"
6
6
  require_relative "acidic_job/response"
7
7
  require "active_support/concern"
8
8
 
9
- # rubocop:disable Metrics/ModuleLength, Style/Documentation, Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
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
@@ -37,48 +31,34 @@ module AcidicJob
37
31
  # retry_on ActiveRecord::SerializationFailure
38
32
  end
39
33
 
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
34
  # 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
35
+ # defunct and eligible to be locked again by a different job run. We try to
54
36
  # unlock keys on our various failure conditions, but software is buggy, and
55
37
  # this might not happen 100% of the time, so this is a hedge against it.
56
38
  IDEMPOTENCY_KEY_LOCK_TIMEOUT = 90
57
39
 
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
40
  # &block
65
- def idempotently(key:, with:)
41
+ def idempotently(with:) # &block
66
42
  # set accessors for each argument passed in to ensure they are available
67
43
  # to the step methods the job will have written
68
44
  define_accessors_for_passed_arguments(with)
69
45
 
70
- validate_passed_idempotency_key(key)
71
- validate_passed_arguments(with)
72
-
73
46
  # execute the block to gather the info on what phases are defined for this job
74
47
  defined_steps = yield
48
+ # [:create_ride_and_audit_record, :create_stripe_charge, :send_receipt]
75
49
 
76
50
  # convert the array of steps into a hash of recovery_points and callable actions
77
51
  phases = define_atomic_phases(defined_steps)
52
+ # { create_ride_and_audit_record: <#Method >, ... }
78
53
 
79
- # 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
80
55
  # side-effect: will set the @key instance variable
81
- ensure_idempotency_key_record(key, with[:params], defined_steps.first)
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.
61
+ ensure_idempotency_key_record(job_id, with, defined_steps.first)
82
62
 
83
63
  # if the key record is already marked as finished, immediately return its result
84
64
  return @key.succeeded? if @key.finished?
@@ -89,7 +69,7 @@ module AcidicJob
89
69
  recovery_point = @key.recovery_point.to_sym
90
70
 
91
71
  case recovery_point
92
- when :FINISHED
72
+ when AcidicJobKey::RECOVERY_POINT_FINISHED.to_sym
93
73
  break
94
74
  else
95
75
  raise UnknownRecoveryPoint unless phases.key? recovery_point
@@ -110,21 +90,14 @@ module AcidicJob
110
90
 
111
91
  private
112
92
 
113
- def atomic_phase(key = nil, proc = nil, &block)
93
+ def atomic_phase(key, proc = nil, &block)
114
94
  error = false
115
95
  phase_callable = (proc || block)
116
96
 
117
97
  begin
118
- # ActiveRecord::Base.transaction(isolation: :serializable) do
119
- ActiveRecord::Base.transaction(isolation: :read_uncommitted) do
98
+ key.with_lock do
120
99
  phase_result = phase_callable.call
121
100
 
122
- raise UnknownAtomicPhaseType unless phase_result.is_a?(NoOp) ||
123
- phase_result.is_a?(RecoveryPoint) ||
124
- phase_result.is_a?(Response)
125
-
126
- # TODO: why is this here?
127
- key ||= @key
128
101
  phase_result.call(key: key)
129
102
  end
130
103
  rescue StandardError => e
@@ -133,33 +106,37 @@ module AcidicJob
133
106
  ensure
134
107
  # If we're leaving under an error condition, try to unlock the idempotency
135
108
  # key right away so that another request can try again.
136
- if error && !key.nil?
137
- begin
138
- key.update_columns(locked_at: nil, error_object: error)
139
- rescue StandardError => e
140
- # We're already inside an error condition, so swallow any additional
141
- # errors from here and just send them to logs.
142
- puts "Failed to unlock key #{key.id} because of #{e}."
143
- end
109
+ begin
110
+ key.update_columns(locked_at: nil, error_object: error) if error.present?
111
+ rescue StandardError => e
112
+ # We're already inside an error condition, so swallow any additional
113
+ # errors from here and just send them to logs.
114
+ puts "Failed to unlock key #{key.id} because of #{e}."
144
115
  end
145
116
  end
146
117
  end
147
118
 
148
- def ensure_idempotency_key_record(key_val, params, first_step)
149
- atomic_phase do
119
+ def ensure_idempotency_key_record(key_val, job_args, first_step)
120
+ isolation_level = case ActiveRecord::Base.connection.adapter_name.downcase.to_sym
121
+ when :sqlite
122
+ :read_uncommitted
123
+ else
124
+ :serializable
125
+ end
126
+
127
+ ActiveRecord::Base.transaction(isolation: isolation_level) do
150
128
  @key = AcidicJobKey.find_by(idempotency_key: key_val)
151
129
 
152
130
  if @key
153
131
  # Programs enqueuing multiple jobs with different parameters but the
154
132
  # same idempotency key is a bug.
155
- raise MismatchedIdempotencyKeyAndJobArguments if @key.job_args != params.as_json
133
+ raise MismatchedIdempotencyKeyAndJobArguments if @key.job_args != job_args.as_json
156
134
 
157
135
  # Only acquire a lock if the key is unlocked or its lock has expired
158
136
  # because the original job was long enough ago.
159
137
  raise LockedIdempotencyKey if @key.locked_at && @key.locked_at > Time.current - IDEMPOTENCY_KEY_LOCK_TIMEOUT
160
138
 
161
- # Lock the key and update latest run unless the job is already
162
- # finished.
139
+ # Lock the key and update latest run unless the job is already finished.
163
140
  @key.update!(last_run_at: Time.current, locked_at: Time.current) unless @key.finished?
164
141
  else
165
142
  @key = AcidicJobKey.create!(
@@ -168,12 +145,9 @@ module AcidicJob
168
145
  last_run_at: Time.current,
169
146
  recovery_point: first_step,
170
147
  job_name: self.class.name,
171
- job_args: params.as_json
148
+ job_args: job_args.as_json
172
149
  )
173
150
  end
174
-
175
- # no response and no need to set a recovery point
176
- NoOp.new
177
151
  end
178
152
  end
179
153
 
@@ -188,33 +162,15 @@ module AcidicJob
188
162
  true
189
163
  end
190
164
 
191
- def validate_passed_idempotency_key(key)
192
- raise IdempotencyKeyRequired if key.nil?
193
- raise IdempotencyKeyTooShort if key.length < IDEMPOTENCY_KEY_MIN_LENGTH
194
-
195
- true
196
- end
197
-
198
- def validate_passed_arguments(attributes)
199
- missing_attributes = self.class.required_attributes.select do |required_attribute|
200
- attributes[required_attribute].nil?
201
- end
202
-
203
- return if missing_attributes.empty?
204
-
205
- raise MissingRequiredAttribute,
206
- "The following required job parameters are missing: #{missing_attributes.to_sentence}"
207
- end
208
-
209
165
  def define_atomic_phases(defined_steps)
210
- defined_steps << :FINISHED
166
+ defined_steps << AcidicJobKey::RECOVERY_POINT_FINISHED
211
167
 
212
168
  {}.tap do |phases|
213
169
  defined_steps.each_cons(2).map do |enter_method, exit_method|
214
170
  phases[enter_method] = lambda do
215
171
  method(enter_method).call
216
172
 
217
- if exit_method == :FINISHED
173
+ if exit_method.to_s == AcidicJobKey::RECOVERY_POINT_FINISHED
218
174
  Response.new
219
175
  else
220
176
  RecoveryPoint.new(exit_method)
@@ -224,4 +180,4 @@ module AcidicJob
224
180
  end
225
181
  end
226
182
  end
227
- # rubocop:enable Metrics/ModuleLength, Style/Documentation, Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
183
+ # rubocop:enable Metrics/ModuleLength, Style/Documentation, Metrics/AbcSize, Metrics/MethodLength
@@ -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.2"
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.2
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-07 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