acidic_job 1.0.0.pre17 → 1.0.0.pre20

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: c458faa1225d1b5e0fe7590bdd00371169de92b638dfbc74e88958b2430c5c7e
4
- data.tar.gz: 8657eea5fd2a3929d049be9a7da1a252413cab1de42b991178673328202f4ee1
3
+ metadata.gz: e608eefef4baffbfb2c95dcbef942a53c9ca242c4c593ddbbe0d677ef7d318da
4
+ data.tar.gz: 65bed32739c1a1a860102e59dac69598c4357512034d8d3e87ed772469b9bac0
5
5
  SHA512:
6
- metadata.gz: 4ae6d08046b2bc2e3388544fa61f308cf57c9dab5a4f67a923d1400ca7743fb48966a9aa0ce504a2d16d5c7e80fc98d8fd5308583236ff305ffb0d1b2eb17c70
7
- data.tar.gz: afee2f7723b316410d1144cf71c42cbace68ff304842cc1c29cd0a358c297ae47f9ae1166e3cd1daef8e677dbdd38b11fb64e771d2d1975afc399cafb8329129
6
+ metadata.gz: 5a5f6155e20f929c631392f00480f57928455778e13117609457be9b8624e7f16c41661df96ae34a4045582f957b044cbdf811bfa7e187dd627b8b72b80edb90
7
+ data.tar.gz: 6a663d288bd1ef3ba5cfac8e137fd496ebbca76e08573e50d706ed2570c98b6fe3148b8c9335b55313a091ccd0cc226e3b9023878dd418d400af33fd0280c1b4
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- acidic_job (1.0.0.pre17)
4
+ acidic_job (1.0.0.pre20)
5
5
  activerecord
6
6
  activesupport
7
7
  database_cleaner
@@ -9,67 +9,67 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- actioncable (7.0.2.3)
13
- actionpack (= 7.0.2.3)
14
- activesupport (= 7.0.2.3)
12
+ actioncable (7.0.3)
13
+ actionpack (= 7.0.3)
14
+ activesupport (= 7.0.3)
15
15
  nio4r (~> 2.0)
16
16
  websocket-driver (>= 0.6.1)
17
- actionmailbox (7.0.2.3)
18
- actionpack (= 7.0.2.3)
19
- activejob (= 7.0.2.3)
20
- activerecord (= 7.0.2.3)
21
- activestorage (= 7.0.2.3)
22
- activesupport (= 7.0.2.3)
17
+ actionmailbox (7.0.3)
18
+ actionpack (= 7.0.3)
19
+ activejob (= 7.0.3)
20
+ activerecord (= 7.0.3)
21
+ activestorage (= 7.0.3)
22
+ activesupport (= 7.0.3)
23
23
  mail (>= 2.7.1)
24
24
  net-imap
25
25
  net-pop
26
26
  net-smtp
27
- actionmailer (7.0.2.3)
28
- actionpack (= 7.0.2.3)
29
- actionview (= 7.0.2.3)
30
- activejob (= 7.0.2.3)
31
- activesupport (= 7.0.2.3)
27
+ actionmailer (7.0.3)
28
+ actionpack (= 7.0.3)
29
+ actionview (= 7.0.3)
30
+ activejob (= 7.0.3)
31
+ activesupport (= 7.0.3)
32
32
  mail (~> 2.5, >= 2.5.4)
33
33
  net-imap
34
34
  net-pop
35
35
  net-smtp
36
36
  rails-dom-testing (~> 2.0)
37
- actionpack (7.0.2.3)
38
- actionview (= 7.0.2.3)
39
- activesupport (= 7.0.2.3)
37
+ actionpack (7.0.3)
38
+ actionview (= 7.0.3)
39
+ activesupport (= 7.0.3)
40
40
  rack (~> 2.0, >= 2.2.0)
41
41
  rack-test (>= 0.6.3)
42
42
  rails-dom-testing (~> 2.0)
43
43
  rails-html-sanitizer (~> 1.0, >= 1.2.0)
