acidic_job 1.0.0.pre24 → 1.0.0.pre27

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: 223aa8cad5d48e2a4fc04a9e33f82bdf7ef54cb933c2e14f3dc0aa6d46986932
4
- data.tar.gz: a8a646da8f9e5987267f1448e44056bdd37c2d4f8cbfbc84650659555567b635
3
+ metadata.gz: 2b41d1bacc6dc699e4e70e38c535795039f7dbfd5ed4b7ea3671809d7658fc5d
4
+ data.tar.gz: 53641ee78275b54defdae3ab1c4ca0087d6e193009c0d3df758114019a5a3efb
5
5
  SHA512:
6
- metadata.gz: 857647ff5cce856d41a8e30014005ef035cefcca383a719e6e679a4fc7bdc254c76f65e2d312c508ad56fd45b1e33ed533aecf2541e13b0480c579e0127ec338
7
- data.tar.gz: 4208fa26057e5a1fb0908c554b8e2ef74c6a5018a4d782815caa1fbd7716f18b33598ff6877e3c0c583f449099438d316c506dee6fa84afea34d54aa42f085b8
6
+ metadata.gz: d383cde62b69a013979df927ff64d1c99ff591261a103b3694733f65f0fa0d44c1216bf3b7e8638b746a59754efbc32afd410c9039bda9f88a89926e552ee1d9
7
+ data.tar.gz: ced115b0b7607c60b48a51d2fd4e2db6442fd0daa44b2a9b648a1ced7598ca3e0f05dd8894359ed163c27287146769d37aebe9e464c989e46ebabfa6ba07d41d
data/.gitignore CHANGED
@@ -12,3 +12,5 @@ slides.md
12
12
  /test/dummy
13
13
  /test/log
14
14
  /database.sqlite
15
+ /test/combustion/log/test.log
16
+ /test/combustion/database.sqlite
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- acidic_job (1.0.0.pre24)
4
+ acidic_job (1.0.0.pre27)
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.3)
13
- actionpack (= 7.0.3)
14
- activesupport (= 7.0.3)
12
+ actioncable (7.0.3.1)
13
+ actionpack (= 7.0.3.1)
14
+ activesupport (= 7.0.3.1)
15
15
  nio4r (~> 2.0)
16
16
  websocket-driver (>= 0.6.1)
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)
17
+ actionmailbox (7.0.3.1)
18
+ actionpack (= 7.0.3.1)
19
+ activejob (= 7.0.3.1)
20
+ activerecord (= 7.0.3.1)
21
+ activestorage (= 7.0.3.1)
22
+ activesupport (= 7.0.3.1)
23
23
  mail (>= 2.7.1)
24
24
  net-imap
25
25
  net-pop
26
26
  net-smtp
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)
27
+ actionmailer (7.0.3.1)
28
+ actionpack (= 7.0.3.1)
29
+ actionview (= 7.0.3.1)
30
+ activejob (= 7.0.3.1)
31
+ activesupport (= 7.0.3.1)
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.3)
38
- actionview (= 7.0.3)
39
- activesupport (= 7.0.3)
37
+ actionpack (7.0.3.1)
38
+ actionview (= 7.0.3.1)
39
+ activesupport (= 7.0.3.1)
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.3)
45
- actionpack (= 7.0.3)
46
- activerecord (= 7.0.3)
47
- activestorage (= 7.0.3)
48
- activesupport (= 7.0.3)
44
+ actiontext (7.0.3.1)
45
+ actionpack (= 7.0.3.1)
46
+ activerecord (= 7.0.3.1)
47
+ activestorage (= 7.0.3.1)
48
+ activesupport (= 7.0.3.1)
49
49
  globalid (>= 0.6.0)
50
50
  nokogiri (>= 1.8.5)
51
- actionview (7.0.3)
52
- activesupport (= 7.0.3)
51
+ actionview (7.0.3.1)
52
+ activesupport (= 7.0.3.1)
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.3)
58
- activesupport (= 7.0.3)
57
+ activejob (7.0.3.1)
58
+ activesupport (= 7.0.3.1)
59
59
  globalid (>= 0.3.6)
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)
60
+ activemodel (7.0.3.1)
61
+ activesupport (= 7.0.3.1)
62
+ activerecord (7.0.3.1)
63
+ activemodel (= 7.0.3.1)
64
+ activesupport (= 7.0.3.1)
65
+ activestorage (7.0.3.1)
66
+ actionpack (= 7.0.3.1)
67
+ activejob (= 7.0.3.1)
68
+ activerecord (= 7.0.3.1)
69
+ activesupport (= 7.0.3.1)
70
70
  marcel (~> 1.0)
