acidic_job 1.0.0.pre19 → 1.0.0.pre22

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: cb1a8e3e2658bf86b44012171e33458c6e25803e41c255401575bc772cf033db
4
- data.tar.gz: a2672fb1755af7ada91b7e8b156cffe951f22d530cbf7d94aee794622f9ad5c8
3
+ metadata.gz: 892c6a2598d63440a7e749484e790b3f47380954b615bd7b2c4fc4c8de541759
4
+ data.tar.gz: bf7383000fa8e0fb1c545e5ccb014c7123884eb14da77178dd088bb9b12e73e1
5
5
  SHA512:
6
- metadata.gz: d6fc3314e1eb512ef9eab52cada5b7705891563715f1d549912b45b13b85bd0bf778a8950a5657037ddf75e7e81819cf8c85a240c191cb71d615e98ca22c7c3a
7
- data.tar.gz: 011f57cb97e93bd6e09c2c07d164d06679c8311b8e76a1b1cf9438376e65da5d4d76851cd7e1b6eb4d2f1ffba7064ff722c8cec75db08e78a1e96b7353672d13
6
+ metadata.gz: 87610f9f920ddb6e34c727a6b4c9e8d98d9ec556bff1dea221e3f588694b4e5f77b482985ba906cc62c5a3593e398e906e8dd6aa815e9c6d3031e410b71abfeb
7
+ data.tar.gz: 7ca46ec550598f545d3baaafccf09cf082b6eaad01a1d6bb4b8f5a7298d9023b0b33c824183e3c5b30b3a1fbb2a9200286761b4e671adf39569a62b6ac392740
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- acidic_job (1.0.0.pre19)
4
+ acidic_job (1.0.0.pre22)
5
5
  activerecord
6
6
  activesupport
7
7
  database_cleaner
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,86 @@ class RideCreateJob < ActiveJob::Base
240
269
  end