44
- actiontext (7.0.2.3)
45
- actionpack (= 7.0.2.3)
46
- activerecord (= 7.0.2.3)
47
- activestorage (= 7.0.2.3)
48
- activesupport (= 7.0.2.3)
44
+ actiontext (7.0.3)
45
+ actionpack (= 7.0.3)
46
+ activerecord (= 7.0.3)
47
+ activestorage (= 7.0.3)
48
+ activesupport (= 7.0.3)
49
49
  globalid (>= 0.6.0)
50
50
  nokogiri (>= 1.8.5)
51
- actionview (7.0.2.3)
52
- activesupport (= 7.0.2.3)
51
+ actionview (7.0.3)
52
+ activesupport (= 7.0.3)
53
53
  builder (~> 3.1)
54
54
  erubi (~> 1.4)
55
55
  rails-dom-testing (~> 2.0)
56
56
  rails-html-sanitizer (~> 1.1, >= 1.2.0)
57
- activejob (7.0.2.3)
58
- activesupport (= 7.0.2.3)
57
+ activejob (7.0.3)
58
+ activesupport (= 7.0.3)
59
59
  globalid (>= 0.3.6)
60
- activemodel (7.0.2.3)
61
- activesupport (= 7.0.2.3)
62
- activerecord (7.0.2.3)
63
- activemodel (= 7.0.2.3)
64
- activesupport (= 7.0.2.3)
65
- activestorage (7.0.2.3)
66
- actionpack (= 7.0.2.3)
67
- activejob (= 7.0.2.3)
68
- activerecord (= 7.0.2.3)
69
- activesupport (= 7.0.2.3)
60
+ activemodel (7.0.3)
61
+ activesupport (= 7.0.3)
62
+ activerecord (7.0.3)
63
+ activemodel (= 7.0.3)
64
+ activesupport (= 7.0.3)
65
+ activestorage (7.0.3)
66
+ actionpack (= 7.0.3)
67
+ activejob (= 7.0.3)
68
+ activerecord (= 7.0.3)
69
+ activesupport (= 7.0.3)
70
70
  marcel (~> 1.0)
71
71
  mini_mime (>= 1.1.0)
72
- activesupport (7.0.2.3)
72
+ activesupport (7.0.3)
73
73
  concurrent-ruby (~> 1.0, >= 1.0.2)
74
74
  i18n (>= 1.6, < 2)
75
75
  minitest (>= 5.1)
@@ -82,7 +82,7 @@ GEM
82
82
  activesupport (>= 3.0.0)
83
83
  railties (>= 3.0.0)
84
84
  thor (>= 0.14.6)
85
- concurrent-ruby (1.1.9)
85
+ concurrent-ruby (1.1.10)
86
86
  connection_pool (2.2.5)
87
87
  crass (1.0.6)
88
88
  database_cleaner (2.0.1)
@@ -112,11 +112,10 @@ GEM
112
112
  http-form_data (2.3.0)
113
113
  i18n (1.10.0)
114
114
  concurrent-ruby (~> 1.0)
115
- io-wait (0.2.1)
116
115
  llhttp-ffi (0.4.0)
117
116
  ffi-compiler (~> 1.0)
118
117
  rake (~> 13.0)
119
- loofah (2.15.0)
118
+ loofah (2.18.0)
120
119
  crass (~> 1.0.2)
121
120
  nokogiri (>= 1.5.9)
122
121
  mail (2.7.1)
@@ -134,52 +133,51 @@ GEM
134
133
  digest
135
134
  net-protocol
136
135
  timeout
137
- net-protocol (0.1.2)
138
- io-wait
136
+ net-protocol (0.1.3)
139
137
  timeout
140
138
  net-smtp (0.3.1)
141
139
  digest
142
140
  net-protocol
143
141
  timeout
144
142
  nio4r (2.5.8)
145
- nokogiri (1.13.3)
143
+ nokogiri (1.13.6)
146
144
  mini_portile2 (~> 2.8.0)
147
145
  racc (~> 1.4)
148
- nokogiri (1.13.3-x86_64-darwin)
146
+ nokogiri (1.13.6-x86_64-darwin)
149
147
  racc (~> 1.4)
150
148
  noticed (1.5.9)
151
149
  http (>= 4.0.0)
152
150
  rails (>= 5.2.0)
153
- parallel (1.21.0)
154
- parser (3.1.1.0)
151
+ parallel (1.22.1)
152
+ parser (3.1.2.0)
155
153
  ast (~> 2.4.1)
156
- public_suffix (4.0.6)
154
+ public_suffix (4.0.7)
157
155
  racc (1.6.0)
158
156
  rack (2.2.3)
159
157
  rack-test (1.1.0)
160
158
  rack (>= 1.0, < 3)
161
- rails (7.0.2.3)
162
- actioncable (= 7.0.2.3)
163
- actionmailbox (= 7.0.2.3)
164
- actionmailer (= 7.0.2.3)
165
- actionpack (= 7.0.2.3)
166
- actiontext (= 7.0.2.3)
167
- actionview (= 7.0.2.3)
168
- activejob (= 7.0.2.3)
169
- activemodel (= 7.0.2.3)
170
- activerecord (= 7.0.2.3)
171
- activestorage (= 7.0.2.3)
172
- activesupport (= 7.0.2.3)
159
+ rails (7.0.3)
160
+ actioncable (= 7.0.3)
161
+ actionmailbox (= 7.0.3)
162
+ actionmailer (= 7.0.3)
163
+ actionpack (= 7.0.3)
164
+ actiontext (= 7.0.3)
165
+ actionview (= 7.0.3)
166
+ activejob (= 7.0.3)
167
+ activemodel (= 7.0.3)
168
+ activerecord (= 7.0.3)
169
+ activestorage (= 7.0.3)
170
+ activesupport (= 7.0.3)
173
171
  bundler (>= 1.15.0)
174
- railties (= 7.0.2.3)
172
+ railties (= 7.0.3)
175
173
  rails-dom-testing (2.0.3)
176
174
  activesupport (>= 4.2.0)
177
175
  nokogiri (>= 1.6)
178
176
  rails-html-sanitizer (1.4.2)
179
177
  loofah (~> 2.3)
180
- railties (7.0.2.3)
181
- actionpack (= 7.0.2.3)
182
- activesupport (= 7.0.2.3)
178
+ railties (7.0.3)
179
+ actionpack (= 7.0.3)
180
+ activesupport (= 7.0.3)
183
181
  method_source
184
182
  rake (>= 12.2)
185
183
  thor (~> 1.0)
@@ -187,20 +185,20 @@ GEM
187
185
  rainbow (3.1.1)
188
186
  rake (13.0.6)
189
187
  redis (4.6.0)
190
- regexp_parser (2.2.1)
188
+ regexp_parser (2.4.0)
191
189
  rexml (3.2.5)
192
- rubocop (1.26.0)
190
+ rubocop (1.29.1)
193
191
  parallel (~> 1.10)
194
192
  parser (>= 3.1.0.0)
195
193
  rainbow (>= 2.2.2, < 4.0)
196
194
  regexp_parser (>= 1.8, < 3.0)
197
- rexml
198
- rubocop-ast (>= 1.16.0, < 2.0)
195
+ rexml (>= 3.2.5, < 4.0)
196
+ rubocop-ast (>= 1.17.0, < 2.0)
199
197
  ruby-progressbar (~> 1.7)
200
198
  unicode-display_width (>= 1.4.0, < 3.0)
201
- rubocop-ast (1.16.0)
199
+ rubocop-ast (1.18.0)
202
200
  parser (>= 3.1.1.0)
203
- rubocop-minitest (0.18.0)
201
+ rubocop-minitest (0.19.1)
204
202
  rubocop (>= 0.90, < 2.0)
205
203
  rubocop-rake (0.6.0)
206
204
  rubocop (~> 1.0)
@@ -216,7 +214,7 @@ GEM
216
214
  simplecov-html (0.12.3)
217
215
  simplecov_json_formatter (0.1.4)
218
216
  sqlite3 (1.4.2)
