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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +116 -7
- data/lib/acidic_job/awaiting.rb +46 -14
- data/lib/acidic_job/version.rb +1 -1
- data/lib/acidic_job.rb +13 -8
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 892c6a2598d63440a7e749484e790b3f47380954b615bd7b2c4fc4c8de541759
|
4
|
+
data.tar.gz: bf7383000fa8e0fb1c545e5ccb014c7123884eb14da77178dd088bb9b12e73e1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 87610f9f920ddb6e34c727a6b4c9e8d98d9ec556bff1dea221e3f588694b4e5f77b482985ba906cc62c5a3593e398e906e8dd6aa815e9c6d3031e410b71abfeb
|
7
|
+
data.tar.gz: 7ca46ec550598f545d3baaafccf09cf082b6eaad01a1d6bb4b8f5a7298d9023b0b33c824183e3c5b30b3a1fbb2a9200286761b4e671adf39569a62b6ac392740
|
data/Gemfile.lock
CHANGED
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: {
|
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:
|
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: {
|
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: {
|
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:
|
data/lib/acidic_job/awaiting.rb
CHANGED
@@ -6,37 +6,41 @@ module AcidicJob
|
|
6
6
|
module Awaiting
|
7
7
|
extend ActiveSupport::Concern
|
8
8
|
|
9
|
-
|
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
|
-
{
|
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(
|
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 |
|
30
|
-
|
31
|
-
|
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
|
data/lib/acidic_job/version.rb
CHANGED
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
-
@
|
104
|
+
@acidic_job_run = ensure_run_record(workflow, providing)
|
100
105
|
|
101
106
|
# begin the workflow
|
102
|
-
process_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", [])).
|
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.
|
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-
|
11
|
+
date: 2022-05-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|