241
270
  ```
242
271
 
272
+ If you need to await a job that takes arguments, you can prepare that job along with its arguments using the `with` class method that `acidic_job` will add to your jobs:
273
+
274
+ ```ruby
275
+ class RideCreateJob < ActiveJob::Base
276
+ include AcidicJob
277
+
278
+ def perform(user_id, ride_params)
279
+ @user = User.find(user_id)
280
+ @params = ride_params
281
+
282
+ with_acidity providing: { ride: nil } do
283
+ step :create_ride_and_audit_record, awaits: awaits: [SomeJob.with('argument_1', keyword: 'value')]
284
+ step :create_stripe_charge, args: [1, 2, 3], kwargs: { some: 'thing' }
285
+ step :send_receipt
286
+ end
287
+ end
288
+ end
289
+ ```
290
+
291
+ You can also await a batch of jobs by simply passing multiple jobs to the `awaits` array (e.g. `awaits: [SomeJob, AnotherJob.with('argument_1', keyword: 'value')]`). Your top level workflow job will only continue to the next step once all of the jobs in your `awaits` array have successfully finished.
292
+
293
+ In some cases, you may need to _dynamically_ determine the collection of jobs that the step should wait for; in these cases, you can pass the name of a method to the `awaits` option:
294
+
295
+ ```ruby
296
+ class RideCreateJob < ActiveJob::Base
297
+ include AcidicJob
298
+ set_callback :finish, :after, :delete_run_record
299
+
300
+ def perform(user_id, ride_params)
301
+ @user = User.find(user_id)
302
+ @params = ride_params
303
+
304
+ with_acidity providing: { ride: nil } do
305
+ step :create_ride_and_audit_record, awaits: :dynamic_awaits
306
+ step :create_stripe_charge, args: [1, 2, 3], kwargs: { some: 'thing' }
307
+ step :send_receipt
308
+ end
309
+ end
310
+
311
+ def dynamic_awaits
312
+ if @params["key"].present?
313
+ [SomeJob.with('argument_1', keyword: 'value')]
314
+ else
315
+ [AnotherJob]
316
+ end
317
+ end
318
+ end
319
+ ```
320
+
321
+
322
+ ### Run Finished Callbacks
323
+
324
+ 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.
325
+
326
+ For example, you could use this hook to immediately clean up the `AcidicJob::Run` database record whenever the workflow job finishes successfully like so:
327
+
328
+ ```ruby
329
+ class RideCreateJob < ActiveJob::Base
330
+ include AcidicJob
331
+ set_callback :finish, :after, :delete_run_record
332
+
333
+ def perform(user_id, ride_params)
334
+ @user = User.find(user_id)
335
+ @params = ride_params
336
+
337
+ with_acidity providing: { ride: nil } do
338
+ step :create_ride_and_audit_record, awaits: [SomeJob.with('argument_1', keyword: 'value')]
339
+ step :create_stripe_charge, args: [1, 2, 3], kwargs: { some: 'thing' }
340
+ step :send_receipt
341
+ end
342
+ end
343
+
344
+ def delete_run_record
345
+ return unless acidic_job_run.succeeded?
346
+
347
+ acidic_job_run.destroy!
348
+ end
349
+ end
350
+ ```
351
+
243
352
  ## Testing
244
353
 
245
354
  When testing acidic jobs, you are likely to run into `ActiveRecord::TransactionIsolationError`s:
@@ -6,37 +6,41 @@ module AcidicJob
6
6
  module Awaiting
7
7
  extend ActiveSupport::Concern
8
8
 
9
- def enqueue_step_parallel_jobs(jobs, run, step_result)
9
+ private
10
+
11
+ def enqueue_step_parallel_jobs(jobs_or_jobs_getter, run, step_result)
10
12
  # `batch` is available from Sidekiq::Pro
11
13
  raise SidekiqBatchRequired unless defined?(Sidekiq::Batch)
12
14
 
15
+ jobs = case jobs_or_jobs_getter
16
+ when Array
17
+ jobs_or_jobs_getter
18
+ when Symbol, String
19
+ method(jobs_or_jobs_getter).call
20
+ end
21
+
13
22
  step_batch = Sidekiq::Batch.new
14
23
  # step_batch.description = "AcidicJob::Workflow Step: #{step}"
15
24
  step_batch.on(
16
25
  :success,
17
26
  "#{self.class.name}#step_done",
18
27
  # NOTE: options are marshalled through JSON so use only basic types.
19
- { "run_id" => run.id,
28
+ {
29
+ "run_id" => run.id,
20
30
  "step_result_yaml" => step_result.to_yaml.strip,
21
31
  "parent_worker" => self.class.name,
22
- "job_names" => jobs.map(&:to_s) }
32
+ "job_names" => jobs.map { |job| job_name(job) }
33
+ }
23
34
  )
24
35
 
25
36
  # NOTE: The jobs method is atomic.
26
37
  # All jobs created in the block are actually pushed atomically at the end of the block.
27
38
  # If an error is raised, none of the jobs will go to Redis.
28
39
  step_batch.jobs do
29
- jobs.each do |worker_name|
30
- # TODO: handle Symbols as well
31
- worker = worker_name.is_a?(String) ? worker_name.constantize : worker_name
32
-
33
- if worker.instance_method(:perform).arity.presence_in [0, -1]
34
- worker.perform_async
35
- elsif worker.instance_method(:perform).arity == 1
36
- worker.perform_async(run.id)
37
- else
38
- raise TooManyParametersForParallelJob
39
- end
40
+ jobs.each do |job|
41
+ worker, args, kwargs = job_args_and_kwargs(job)
42
+
43
+ worker.perform_async(*args, **kwargs)
40
44
  end
41
45
  end
42
46
  end
@@ -53,5 +57,33 @@ module AcidicJob
53
57
  # when a batch of jobs for a step succeeds, we begin processing the `AcidicJob::Run` record again
54
58
  process_run(run)
55
59
  end
60
+
61
+ def job_name(job)
62
+ case job
63
+ when Class, Symbol
64
+ job.to_s
65
+ when String
66
+ job
67
+ else
68
+ job.class.name
69
+ end
70
+ end
71
+
72
+ def job_args_and_kwargs(job)
73
+ case job
74
+ when Class
75
+ [job, [], {}]
76
+ when String
77
+ [job.constantize, [], {}]
78
+ when Symbol
79
+ [job.to_s.constantize, [], {}]
80
+ else
81
+ [
82
+ job.class,
83
+ job.instance_variable_get(:@__acidic_job_args),
84
+ job.instance_variable_get(:@__acidic_job_kwargs)
85
+ ]
86
+ end
87
+ end
56
88
  end
57
89
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AcidicJob
4
- VERSION = "1.0.0.pre19"
4
+ VERSION = "1.0.0.pre22"
5
5
  end
data/lib/acidic_job.rb CHANGED
@@ -55,6 +55,7 @@ module AcidicJob
55
55
  klass.define_singleton_method(:acidic_by_job_id) { @acidic_identifier = :job_id }
56
56
  klass.define_singleton_method(:acidic_by_job_args) { @acidic_identifier = :job_args }
57
57
  klass.define_singleton_method(:acidic_by) { |proc| @acidic_identifier = proc }
58
+ klass.attr_reader(:acidic_job_run)
58
59
  end
59
60
 
60
61
  included do
@@ -67,6 +68,10 @@ module AcidicJob
67
68
  super
68
69
  end
69
70
 
71
+ def with(*args, **kwargs)
72
+ new(*args, **kwargs)
73
+ end
74
+
70
75
  def acidic_identifier
71
76
  @acidic_identifier
72
77
  end
@@ -78,11 +83,11 @@ module AcidicJob
78
83
  @__acidic_job_args = args
79
84
  @__acidic_job_kwargs = kwargs
80
85
 
81
- if method(__method__).super_method.arity.zero?
82
- super()
83
- else
84
- super(*args, **kwargs)
85
- end
86
+ super(*args, **kwargs)
87
+ rescue ArgumentError => e
88
+ raise e unless e.message.include?("wrong number of arguments")
89
+
90
+ super()
86
91
  end
87
92
 
88
93
  def with_acidity(providing: {})
@@ -96,10 +101,10 @@ module AcidicJob
96
101
  # convert the array of steps into a hash of recovery_points and next steps
97
102
  workflow = define_workflow(@__acidic_job_steps)
98
103
 
99
- @run = ensure_run_record(workflow, providing)
104
+ @acidic_job_run = ensure_run_record(workflow, providing)
100
105
 
101
106
  # begin the workflow
102
- process_run(@run)
107
+ process_run(@acidic_job_run)
103
108
  end
104
109
 
105
110
  # DEPRECATED
@@ -143,7 +148,7 @@ module AcidicJob
143
148
  break
144
149
  elsif current_step.nil?
145
150
  raise UnknownRecoveryPoint, "Defined workflow does not reference this step: #{recovery_point}"
146
- elsif (jobs = current_step.fetch("awaits", [])).any?
151
+ elsif !(jobs = current_step.fetch("awaits", []) || []).empty?
147
152
  step = Step.new(current_step, run, self)
148
153
  # Only execute the current step, without yet progressing the recovery_point to the next step.
149
154
  # This ensures that any failures in parallel jobs will have this step retried in the main workflow
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.pre19
4
+ version: 1.0.0.pre22
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-16 00:00:00.000000000 Z
11
+ date: 2022-05-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord