acidic_job 0.1.3 → 0.2.1

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: 62d9233935caa3f86b25ae851b5039dbc6fc3ca29365acceb9e2fb6c50a37e84
4
- data.tar.gz: 17488a81beea742db0f5e2ef24f40c11e43e89199e1986d95882313dc2cf5f64
3
+ metadata.gz: ed6991438cd55a757d0c3606591dd3e39cf470a2fca7486afea1d9efbb948aa6
4
+ data.tar.gz: d5f078c2111b538ebc7b16df70b2986fa7e1c958716806ad4778c81817b9d3f4
5
5
  SHA512:
6
- metadata.gz: 1634b63e1d1a6552f1f3c4615ac8376b9b055d2812a8b1e3018ba8f3297121c04f7a3c41e53b17fdfa30c0d01878183074c730907139b02cd3c4567ec5eb51ef
7
- data.tar.gz: 2d4837e2ac68315de5e6ab60b757f10cc26244de1294956f4b6834f2552062487b7969a28758a6770815c3e352e4573f8da22d65ebafe53589459580f2140560
6
+ metadata.gz: 24aa6c22959133bd68f3947bd15a742b6b314e7fb174b45fede5b598a85a63f04c2ba95a96e53ad3167fbfa1b9c9dcb981c7d59786be87d36520bab825255605
7
+ data.tar.gz: 02fad1948a7a17b81c2a6be0e48c271563fc7ce84a8de5ec5b84d3e1bb2ca65dbe6e838c4f4eb3c05eb2497424fe33fb5ea0e2c8e1001861ec75cb0c835a153a
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.3)
4
+ acidic_job (0.2.1)
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
 
@@ -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/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
@@ -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 :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
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
@@ -37,48 +58,34 @@ module AcidicJob
37
58
  # retry_on ActiveRecord::SerializationFailure
38
59
  end
39
60
 
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
61
  # 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
62
+ # defunct and eligible to be locked again by a different job run. We try to
54
63
  # unlock keys on our various failure conditions, but software is buggy, and
55
64
  # this might not happen 100% of the time, so this is a hedge against it.
56
65
  IDEMPOTENCY_KEY_LOCK_TIMEOUT = 90
57
66
 
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
67
  # &block
65
- def idempotently(key:, with:)
68
+ def idempotently(with:) # &block
66
69
  # set accessors for each argument passed in to ensure they are available
67
70
  # to the step methods the job will have written
68
71
  define_accessors_for_passed_arguments(with)
69
72
 
70
- validate_passed_idempotency_key(key)
71
- validate_passed_arguments(with)
72
-
73
73
  # execute the block to gather the info on what phases are defined for this job
74
74
  defined_steps = yield
75
+ # [:create_ride_and_audit_record, :create_stripe_charge, :send_receipt]
75
76
 
76
77
  # convert the array of steps into a hash of recovery_points and callable actions
77
78
  phases = define_atomic_phases(defined_steps)
79
+ # { create_ride_and_audit_record: <#Method >, ... }
78
80
 
79
- # 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
80
82
  # side-effect: will set the @key instance variable
81
- ensure_idempotency_key_record(key, with[:params], 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, with, defined_steps.first)
82
89
 
83
90
  # if the key record is already marked as finished, immediately return its result
84
91
  return @key.succeeded? if @key.finished?
@@ -89,7 +96,7 @@ module AcidicJob
89
96
  recovery_point = @key.recovery_point.to_sym
90
97
 
91
98
  case recovery_point
92
- when :FINISHED
99
+ when Key::RECOVERY_POINT_FINISHED.to_sym
93
100
  break
94
101
  else
95
102
  raise UnknownRecoveryPoint unless phases.key? recovery_point
@@ -110,17 +117,14 @@ module AcidicJob
110
117
 
111
118
  private
112
119
 
113
- def atomic_phase(key = nil, proc = nil, &block)
120
+ def atomic_phase(key, proc = nil, &block)
114
121
  error = false
115
122
  phase_callable = (proc || block)
116
123
 
117
124
  begin
118
- # ActiveRecord::Base.transaction(isolation: :serializable) do
119
- ActiveRecord::Base.transaction(isolation: :read_uncommitted) do
125
+ key.with_lock do
120
126
  phase_result = phase_callable.call
121
127
 
122
- # TODO: why is this here?
123
- key ||= @key
124
128
  phase_result.call(key: key)
125
129
  end
126
130
  rescue StandardError => e