71
71
  mini_mime (>= 1.1.0)
72
- activesupport (7.0.3)
72
+ activesupport (7.0.3.1)
73
73
  concurrent-ruby (~> 1.0, >= 1.0.2)
74
74
  i18n (>= 1.6, < 2)
75
75
  minitest (>= 5.1)
@@ -78,7 +78,7 @@ GEM
78
78
  public_suffix (>= 2.0.2, < 5.0)
79
79
  ast (2.4.2)
80
80
  builder (3.2.4)
81
- combustion (1.3.5)
81
+ combustion (1.3.7)
82
82
  activesupport (>= 3.0.0)
83
83
  railties (>= 3.0.0)
84
84
  thor (>= 0.14.6)
@@ -102,16 +102,17 @@ GEM
102
102
  rake
103
103
  globalid (1.0.0)
104
104
  activesupport (>= 5.0)
105
- http (5.0.4)
105
+ http (5.1.0)
106
106
  addressable (~> 2.8)
107
107
  http-cookie (~> 1.0)
108
108
  http-form_data (~> 2.2)
109
109
  llhttp-ffi (~> 0.4.0)
110
- http-cookie (1.0.4)
110
+ http-cookie (1.0.5)
111
111
  domain_name (~> 0.5)
112
112
  http-form_data (2.3.0)
113
- i18n (1.10.0)
113
+ i18n (1.12.0)
114
114
  concurrent-ruby (~> 1.0)
115
+ json (2.6.2)
115
116
  llhttp-ffi (0.4.0)
116
117
  ffi-compiler (~> 1.0)
117
118
  rake (~> 13.0)
@@ -124,7 +125,7 @@ GEM
124
125
  method_source (1.0.0)
125
126
  mini_mime (1.1.2)
126
127
  mini_portile2 (2.8.0)
127
- minitest (5.15.0)
128
+ minitest (5.16.2)
128
129
  net-imap (0.2.3)
129
130
  digest
130
131
  net-protocol
@@ -140,70 +141,71 @@ GEM
140
141
  net-protocol
141
142
  timeout
142
143
  nio4r (2.5.8)
143
- nokogiri (1.13.6)
144
+ nokogiri (1.13.7)
144
145
  mini_portile2 (~> 2.8.0)
145
146
  racc (~> 1.4)
146
- nokogiri (1.13.6-x86_64-darwin)
147
- racc (~> 1.4)
148
147
  noticed (1.5.9)
149
148
  http (>= 4.0.0)
150
149
  rails (>= 5.2.0)
151
150
  parallel (1.22.1)
152
151
  parser (3.1.2.0)
153
152
  ast (~> 2.4.1)
153
+ psych (4.0.4)
154
+ stringio
154
155
  public_suffix (4.0.7)
155
156
  racc (1.6.0)
156
- rack (2.2.3)
157
- rack-test (1.1.0)
158
- rack (>= 1.0, < 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)
157
+ rack (2.2.4)
158
+ rack-test (2.0.2)
159
+ rack (>= 1.3)
160
+ rails (7.0.3.1)
161
+ actioncable (= 7.0.3.1)
162
+ actionmailbox (= 7.0.3.1)
163
+ actionmailer (= 7.0.3.1)
164
+ actionpack (= 7.0.3.1)
165
+ actiontext (= 7.0.3.1)
166
+ actionview (= 7.0.3.1)
167
+ activejob (= 7.0.3.1)
168
+ activemodel (= 7.0.3.1)
169
+ activerecord (= 7.0.3.1)
170
+ activestorage (= 7.0.3.1)
171
+ activesupport (= 7.0.3.1)
171
172
  bundler (>= 1.15.0)
172
- railties (= 7.0.3)
173
+ railties (= 7.0.3.1)
173
174
  rails-dom-testing (2.0.3)
174
175
  activesupport (>= 4.2.0)