219
- strscan (3.0.1)
217
+ strscan (3.0.3)
220
218
  thor (1.2.1)
221
219
  timeout (0.2.0)
222
220
  tzinfo (2.0.4)
data/README.md CHANGED
@@ -68,8 +68,10 @@ It provides a suite of functionality that empowers you to create complex, robust
68
68
  * Persisted Attributes — when retrying jobs at later steps, we need to ensure that data created in previous steps is still available to later steps on retry.
69
69
  * Transactionally Staged Jobs — enqueue additional jobs within the acidic transaction safely
70
70
  * Custom Idempotency Keys — use something other than the job ID for the idempotency key of the job run
71
+ * Iterable Steps — define steps that iterate over some collection fully until moving on to the next step
71
72
  * Sidekiq Callbacks — bring ActiveJob-like callbacks into your pure Sidekiq Workers
72
73
  * Sidekiq Batches — leverage the power of Sidekiq Pro's `batch` functionality without the hassle
74
+ * Run Finished Callbacks — set callbacks for when a job run finishes fully
73
75
 
74
76
  ### Transactional Steps
75
77
 
@@ -80,9 +82,10 @@ class RideCreateJob < ActiveJob::Base
80
82
  include AcidicJob
81
83
 
82
84
  def perform(user_id, ride_params)
83
- user = User.find(user_id)
85
+ @user = User.find(user_id)
86
+ @params = ride_params
84
87
 
85
- with_acidity providing: { user: user, params: ride_params, ride: nil } do
88
+ with_acidity providing: { ride: nil } do
86
89
  step :create_ride_and_audit_record
87
90
  step :create_stripe_charge
88
91
  step :send_receipt
@@ -128,7 +131,7 @@ class RideCreateJob < ActiveJob::Base
128
131
  end
129
132
 
130
133
  def create_stripe_charge
131
- Stripe::Charge.create(amount: 20_00, customer: self.ride.user)
134
+ Stripe::Charge.create(amount: 20_00, customer: @ride.user)
132
135
  end
133
136
 
134
137
  # ...
@@ -139,6 +142,8 @@ end
139
142
 
140
143
  **Note:** You will note the use of `self.ride = ...` in the code sample above. In order to call the attribute setter method that will sync with the database record, you _must_ use this style. `@ride = ...` and/or `ride = ...` will both fail to sync the value with the database record.
141
144
 
145
+ The default pattern you should follow when defining your `perform` method is to make any values that your `step` methods need access to, but are present at the start of the `perform` method simply instance variables. You only need to `provide` attributes that will be set _during a step_. This means, the initial value will almost always be `nil`.
146
+
142
147
  ### Transactionally Staged Jobs
143
148
 
144
149
  A standard problem when inside of database transactions is enqueuing other jobs. On the one hand, you could enqueue a job inside of a transaction that then rollbacks, which would leave that job to fail and retry and fail. On the other hand, you could enqueue a job that is picked up before the transaction commits, which would mean the records are not yet available to this job.
@@ -150,9 +155,10 @@ class RideCreateJob < ActiveJob::Base
150
155
  include AcidicJob
151
156
 
152
157
  def perform(user_id, ride_params)
153
- user = User.find(user_id)
158
+ @user = User.find(user_id)
159
+ @params = ride_params
154
160
 
155
- with_acidity providing: { user: user, params: ride_params, ride: nil } do
161
+ with_acidity providing: { ride: nil } do
156
162
  step :create_ride_and_audit_record
157
163
  step :create_stripe_charge
158
164
  step :send_receipt
@@ -211,6 +217,28 @@ end
211
217
 
212
218
  > **Note:** The signature of the `acidic_by` proc _needs to match the signature_ of the job's `perform` method.
213
219
 