@@ -129,47 +133,48 @@ module AcidicJob
129
133
  ensure
130
134
  # If we're leaving under an error condition, try to unlock the idempotency
131
135
  # key right away so that another request can try again.
132
- if error && !key.nil?
133
- begin
134
- key.update_columns(locked_at: nil, error_object: error)
135
- rescue StandardError => e
136
- # We're already inside an error condition, so swallow any additional
137
- # errors from here and just send them to logs.
138
- puts "Failed to unlock key #{key.id} because of #{e}."
139
- end
136
+ begin
137
+ key.update_columns(locked_at: nil, error_object: error) if error.present?
138
+ rescue StandardError => e
139
+ # We're already inside an error condition, so swallow any additional
140
+ # errors from here and just send them to logs.
141
+ puts "Failed to unlock key #{key.id} because of #{e}."
140
142
  end
141
143
  end
142
144
  end
143
145
 
144
- def ensure_idempotency_key_record(key_val, params, first_step)
145
- atomic_phase do
146
- @key = AcidicJobKey.find_by(idempotency_key: key_val)
146
+ def ensure_idempotency_key_record(key_val, job_args, 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
+
154
+ ActiveRecord::Base.transaction(isolation: isolation_level) do
155
+ @key = Key.find_by(idempotency_key: key_val)
147
156
 
148
157
  if @key
149
158
  # Programs enqueuing multiple jobs with different parameters but the
150
159
  # same idempotency key is a bug.
151
- raise MismatchedIdempotencyKeyAndJobArguments if @key.job_args != params.as_json
160
+ raise MismatchedIdempotencyKeyAndJobArguments if @key.job_args != job_args.as_json
152
161
 
153
162
  # Only acquire a lock if the key is unlocked or its lock has expired
154
163
  # because the original job was long enough ago.
155
164
  raise LockedIdempotencyKey if @key.locked_at && @key.locked_at > Time.current - IDEMPOTENCY_KEY_LOCK_TIMEOUT
156
165
 
157
- # Lock the key and update latest run unless the job is already
158
- # finished.
166
+ # Lock the key and update latest run unless the job is already finished.
159
167
  @key.update!(last_run_at: Time.current, locked_at: Time.current) unless @key.finished?
160
168
  else
161
- @key = AcidicJobKey.create!(
169
+ @key = Key.create!(
162
170
  idempotency_key: key_val,
163
171
  locked_at: Time.current,
164
172
  last_run_at: Time.current,
165
173
  recovery_point: first_step,
166
174
  job_name: self.class.name,
167
- job_args: params.as_json
175
+ job_args: job_args.as_json
168
176
  )
169
177
  end
170
-
171
- # no response and no need to set a recovery point
172
- NoOp.new
173
178
  end
174
179
  end
175
180
 
@@ -184,33 +189,15 @@ module AcidicJob
184
189
  true
185
190
  end
186
191
 
187
- def validate_passed_idempotency_key(key)
188
- raise IdempotencyKeyRequired if key.nil?
189
- raise IdempotencyKeyTooShort if key.length < IDEMPOTENCY_KEY_MIN_LENGTH
190
-
191
- true
192
- end
193
-
194
- def validate_passed_arguments(attributes)
195
- missing_attributes = self.class.required_attributes.select do |required_attribute|
196
- attributes[required_attribute].nil?
197
- end
198
-
199
- return if missing_attributes.empty?
200
-
201
- raise MissingRequiredAttribute,
202
- "The following required job parameters are missing: #{missing_attributes.to_sentence}"
203
- end
204
-
205
192
  def define_atomic_phases(defined_steps)
206
- defined_steps << :FINISHED
193
+ defined_steps << Key::RECOVERY_POINT_FINISHED
207
194
 
208
195
  {}.tap do |phases|
209
196
  defined_steps.each_cons(2).map do |enter_method, exit_method|
210
197
  phases[enter_method] = lambda do
211
198
  method(enter_method).call
212
199
 
213
- if exit_method == :FINISHED
200
+ if exit_method.to_s == Key::RECOVERY_POINT_FINISHED
214
201
  Response.new
215
202
  else
216
203
  RecoveryPoint.new(exit_method)
@@ -220,4 +207,4 @@ module AcidicJob
220
207
  end
221
208
  end
222
209
  end
223
- # rubocop:enable Metrics/ModuleLength, Style/Documentation, Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
210
+ # 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: 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.3"
4
+ VERSION = "0.2.1"
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.3
4
+ version: 0.2.1
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