175
176
  nokogiri (>= 1.6)
176
- rails-html-sanitizer (1.4.2)
177
+ rails-html-sanitizer (1.4.3)
177
178
  loofah (~> 2.3)
178
- railties (7.0.3)
179
- actionpack (= 7.0.3)
180
- activesupport (= 7.0.3)
179
+ railties (7.0.3.1)
180
+ actionpack (= 7.0.3.1)
181
+ activesupport (= 7.0.3.1)
181
182
  method_source
182
183
  rake (>= 12.2)
183
184
  thor (~> 1.0)
184
185
  zeitwerk (~> 2.5)
185
186
  rainbow (3.1.1)
186
187
  rake (13.0.6)
187
- redis (4.6.0)
188
- regexp_parser (2.4.0)
188
+ redis (4.7.1)
189
+ regexp_parser (2.5.0)
189
190
  rexml (3.2.5)
190
- rubocop (1.29.1)
191
+ rubocop (1.31.2)
192
+ json (~> 2.3)
191
193
  parallel (~> 1.10)
192
194
  parser (>= 3.1.0.0)
193
195
  rainbow (>= 2.2.2, < 4.0)
194
196
  regexp_parser (>= 1.8, < 3.0)
195
197
  rexml (>= 3.2.5, < 4.0)
196
- rubocop-ast (>= 1.17.0, < 2.0)
198
+ rubocop-ast (>= 1.18.0, < 2.0)
197
199
  ruby-progressbar (~> 1.7)
198
200
  unicode-display_width (>= 1.4.0, < 3.0)
199
- rubocop-ast (1.18.0)
201
+ rubocop-ast (1.19.1)
200
202
  parser (>= 3.1.1.0)
201
- rubocop-minitest (0.19.1)
203
+ rubocop-minitest (0.20.1)
202
204
  rubocop (>= 0.90, < 2.0)
203
205
  rubocop-rake (0.6.0)
204
206
  rubocop (~> 1.0)
205
207
  ruby-progressbar (1.11.0)
206
- sidekiq (6.4.2)
208
+ sidekiq (6.5.1)
207
209
  connection_pool (>= 2.2.2)
208
210
  rack (~> 2.0)
209
211
  redis (>= 4.2.0)
@@ -213,21 +215,22 @@ GEM
213
215
  simplecov_json_formatter (~> 0.1)
214
216
  simplecov-html (0.12.3)
215
217
  simplecov_json_formatter (0.1.4)
216
- sqlite3 (1.4.2)
218
+ sqlite3 (1.4.4)
219
+ stringio (3.0.2)
217
220
  strscan (3.0.3)
218
221
  thor (1.2.1)
219
- timeout (0.2.0)
222
+ timeout (0.3.0)
220
223
  tzinfo (2.0.4)
221
224
  concurrent-ruby (~> 1.0)
222
225
  unf (0.1.4)
223
226
  unf_ext
224
- unf_ext (0.0.8.1)
225
- unicode-display_width (2.1.0)
226
- warning (1.2.1)
227
+ unf_ext (0.0.8.2)
228
+ unicode-display_width (2.2.0)
229
+ warning (1.3.0)
227
230
  websocket-driver (0.7.5)
228
231
  websocket-extensions (>= 0.1.0)
229
232
  websocket-extensions (0.1.5)
230
- zeitwerk (2.5.4)
233
+ zeitwerk (2.6.0)
231
234
 
232
235
  PLATFORMS
233
236
  ruby
@@ -240,6 +243,7 @@ DEPENDENCIES
240
243
  minitest
241
244
  net-smtp
242
245
  noticed
246
+ psych (> 4.0)
243
247
  railties
244
248
  rake
245
249
  rubocop
data/README.md CHANGED
@@ -64,14 +64,23 @@ It provides a suite of functionality that empowers you to create complex, robust
64
64
 
65
65
  #### Key Features
66
66
 