220
+ ### Iterable Steps
221
+
222
+ Sometimes our workflows have steps that need to iterate over a collection and perform an action for each item in the collection before moving on to the next step in the workflow. In these cases, we can use the `for_each` option when defining our step to specific the collection, and `acidic_job` will pass each item into your step method for processing, keeping the same transactional guarantees as for any step. This means that if your step encounters an error in processing any item in the collection, when your job is retried, the job will jump right back to that step and right back to that item in the collection to try again.
223
+
224
+ ```ruby
225
+ class ExampleJob < ActiveJob::Base
226
+ include AcidicJob
227
+
228
+ def perform(record:)
229
+ with_acidity providing: { collection: [1, 2, 3, 4, 5] } do
230
+ step :process_item, for_each: :collection
231
+ step :next_step
232
+ end
233
+ end
234
+
235
+ def process_item(item)
236
+ # do whatever work needs to be done with this individual item
237
+ end
238
+ end
239
+ ```
240
+
241
+ **Note:** The same restrictions apply here as for any persisted attribute — you can only use objects that can be serialized by ActiveRecord.
214
242
 
215
243
  ### Sidekiq Callbacks
216
244
 
@@ -229,9 +257,10 @@ class RideCreateJob < ActiveJob::Base
229
257
  include AcidicJob
230
258
 
231
259
  def perform(user_id, ride_params)
232
- user = User.find(user_id)
260
+ @user = User.find(user_id)
261
+ @params = ride_params
233
262
 
234
- with_acidity providing: { user: user, params: ride_params, ride: nil } do
263
+ with_acidity providing: { ride: nil } do
235
264
  step :create_ride_and_audit_record, awaits: [SomeJob]
236
265
  step :create_stripe_charge, args: [1, 2, 3], kwargs: { some: 'thing' }
237
266
  step :send_receipt
@@ -240,6 +269,36 @@ class RideCreateJob < ActiveJob::Base
240
269
  end