67
- * Transactional Steps — break your job into a series of steps, each of which will be run within an acidic database transaction, allowing retries to jump back to the last "recovery point".
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
- * Transactionally Staged Jobs — enqueue additional jobs within the acidic transaction safely
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
72
- * Sidekiq Callbacks — bring ActiveJob-like callbacks into your pure Sidekiq Workers
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
67
+ * **Transactional Steps**
68
+ break your job into a series of steps, each of which will be run within an acidic database transaction, allowing retries to jump back to the last "recovery point".
69
+ * **Steps that Await Jobs**
70
+ have workflow steps await other jobs, which will be enqueued and processed independently, and only when they all have finished will the parent job be re-enqueued to continue the workflow
71
+ * **Iterable Steps**
72
+ define steps that iterate over some collection fully until moving on to the next step
73
+ * **Persisted Attributes**
74
+ when retrying jobs at later steps, we need to ensure that data created in previous steps is still available to later steps on retry.
75
+ * **Transactionally Staged Jobs**
76
+ enqueue additional jobs within the acidic transaction safely
77
+ * **Custom Idempotency Keys**
78
+ use something other than the job ID for the idempotency key of the job run
79
+ * **Sidekiq Callbacks**
80
+ bring ActiveJob-like callbacks into your pure Sidekiq Workers
81
+ * **Run Finished Callbacks**
82
+ set callbacks for when a job run finishes fully
83
+
75
84
 
76
85
  ### Transactional Steps
77
86
 
@@ -110,15 +119,114 @@ end
110
119
 
111
120
  Now, each execution of this job will find or create an `AcidicJob::Run` record, which we leverage to wrap every step in a database transaction. Moreover, this database record allows `acidic_job` to ensure that if your job fails on step 3, when it retries, it will simply jump right back to trying to execute the method defined for the 3rd step, and won't even execute the first two step methods. This means your step methods only need to be idempotent on failure, not on success, since they will never be run again if they succeed.
112
121
 
122
+
123
+ ### Steps that Await Jobs
124
+
125
+ By simply adding the `awaits` option to your step declarations, you can attach any number of additional, asynchronous jobs to your step. This is profoundly powerful, as it means that you can define a workflow where step 2 is started _if and only if_ step 1 succeeds, but step 1 can have 3 different jobs enqueued on 3 different queues, each running in parallel. Once all 3 jobs succeed, `acidic_job` will re-enqueue the parent job and it will move on to step 2. That's right, you can have workers that are _executed in parallel_, **on separate queues**, and _asynchronously_, but are still **blocking**—as a group—the next step in your workflow! This unlocks incredible power and flexibility for defining and structuring complex workflows and operations.
126
+
127
+ ```ruby
128
+ class RideCreateJob < ActiveJob::Base
129
+ include AcidicJob
130
+
131
+ def perform(user_id, ride_params)
132
+ @user = User.find(user_id)
133
+ @params = ride_params
134
+
135
+ with_acidity providing: { ride: nil } do
136
+ step :create_ride_and_audit_record, awaits: [SomeJob, AnotherJob]
137
+ step :create_stripe_charge
138
+ step :send_receipt
139
+ end
140
+ end
141
+ end
142
+ ```
143
+
144
+ 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:
145
+
146
+ ```ruby
147
+ class RideCreateJob < ActiveJob::Base
148
+ include AcidicJob
149
+
150
+ def perform(user_id, ride_params)
151
+ @user = User.find(user_id)
152
+ @params = ride_params
153
+
154
+ with_acidity providing: { ride: nil } do
155
+ step :create_ride_and_audit_record, awaits: awaits: [SomeJob.with('argument_1', keyword: 'value'), AnotherJob.with(1, 2, 3, some: 'thing')]
156
+ step :create_stripe_charge
157
+ step :send_receipt
158
+ end
159
+ end
160
+ end
161
+ ```
162
+
163
+ If your step awaits multiple jobs (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 finished.
164
+
165
+ 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:
166
+
167
+ ```ruby
168
+ class RideCreateJob < ActiveJob::Base
169
+ include AcidicJob
170
+ set_callback :finish, :after, :delete_run_record
171
+
172
+ def perform(user_id, ride_params)
173
+ @user = User.find(user_id)
174
+ @params = ride_params
175
+
176
+ with_acidity providing: { ride: nil } do
177
+ step :create_ride_and_audit_record, awaits: :dynamic_awaits
178
+ step :create_stripe_charge
179
+ step :send_receipt
180
+ end
181
+ end
182
+
183
+ def dynamic_awaits
184
+ if @params["key"].present?
185
+ [SomeJob.with('argument_1', keyword: 'value')]
186
+ else
187
+ [AnotherJob.with(1, 2, 3, some: 'thing')]
188
+ end
189
+ end
190
+ end
191
+ ```
192
+
193
+
194
+ ### Iterable Steps
195
+
196
+ 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.
197
+
198
+ ```ruby
199
+ class ExampleJob < ActiveJob::Base
200
+ include AcidicJob
201
+
202
+ def perform(record:)
203
+ with_acidity providing: { collection: [1, 2, 3, 4, 5] } do
204
+ step :process_item, for_each: :collection
205
+ step :next_step
206
+ end
207
+ end
208
+
209
+ def process_item(item)
210
+ # do whatever work needs to be done with this individual item
211
+ end
212
+ end
213
+ ```
214
+
215
+ **Note:** This feature relies on the "Persisted Attributes" feature detailed below. This means that you can only iterate over collections that ActiveRecord can serialize.
216
+
217
+
113
218
  ### Persisted Attributes
114
219
 
115
- Any objects passed to the `providing` option on the `with_acidity` method are not just made available to each of your step methods, they are made available across retries. This means that you can set an attribute in step 1, access it in step 2, have step 2 fail, have the job retry, jump directly back to step 2 on retry, and have that object still accessible. This is done by serializing all objects to a field on the `AcidicJob::Run` and manually providing getters and setters that sync with the database record.
220
+ Any objects passed to the `providing` option on the `with_acidity` method are made available across retries. This means that you can set an attribute in step 1, access it in step 2, have step 2 fail, have the job retry, jump directly back to step 2 on retry, and have that object still accessible. This is done by serializing all objects to a field on the `AcidicJob::Run` and manually providing getters and setters that sync with the database record.
116
221
 
117
222
  ```ruby
118
223
  class RideCreateJob < ActiveJob::Base
119
224
  include AcidicJob
120
225
 
121
- def perform(ride_params)
226
+ def perform(user_id, ride_params)
227
+ @user = User.find(user_id)
228
+ @params = ride_params
229
+
122
230
  with_acidity providing: { ride: nil } do
123
231
  step :create_ride_and_audit_record
124
232
  step :create_stripe_charge
@@ -144,6 +252,7 @@ end
144
252
 
145
253
  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
254
 
255
+
147
256
  ### Transactionally Staged Jobs
148
257
 
149
258
  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.
@@ -173,6 +282,7 @@ class RideCreateJob < ActiveJob::Base
173
282
  end
174
283
  ```
175
284
 
285
+
176
286
  ### Custom Idempotency Keys
177
287
 
178
288
  By default, `AcidicJob` uses the job identifier provided by the queueing system (ActiveJob or Sidekiq) as the idempotency key for the job run. The idempotency key is what is used to guarantee that no two runs of the same job occur. However, sometimes we need particular jobs to be idempotent based on some other criteria. In these cases, `AcidicJob` provides a collection of tools to allow you to ensure the idempotency of your jobs.
@@ -217,28 +327,6 @@ end
217
327
 
218
328
  > **Note:** The signature of the `acidic_by` proc _needs to match the signature_ of the job's `perform` method.
219
329
 
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.
242
330
 
243
331
  ### Sidekiq Callbacks
244
332
 
@@ -246,78 +334,6 @@ In order to ensure that `AcidicJob::Staged` records are only destroyed once the
246
334
 
247
335
  This allows `acidic_job` to use an `after_perform` callback to delete the `AcidicJob::Staged` record, whether you are using the gem with ActiveJob or pure Sidekiq Workers. Of course, this means that you can add your own callbacks to any jobs or workers that include the `AcidicJob` module as well.
248
336
 
249
- ### Sidekiq Batches
250
-
251
- One final feature for those of you using Sidekiq Pro: an integrated DSL for Sidekiq Batches. By simply adding the `awaits` option to your step declarations, you can attach any number of additional, asynchronous workers to your step. This is profoundly powerful, as it means that you can define a workflow where step 2 is started _if and only if_ step 1 succeeds, but step 1 can have 3 different workers enqueued on 3 different queues, each running in parallel. Once all 3 workers succeed, `acidic_job` will move on to step 2. That's right, by leveraging the power of Sidekiq Batches, you can have workers that are _executed in parallel_, **on separate queues**, and _asynchronously_, but are still **blocking**—as a group—the next step in your workflow! This unlocks incredible power and flexibility for defining and structuring complex workflows and operations, and in my mind is the number one selling point for Sidekiq Pro.
252
-
253
- In my opinion, any commercial software using Sidekiq should get Sidekiq Pro; it is _absolutely_ worth the money. If, however, you are using `acidic_job` in a non-commercial application, you could use the open-source dropin replacement for this functionality: https://github.com/breamware/sidekiq-batch
254
-
255
- ```ruby
256
- class RideCreateJob < ActiveJob::Base
257
- include AcidicJob
258
-
259
- def perform(user_id, ride_params)
260
- @user = User.find(user_id)
261
- @params = ride_params
262
-
263
- with_acidity providing: { ride: nil } do
264
- step :create_ride_and_audit_record, awaits: [SomeJob]
265
- step :create_stripe_charge, args: [1, 2, 3], kwargs: { some: 'thing' }
266
- step :send_receipt
267
- end
268
- end
269
- end
270
- ```
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
337
 
322
338
  ### Run Finished Callbacks
323
339
 
@@ -349,6 +365,7 @@ class RideCreateJob < ActiveJob::Base
349
365
  end
350
366
  ```
351
367
 
368
+
352
369
  ## Testing
353
370
 
354
371
  When testing acidic jobs, you are likely to run into `ActiveRecord::TransactionIsolationError`s:
data/acidic_job.gemspec CHANGED
@@ -35,6 +35,7 @@ Gem::Specification.new do |spec|
35
35
  spec.add_development_dependency "minitest"
36
36
  spec.add_development_dependency "net-smtp"
37
37
  spec.add_development_dependency "noticed"
38
+ spec.add_development_dependency "psych", "> 4.0"
38
39
  spec.add_development_dependency "railties"
39
40
  spec.add_development_dependency "rake"
40
41
  spec.add_development_dependency "rubocop"
File without changes
@@ -17,6 +17,7 @@ module AcidicJob
17
17
  run = staged_job_run&.awaited_by || acidic_job_run&.awaited_by
18
18
 
19
19
  return unless run
20
+ return if run.batched_runs.outstanding.any?
20
21
 
21
22
  current_step = run.workflow[run.recovery_point.to_s]
22
23
  step_result = run.returning_to
@@ -47,7 +48,7 @@ module AcidicJob
47
48
  end
48
49
 
49
50
  AcidicJob::Run.transaction do
50
- awaited_jobs.each do |awaited_job|
51
+ awaited_jobs.compact.each do |awaited_job|
51
52
  worker_class, args, kwargs = job_args_and_kwargs(awaited_job)
52
53
 
53
54
  job = worker_class.new(*args, **kwargs)
@@ -47,7 +47,7 @@ module AcidicJob
47
47
  arguments = args || @args
48
48
  arguments += [kwargs] unless kwargs.empty?
49
49
  normalized_args = ::Sidekiq.load_json(::Sidekiq.dump_json(arguments))
50
- item = { "class" => self.class, "args" => normalized_args, "jid" => jid }
50
+ item = { "class" => self.class.name, "args" => normalized_args, "jid" => jid }
51
51
  sidekiq_options = sidekiq_options_hash || {}
52
52
 
53
53
  sidekiq_options.merge(item)
@@ -3,6 +3,7 @@
3
3
  require "active_record"
4
4
  require "global_id"
5
5
  require "active_support/core_ext/object/with_options"
6
+ require_relative "./serializer"
6
7
 
7
8
  module AcidicJob
8
9
  class Run < ActiveRecord::Base
@@ -17,11 +18,11 @@ module AcidicJob
17
18
 
18
19
  after_create_commit :enqueue_staged_job, if: :staged?
19
20
 
20
- serialize :error_object
21
- serialize :serialized_job
22
- serialize :workflow
23
- serialize :returning_to
24
- store :attr_accessors
21
+ serialize :serialized_job, JSON
22
+ serialize :error_object, Serializer
23
+ serialize :workflow, Serializer
24
+ serialize :returning_to, Serializer
25
+ store :attr_accessors, coder: Serializer
25
26
 
26
27
  validates :staged, inclusion: { in: [true, false] } # uses database default
27
28
  validates :serialized_job, presence: true
@@ -31,7 +32,9 @@ module AcidicJob
31
32
  scope :staged, -> { where(staged: true) }
32
33
  scope :unstaged, -> { where(staged: false) }
33
34
  scope :finished, -> { where(recovery_point: FINISHED_RECOVERY_POINT) }
34
- scope :running, -> { where.not(recovery_point: FINISHED_RECOVERY_POINT) }
35
+ scope :outstanding, lambda {
36
+ where.not(recovery_point: FINISHED_RECOVERY_POINT).or(where(recovery_point: [nil, ""]))
37
+ }
35
38
 
36
39
  with_options unless: :staged? do
37
40
  validates :last_run_at, presence: true
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job/serializers"
4
+ require "active_job/arguments"
5
+ require "json"
6
+
7
+ class WorkerSerializer < ActiveJob::Serializers::ObjectSerializer
8
+ def serialize(worker)
9
+ # {"_aj_serialized"=>"WorkerSerializer", "class"=>"SuccessfulArgWorker", "args"=>[123], "kwargs"=>{}}]
10
+ super(
11
+ "class" => worker.class.name,
12
+ "args" => worker.instance_variable_get(:@__acidic_job_args),
13
+ "kwargs" => worker.instance_variable_get(:@__acidic_job_kwargs)
14
+ )
15
+ end
16
+
17
+ def deserialize(hash)
18
+ worker_class = hash["class"].constantize
19
+ worker_class.new(*hash["args"], **hash["kwargs"])
20
+ end
21
+
22
+ def serialize?(argument)
23
+ defined?(Sidekiq) && argument.class.include?(Sidekiq::Worker)
24
+ end
25
+ end
26
+
27
+ class ExceptionSerializer < ActiveJob::Serializers::ObjectSerializer
28
+ def serialize(exception)
29
+ hash = {
30
+ "class" => exception.class.name,
31
+ "message" => exception.message,
32
+ "cause" => exception.cause,
33
+ "backtrace" => {}
34
+ }
35
+
36
+ exception.backtrace.map do |trace|
37
+ path, _, location = trace.rpartition("/")
38
+
39
+ next if hash["backtrace"].key?(path)
40
+
41
+ hash["backtrace"][path] = location
42
+ end
43
+
44
+ super(hash)
45
+ end
46
+
47
+ def deserialize(hash)
48
+ exception_class = hash["class"].constantize
49
+ exception = exception_class.new(hash["message"])
50
+ exception.set_backtrace(hash["backtrace"].map do |path, location|
51
+ [path, location].join("/")
52
+ end)
53
+ exception
54
+ end
55
+
56
+ def serialize?(argument)
57
+ defined?(Exception) && argument.is_a?(Exception)
58
+ end
59
+ end
60
+
61
+ class FinishedPointSerializer < ActiveJob::Serializers::ObjectSerializer
62
+ def serialize(finished_point)
63
+ super(
64
+ "class" => finished_point.class.name
65
+ )
66
+ end
67
+
68
+ def deserialize(hash)
69
+ finished_point_class = hash["class"].constantize
70
+ finished_point_class.new
71
+ end
72
+
73
+ def serialize?(argument)
74
+ defined?(::AcidicJob::FinishedPoint) && argument.is_a?(::AcidicJob::FinishedPoint)
75
+ end
76
+ end
77
+
78
+ class RecoveryPointSerializer < ActiveJob::Serializers::ObjectSerializer
79
+ def serialize(recovery_point)
80
+ super(
81
+ "class" => recovery_point.class.name,
82
+ "name" => recovery_point.name
83
+ )
84
+ end
85
+
86
+ def deserialize(hash)
87
+ recovery_point_class = hash["class"].constantize
88
+ recovery_point_class.new(hash["name"])
89
+ end
90
+
91
+ def serialize?(argument)
92
+ defined?(::AcidicJob::RecoveryPoint) && argument.is_a?(::AcidicJob::RecoveryPoint)
93
+ end
94
+ end
95
+
96
+ ActiveJob::Serializers.add_serializers WorkerSerializer, ExceptionSerializer, FinishedPointSerializer,
97
+ RecoveryPointSerializer
98
+
99
+ # ...
100
+ module AcidicJob
101
+ module Arguments
102
+ include ActiveJob::Arguments
103
+ extend self # rubocop:disable Style/ModuleFunction
104
+
105
+ # `ActiveJob` will throw an error if it tries to deserialize a GlobalID record.
106
+ # However, this isn't the behavior that we want for our custom `ActiveRecord` serializer.
107
+ # Since `ActiveRecord` does _not_ reset instance record state to its pre-transactional state
108
+ # on a transaction ROLLBACK, we can have GlobalID entries in a serialized column that point to
109
+ # non-persisted records. This is ok. We should simply return `nil` for that portion of the
110
+ # serialized field.
111
+ def deserialize_global_id(hash)
112
+ GlobalID::Locator.locate hash[GLOBALID_KEY]
113
+ rescue ActiveRecord::RecordNotFound
114
+ nil
115
+ end
116
+ end
117
+
118
+ class Serializer
119
+ # Used for `serialize` method in ActiveRecord
120
+ class << self
121
+ def load(json)
122
+ return if json.nil? || json.empty?
123
+
124
+ data = JSON.parse(json)
125
+ Arguments.deserialize(data).first
126
+ end
127
+
128
+ def dump(obj)
129
+ data = Arguments.serialize [obj]
130
+ data.to_json
131
+ end
132
+ end
133
+ end
134
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AcidicJob
4
- VERSION = "1.0.0.pre24"
4
+ VERSION = "1.0.0.pre27"
5
5
  end
data/lib/acidic_job.rb CHANGED
@@ -50,6 +50,7 @@ module AcidicJob
50
50
  # TODO: write test for a staged job that uses awaits
51
51
  klass.set_callback :perform, :after, :reenqueue_awaited_by_job,
52
52
  if: -> { was_awaited_job? && !was_workflow_job? }
53
+ klass.set_callback :perform, :after, :finish_staged_job, if: -> { was_staged_job? && !was_workflow_job? }
53
54
  klass.define_callbacks :finish
54
55
  klass.set_callback :finish, :after, :reenqueue_awaited_by_job,
55
56
  if: -> { was_workflow_job? && was_awaited_job? }
@@ -136,6 +137,10 @@ module AcidicJob
136
137
 
137
138
  private
138
139
 
140
+ def finish_staged_job
141
+ FinishedPoint.new.call(run: staged_job_run)
142
+ end
143
+
139
144
  def was_workflow_job?
140
145
  defined?(@acidic_job_run) && @acidic_job_run.present?
141
146
  end
@@ -155,7 +160,7 @@ module AcidicJob
155
160
  break
156
161
  elsif current_step.nil?
157
162
  raise UnknownRecoveryPoint, "Defined workflow does not reference this step: #{recovery_point}"
158
- elsif !(jobs = current_step.fetch("awaits", []) || []).empty?
163
+ elsif !Array(jobs = current_step.fetch("awaits", []) || []).compact.empty?
159
164
  step = Step.new(current_step, run, self)
160
165
  # Only execute the current step, without yet progressing the recovery_point to the next step.
161
166
  # 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.pre24
4
+ version: 1.0.0.pre27
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-26 00:00:00.000000000 Z
11
+ date: 2022-07-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: psych
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">"
130
+ - !ruby/object:Gem::Version
131
+ version: '4.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">"
137
+ - !ruby/object:Gem::Version
138
+ version: '4.0'
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: railties
127
141
  requirement: !ruby/object:Gem::Requirement
@@ -270,6 +284,7 @@ files:
270
284
  - bin/console
271
285
  - bin/setup
272
286
  - blog_post.md
287
+ - combustion/log/test.log
273
288
  - gemfiles/rails_6.1.gemfile
274
289
  - gemfiles/rails_7.0.gemfile
275
290
  - lib/acidic_job.rb
@@ -285,6 +300,7 @@ files:
285
300
  - lib/acidic_job/recovery_point.rb
286
301
  - lib/acidic_job/rspec_configuration.rb
287
302
  - lib/acidic_job/run.rb
303
+ - lib/acidic_job/serializer.rb
288
304
  - lib/acidic_job/staging.rb
289
305
  - lib/acidic_job/step.rb
290
306
  - lib/acidic_job/test_case.rb