241
270
  ```
242
271
 
272
+ ### Run Finished Callbacks
273
+
274
+ When working with workflow jobs that make use of the `awaits` feature for a step, it is important to remember that the `after_perform` callback will be called _as soon as the first `awaits` step has enqueued job_, and **not** when the entire job run has finished. `acidic_job` allows the `perform` method to finish so that the queue for the workflow job is cleared to pick up new work while the `awaits` jobs are running. `acidic_job` will automatically re-enqueue the workflow job and progress to the next step when all of the `awaits` jobs have successfully finished. However, this means that `after_perform` **is not necessarily** the same as `after_finish`. In order to provide the opportunity for you to execute callback logic _if and only if_ a job run has finished, we provide callback hooks for the `finish` event.
275
+
276
+ For example, you could use this hook to immediately clean up the `AcidicJob::Run` database record whenever the workflow job finishes successfully like so:
277
+
278
+ ```ruby
279
+ class RideCreateJob < ActiveJob::Base
280
+ include AcidicJob
281
+ set_callback :finish, :after, :delete_run_record
282
+
283
+ def perform(user_id, ride_params)
284
+ @user = User.find(user_id)
285
+ @params = ride_params
286
+
287
+ with_acidity providing: { ride: nil } do
288
+ step :create_ride_and_audit_record, awaits: [SomeJob]
289
+ step :create_stripe_charge, args: [1, 2, 3], kwargs: { some: 'thing' }
290
+ step :send_receipt
291
+ end
292
+ end
293
+
294
+ def delete_run_record
295
+ return unless acidic_job_run.succeeded?
296
+
297
+ acidic_job_run.destroy!
298
+ end
299
+ end
300
+ ```
301
+
243
302
  ## Testing
244
303
 
245
304
  When testing acidic jobs, you are likely to run into `ActiveRecord::TransactionIsolationError`s:
@@ -17,8 +17,11 @@ module AcidicJob
17
17
  "#{self.class.name}#step_done",
18
18
  # NOTE: options are marshalled through JSON so use only basic types.
19
19
  { "run_id" => run.id,
20
- "step_result_yaml" => step_result.to_yaml.strip }
20
+ "step_result_yaml" => step_result.to_yaml.strip,
21
+ "parent_worker" => self.class.name,
22
+ "job_names" => jobs.map(&:to_s) }
21
23
  )
24
+
22
25
  # NOTE: The jobs method is atomic.
23
26
  # All jobs created in the block are actually pushed atomically at the end of the block.
24
27
  # If an error is raised, none of the jobs will go to Redis.
@@ -26,7 +29,8 @@ module AcidicJob
26
29
  jobs.each do |worker_name|
27
30
  # TODO: handle Symbols as well
28
31
  worker = worker_name.is_a?(String) ? worker_name.constantize : worker_name
29
- if worker.instance_method(:perform).arity.zero?
32
+
33
+ if worker.instance_method(:perform).arity.presence_in [0, -1]
30
34
  worker.perform_async
31
35
  elsif worker.instance_method(:perform).arity == 1
32
36
  worker.perform_async(run.id)
@@ -54,9 +54,9 @@ module AcidicJob
54
54
  next_step = step["then"]
55
55
  # to support iteration within steps
56
56
  iterable_key = step["for_each"]
57
- iterated_key = "processed_#{iterable_key}"
58
- iterables = @run.attr_accessors.fetch(iterable_key, [])
59
- iterateds = @run.attr_accessors.fetch(iterated_key, [])
57
+ iterated_key = "processed_#{current_step}_#{iterable_key}"
58
+ iterables = @run.attr_accessors.fetch(iterable_key, []) || []
59
+ iterateds = @run.attr_accessors.fetch(iterated_key, []) || []
60
60
  next_item = iterables.reject { |item| iterateds.include? item }.first
61
61
 
62
62
  # jobs can have no-op steps, especially so that they can use only the async/await mechanism for that step
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AcidicJob
4
- VERSION = "1.0.0.pre17"
4
+ VERSION = "1.0.0.pre20"
5
5
  end
data/lib/acidic_job.rb CHANGED
@@ -47,12 +47,15 @@ module AcidicJob
47
47
  raise UnknownJobAdapter
48
48
  end
49
49
 
50
+ # TODO: write test for a staged job that uses awaits
50
51
  klass.set_callback :perform, :after, :delete_staged_job_record, if: :was_staged_job?
52
+ klass.define_callbacks :finish
51
53
 
52
54
  klass.instance_variable_set(:@acidic_identifier, :job_id)
53
55
  klass.define_singleton_method(:acidic_by_job_id) { @acidic_identifier = :job_id }
54
56
  klass.define_singleton_method(:acidic_by_job_args) { @acidic_identifier = :job_args }
55
57
  klass.define_singleton_method(:acidic_by) { |proc| @acidic_identifier = proc }
58
+ klass.attr_reader(:acidic_job_run)
56
59
  end
57
60
 
58
61
  included do
@@ -94,10 +97,10 @@ module AcidicJob
94
97
  # convert the array of steps into a hash of recovery_points and next steps
95
98
  workflow = define_workflow(@__acidic_job_steps)
96
99
 
97
- @run = ensure_run_record(workflow, providing)
100
+ @acidic_job_run = ensure_run_record(workflow, providing)
98
101
 
99
102
  # begin the workflow
100
- process_run(@run)
103
+ process_run(@acidic_job_run)
101
104
  end
102
105
 
103
106
  # DEPRECATED
@@ -128,7 +131,7 @@ module AcidicJob
128
131
 
129
132
  def process_run(run)
130
133
  # if the run record is already marked as finished, immediately return its result
131
- return run.succeeded? if run.finished?
134
+ return finish_run(run) if run.finished?
132
135
 
133
136
  # otherwise, we will enter a loop to process each step of the workflow
134
137
  loop do
@@ -167,7 +170,13 @@ module AcidicJob
167
170
  end
168
171
 
169
172
  # the loop will break once the job is finished, so simply report the status
170
- run.succeeded?
173
+ finish_run(run)
174
+ end
175
+
176
+ def finish_run(run)
177
+ run_callbacks :finish do
178
+ run.succeeded?
179
+ end
171
180
  end
172
181
 
173
182
  def step(method_name, awaits: [], for_each: nil)
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: 1.0.0.pre17
4
+ version: 1.0.0.pre20
5
5
  platform: ruby
6
6
  authors:
7
7
  - fractaledmind
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-05-14 00:00:00.000000000 Z
11
+ date: 2022-05-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord