ruby_reactor 0.8.3 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/.specify/feature.json +1 -1
- data/CHANGELOG.md +12 -0
- data/CLAUDE.md +1 -1
- data/README.md +93 -91
- data/lib/ruby_reactor/dsl/async_reactor_builder.rb +3 -6
- data/lib/ruby_reactor/dsl/compose_builder.rb +3 -10
- data/lib/ruby_reactor/dsl/reactor.rb +8 -11
- data/lib/ruby_reactor/dsl/retryable.rb +45 -0
- data/lib/ruby_reactor/dsl/step_builder.rb +40 -14
- data/lib/ruby_reactor/error/undeclared_input_error.rb +13 -0
- data/lib/ruby_reactor/executor/compensation_manager.rb +2 -2
- data/lib/ruby_reactor/executor/result_handler.rb +3 -2
- data/lib/ruby_reactor/executor/retry_manager.rb +11 -12
- data/lib/ruby_reactor/rspec/test_subject.rb +3 -5
- data/lib/ruby_reactor/step/async_reactor_step.rb +6 -2
- data/lib/ruby_reactor/step/compose_step.rb +8 -4
- data/lib/ruby_reactor/step/input_contract.rb +5 -0
- data/lib/ruby_reactor/step/inputs.rb +57 -0
- data/lib/ruby_reactor/step/map_step.rb +32 -22
- data/lib/ruby_reactor/step.rb +13 -7
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor.rb +3 -1
- data/specs/006-step-retry-declarations/checklists/requirements.md +41 -0
- data/specs/006-step-retry-declarations/contracts/dsl-surface.md +89 -0
- data/specs/006-step-retry-declarations/data-model.md +58 -0
- data/specs/006-step-retry-declarations/plan.md +187 -0
- data/specs/006-step-retry-declarations/quickstart.md +80 -0
- data/specs/006-step-retry-declarations/research.md +194 -0
- data/specs/006-step-retry-declarations/spec.md +453 -0
- data/specs/006-step-retry-declarations/tasks.md +382 -0
- data/specs/specs-inputs-by-method-md-piped-wigderson.md +77 -0
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 325464f0f2b9ee374b1267a4c7243121c9b564814d425bfa5ecce59891979468
|
|
4
|
+
data.tar.gz: 996363b210d54be36ac7065532aac6c0349334b18d8d14816896debd4e8993c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3fcbdbe30cce51beec8dd715c543c8db15140de630d94ec3ae01a7ba803d28cb88f44e0ce1ee97c232cbed34863d81b8c4d0ed979f44acf52a27cfbc07f31b04
|
|
7
|
+
data.tar.gz: 946f4082ec39691d47e35983b9466a128246159f0b191aca061d6c984e2340cabd09da8e111e92b4799ec97a8f4e9753826163e97df48424b3a1583abe40afbc
|
data/.specify/feature.json
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -204,6 +204,18 @@
|
|
|
204
204
|
* Docs: a park keeps each level's lock without a second `:lock_acquired` only while the gap stays
|
|
205
205
|
within the lock's `ttl`; a lapsed lock is acquired again.
|
|
206
206
|
|
|
207
|
+
## [0.8.4](https://github.com/arturictus/ruby_reactor/compare/v0.8.3...v0.8.4) (2026-09-26)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
### ⚠ BREAKING CHANGES
|
|
211
|
+
|
|
212
|
+
* `retry_defaults` on a reactor raises RubyReactor::Error::DeprecatedDslError. Migration: move the values onto each step that should retry (`retries max_attempts: 3, backoff: :exponential, base_delay: 2` inside the step block, or on the step class once supported). A step without `retries` runs once. `max_attempts: 0` is not valid: use `max_attempts: 1` (or omit `retries`) for a step that must never retry.
|
|
213
|
+
|
|
214
|
+
### Features
|
|
215
|
+
|
|
216
|
+
* implement step-scoped retry declarations ([#61](https://github.com/arturictus/ruby_reactor/issues/61)) ([75d9c4e](https://github.com/arturictus/ruby_reactor/commit/75d9c4edca1a6ec53183d56c9c6261e792c98cc2))
|
|
217
|
+
* Inputs protection ([#63](https://github.com/arturictus/ruby_reactor/issues/63)) ([faf90e8](https://github.com/arturictus/ruby_reactor/commit/faf90e8dbfff87f6492af863b90e8247c84bd673))
|
|
218
|
+
|
|
207
219
|
## [0.8.3](https://github.com/arturictus/ruby_reactor/compare/v0.8.2...v0.8.3) (2026-09-25)
|
|
208
220
|
|
|
209
221
|
|
data/CLAUDE.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!-- SPECKIT START -->
|
|
2
2
|
For additional context about technologies to be used, project structure,
|
|
3
3
|
shell commands, and other important information, read the current plan
|
|
4
|
-
at specs/
|
|
4
|
+
at specs/006-step-retry-declarations/plan.md
|
|
5
5
|
<!-- SPECKIT END -->
|
data/README.md
CHANGED
|
@@ -21,7 +21,7 @@ The key value is **Reliability**: if any part of your workflow fails, Ruby React
|
|
|
21
21
|
- **Background Execution**: Hand a whole reactor, or everything after a declared step, to a background job via Sidekiq or ActiveJob (so any ActiveJob-compatible queue — Resque, Solid Queue, GoodJob, etc. — works too).
|
|
22
22
|
- **Async Steps & Reactors**: `async_step` and `async_reactor` dispatch independent units of work while the reactor keeps running; steps that read their result wait for it.
|
|
23
23
|
- **Map & Parallel Execution**: Iterate over collections in parallel with the `map` step, distributing work across multiple workers.
|
|
24
|
-
- **Retries**:
|
|
24
|
+
- **Retries**: per-step retry policies (declared on the step class or step block) with exponential, linear, or fixed backoff.
|
|
25
25
|
- **Compensation**: Automatic rollback of completed steps when a failure occurs.
|
|
26
26
|
- **Interrupts**: Pause and resume workflows to wait for external events (webhooks, user approvals).
|
|
27
27
|
- **Input Validation**: Integrated with `dry-validation` for robust input checking.
|
|
@@ -247,14 +247,16 @@ One-line helpers end a step immediately from any call depth: `success!(value)`,
|
|
|
247
247
|
class ReserveInventoryStep < RubyReactor::Step
|
|
248
248
|
# The step's input contract: enforced before `run` on every execution path.
|
|
249
249
|
input :order, :hash
|
|
250
|
+
# The step's retry policy: applies in every reactor that uses this step.
|
|
251
|
+
retries max_attempts: 3
|
|
250
252
|
|
|
251
253
|
def run
|
|
252
|
-
reservation_id = InventoryService.reserve(inputs
|
|
254
|
+
reservation_id = InventoryService.reserve(inputs.order[:items])
|
|
253
255
|
Success(reservation_id: reservation_id)
|
|
254
256
|
end
|
|
255
257
|
|
|
256
258
|
def compensate
|
|
257
|
-
InventoryService.release_partial(inputs
|
|
259
|
+
InventoryService.release_partial(inputs.order[:items])
|
|
258
260
|
Success()
|
|
259
261
|
end
|
|
260
262
|
|
|
@@ -327,7 +329,7 @@ require 'ruby_reactor'
|
|
|
327
329
|
|
|
328
330
|
class ValidateEmailStep < RubyReactor::Step
|
|
329
331
|
def run
|
|
330
|
-
email = inputs
|
|
332
|
+
email = inputs.email
|
|
331
333
|
email&.include?('@') ? Success(email.strip) : Failure("Email must contain @")
|
|
332
334
|
end
|
|
333
335
|
end
|
|
@@ -336,14 +338,14 @@ class CreateUserStep < RubyReactor::Step
|
|
|
336
338
|
def run
|
|
337
339
|
Success(
|
|
338
340
|
id: rand(10000),
|
|
339
|
-
email: inputs
|
|
340
|
-
password_hash: inputs
|
|
341
|
+
email: inputs.email,
|
|
342
|
+
password_hash: inputs.password_hash,
|
|
341
343
|
created_at: Time.now
|
|
342
344
|
)
|
|
343
345
|
end
|
|
344
346
|
|
|
345
347
|
def compensate
|
|
346
|
-
Notify.to(inputs
|
|
348
|
+
Notify.to(inputs.email)
|
|
347
349
|
Success()
|
|
348
350
|
end
|
|
349
351
|
end
|
|
@@ -358,9 +360,9 @@ class UserRegistrationReactor < RubyReactor::Reactor
|
|
|
358
360
|
|
|
359
361
|
step :hash_password do
|
|
360
362
|
argument :password, input(:password)
|
|
361
|
-
run do |
|
|
363
|
+
run do |inputs, _context|
|
|
362
364
|
require 'digest'
|
|
363
|
-
Success(Digest::SHA256.hexdigest(
|
|
365
|
+
Success(Digest::SHA256.hexdigest(inputs.password))
|
|
364
366
|
end
|
|
365
367
|
end
|
|
366
368
|
|
|
@@ -373,13 +375,13 @@ class UserRegistrationReactor < RubyReactor::Reactor
|
|
|
373
375
|
argument :email, result(:validate_email)
|
|
374
376
|
wait_for :create_user
|
|
375
377
|
|
|
376
|
-
run do |
|
|
377
|
-
Email.send!(
|
|
378
|
+
run do |inputs, _context|
|
|
379
|
+
Email.send!(inputs.email, "verify your email")
|
|
378
380
|
Success()
|
|
379
381
|
end
|
|
380
382
|
|
|
381
|
-
compensate do |_error,
|
|
382
|
-
Email.send("support@acme.com", "Email verification for #{
|
|
383
|
+
compensate do |_error, inputs, _context|
|
|
384
|
+
Email.send("support@acme.com", "Email verification for #{inputs.email} couldn't be sent")
|
|
383
385
|
Success()
|
|
384
386
|
end
|
|
385
387
|
end
|
|
@@ -443,12 +445,12 @@ class CreateUserReactor < RubyReactor::Reactor
|
|
|
443
445
|
input :params
|
|
444
446
|
|
|
445
447
|
step :validate_inputs do
|
|
446
|
-
run { |
|
|
448
|
+
run { |inputs| validate(inputs.params) }
|
|
447
449
|
end
|
|
448
450
|
|
|
449
451
|
step :create_user do
|
|
450
452
|
argument :params, result(:validate_inputs)
|
|
451
|
-
run { |
|
|
453
|
+
run { |inputs| User.create(inputs.params) }
|
|
452
454
|
end
|
|
453
455
|
|
|
454
456
|
# :create_user is the LAST step to run in the calling process.
|
|
@@ -457,13 +459,13 @@ class CreateUserReactor < RubyReactor::Reactor
|
|
|
457
459
|
|
|
458
460
|
step :open_account do
|
|
459
461
|
argument :user, result(:create_user)
|
|
460
|
-
run { |
|
|
462
|
+
run { |inputs| Bank.open_account(inputs.user) }
|
|
461
463
|
end
|
|
462
464
|
|
|
463
465
|
step :report_new_user do
|
|
464
466
|
argument :user, result(:create_user)
|
|
465
467
|
wait_for :open_account
|
|
466
|
-
run { |
|
|
468
|
+
run { |inputs| Analytics.track(inputs.user) }
|
|
467
469
|
end
|
|
468
470
|
end
|
|
469
471
|
|
|
@@ -514,19 +516,19 @@ class SignupReactor < RubyReactor::Reactor
|
|
|
514
516
|
|
|
515
517
|
async_step :send_email do
|
|
516
518
|
argument :to, input(:email)
|
|
517
|
-
run { |
|
|
519
|
+
run { |inputs| Mailer.welcome(inputs.to).deliver_now; Success(:sent) }
|
|
518
520
|
end
|
|
519
521
|
|
|
520
522
|
# Does NOT wait — it has no dependency on :send_email.
|
|
521
523
|
step :record_signup do
|
|
522
524
|
argument :email, input(:email)
|
|
523
|
-
run { |
|
|
525
|
+
run { |inputs| Success(Signup.create!(email: inputs.email)) }
|
|
524
526
|
end
|
|
525
527
|
|
|
526
528
|
# DOES wait, because it reads the result.
|
|
527
529
|
step :confirm_delivery do
|
|
528
530
|
argument :delivery, result(:send_email)
|
|
529
|
-
run { |
|
|
531
|
+
run { |inputs| Success("confirmed") }
|
|
530
532
|
end
|
|
531
533
|
end
|
|
532
534
|
```
|
|
@@ -564,8 +566,8 @@ class SignupReactor < RubyReactor::Reactor
|
|
|
564
566
|
|
|
565
567
|
step :verify do
|
|
566
568
|
argument :account, result(:provision_account) # waits for the child
|
|
567
|
-
run do |
|
|
568
|
-
|
|
569
|
+
run do |inputs|
|
|
570
|
+
inputs.account.success? ? Success(inputs.account.value) : Failure(inputs.account.error)
|
|
569
571
|
end
|
|
570
572
|
end
|
|
571
573
|
end
|
|
@@ -667,7 +669,7 @@ Pause execution to wait for external events like webhooks or user approvals.
|
|
|
667
669
|
```ruby
|
|
668
670
|
class ApprovalReactor < RubyReactor::Reactor
|
|
669
671
|
step :submit_request do
|
|
670
|
-
run { |
|
|
672
|
+
run { |inputs| RequestService.submit(inputs.to_h) }
|
|
671
673
|
end
|
|
672
674
|
|
|
673
675
|
interrupt :wait_for_manager do
|
|
@@ -678,8 +680,8 @@ class ApprovalReactor < RubyReactor::Reactor
|
|
|
678
680
|
|
|
679
681
|
step :process_decision do
|
|
680
682
|
argument :decision, result(:wait_for_manager)
|
|
681
|
-
run do |
|
|
682
|
-
|
|
683
|
+
run do |inputs|
|
|
684
|
+
inputs.decision == 'approved' ? Success() : Failure("Rejected")
|
|
683
685
|
end
|
|
684
686
|
end
|
|
685
687
|
end
|
|
@@ -716,7 +718,7 @@ class RefundOrderReactor < RubyReactor::Reactor
|
|
|
716
718
|
|
|
717
719
|
step :refund do
|
|
718
720
|
argument :order_id, input(:order_id)
|
|
719
|
-
run { |
|
|
721
|
+
run { |inputs| PaymentGateway.refund(inputs.order_id) }
|
|
720
722
|
end
|
|
721
723
|
end
|
|
722
724
|
|
|
@@ -745,7 +747,7 @@ class GeocodeReactor < RubyReactor::Reactor
|
|
|
745
747
|
|
|
746
748
|
step :geocode do
|
|
747
749
|
argument :address, input(:address)
|
|
748
|
-
run { |
|
|
750
|
+
run { |inputs| Geocoder.lookup(inputs.address) }
|
|
749
751
|
end
|
|
750
752
|
end
|
|
751
753
|
|
|
@@ -759,7 +761,7 @@ class MonthlyBillingReactor < RubyReactor::Reactor
|
|
|
759
761
|
|
|
760
762
|
step :build do
|
|
761
763
|
argument :org_id, input(:org_id)
|
|
762
|
-
run { |
|
|
764
|
+
run { |inputs| Billing.generate(inputs.org_id) }
|
|
763
765
|
end
|
|
764
766
|
end
|
|
765
767
|
|
|
@@ -775,7 +777,7 @@ class ChargeReactor < RubyReactor::Reactor
|
|
|
775
777
|
|
|
776
778
|
step :charge do
|
|
777
779
|
argument :account_id, input(:account_id)
|
|
778
|
-
run { |
|
|
780
|
+
run { |inputs| Stripe.charge(inputs.account_id) }
|
|
779
781
|
end
|
|
780
782
|
end
|
|
781
783
|
|
|
@@ -792,7 +794,7 @@ class OrderedTransactionReactor < RubyReactor::Reactor
|
|
|
792
794
|
|
|
793
795
|
step :apply do
|
|
794
796
|
argument :transaction, input(:transaction)
|
|
795
|
-
run { |
|
|
797
|
+
run { |inputs| Ledger.apply(inputs.transaction) }
|
|
796
798
|
end
|
|
797
799
|
end
|
|
798
800
|
|
|
@@ -816,7 +818,7 @@ class ChargeReactor < RubyReactor::Reactor
|
|
|
816
818
|
|
|
817
819
|
step :charge do
|
|
818
820
|
argument :account_id, input(:account_id)
|
|
819
|
-
run { |
|
|
821
|
+
run { |inputs| Stripe.charge(inputs.account_id) }
|
|
820
822
|
end
|
|
821
823
|
end
|
|
822
824
|
```
|
|
@@ -841,9 +843,9 @@ A step's `run` block can also return `Halt(reason: "...")` to stop the reactor c
|
|
|
841
843
|
```ruby
|
|
842
844
|
step :ensure_active do
|
|
843
845
|
argument :user, result(:fetch_user)
|
|
844
|
-
run do |
|
|
845
|
-
next Halt(reason: "user_opted_out") if
|
|
846
|
-
Success(
|
|
846
|
+
run do |inputs, _ctx|
|
|
847
|
+
next Halt(reason: "user_opted_out") if inputs.user.opted_out?
|
|
848
|
+
Success(inputs.user)
|
|
847
849
|
end
|
|
848
850
|
end
|
|
849
851
|
```
|
|
@@ -853,9 +855,9 @@ To skip a *single* step while the reactor continues — the step did nothing, bu
|
|
|
853
855
|
```ruby
|
|
854
856
|
step :maybe_sync do
|
|
855
857
|
argument :user, result(:fetch_user)
|
|
856
|
-
run do |
|
|
857
|
-
next Skipped(
|
|
858
|
-
Success(sync!(
|
|
858
|
+
run do |inputs, _ctx|
|
|
859
|
+
next Skipped(inputs.user) if inputs.user.already_synced?
|
|
860
|
+
Success(sync!(inputs.user))
|
|
859
861
|
end
|
|
860
862
|
end
|
|
861
863
|
```
|
|
@@ -879,7 +881,7 @@ class DataProcessingReactor < RubyReactor::Reactor
|
|
|
879
881
|
|
|
880
882
|
step :transform do
|
|
881
883
|
argument :item, input(:item)
|
|
882
|
-
run { |
|
|
884
|
+
run { |inputs| transform_item(inputs.item) }
|
|
883
885
|
end
|
|
884
886
|
|
|
885
887
|
returns :transform
|
|
@@ -913,7 +915,7 @@ map :archive_old_users do
|
|
|
913
915
|
|
|
914
916
|
step :archive do
|
|
915
917
|
argument :user, input(:user)
|
|
916
|
-
run { |
|
|
918
|
+
run { |inputs| inputs.user.archive! }
|
|
917
919
|
end
|
|
918
920
|
|
|
919
921
|
returns :archive
|
|
@@ -921,9 +923,9 @@ end
|
|
|
921
923
|
|
|
922
924
|
step :summary do
|
|
923
925
|
argument :results, result(:archive_old_users)
|
|
924
|
-
run do |
|
|
925
|
-
puts "Archived: #{
|
|
926
|
-
puts "Failed: #{
|
|
926
|
+
run do |inputs|
|
|
927
|
+
puts "Archived: #{inputs.results.successes.count}"
|
|
928
|
+
puts "Failed: #{inputs.results.failures.count}"
|
|
927
929
|
Success()
|
|
928
930
|
end
|
|
929
931
|
end
|
|
@@ -1005,12 +1007,12 @@ class ValidatedUserReactor < RubyReactor::Reactor
|
|
|
1005
1007
|
argument :age, input(:age)
|
|
1006
1008
|
argument :bio, input(:bio)
|
|
1007
1009
|
|
|
1008
|
-
run do |
|
|
1010
|
+
run do |inputs, context|
|
|
1009
1011
|
profile = {
|
|
1010
|
-
name:
|
|
1011
|
-
email:
|
|
1012
|
-
age:
|
|
1013
|
-
bio:
|
|
1012
|
+
name: inputs.name,
|
|
1013
|
+
email: inputs.email,
|
|
1014
|
+
age: inputs.age,
|
|
1015
|
+
bio: inputs.bio || "No bio provided",
|
|
1014
1016
|
created_at: Time.now
|
|
1015
1017
|
}
|
|
1016
1018
|
Success(profile)
|
|
@@ -1124,7 +1126,7 @@ step :charge do
|
|
|
1124
1126
|
argument :amount, input(:amount)
|
|
1125
1127
|
argument :currency, input(:currency)
|
|
1126
1128
|
|
|
1127
|
-
run { |
|
|
1129
|
+
run { |inputs, _| charge!(inputs) }
|
|
1128
1130
|
end
|
|
1129
1131
|
```
|
|
1130
1132
|
|
|
@@ -1201,9 +1203,9 @@ class OrderProcessingReactor < RubyReactor::Reactor
|
|
|
1201
1203
|
step :validate_user do
|
|
1202
1204
|
argument :user_id, input(:user_id)
|
|
1203
1205
|
|
|
1204
|
-
run do |
|
|
1206
|
+
run do |inputs, context|
|
|
1205
1207
|
# Check if user exists and has permission to purchase
|
|
1206
|
-
user = find_user(
|
|
1208
|
+
user = find_user(inputs.user_id)
|
|
1207
1209
|
user ? Success(user) : Failure("User not found")
|
|
1208
1210
|
end
|
|
1209
1211
|
end
|
|
@@ -1211,8 +1213,8 @@ class OrderProcessingReactor < RubyReactor::Reactor
|
|
|
1211
1213
|
step :validate_products do
|
|
1212
1214
|
argument :product_ids, input(:product_ids)
|
|
1213
1215
|
|
|
1214
|
-
run do |
|
|
1215
|
-
products =
|
|
1216
|
+
run do |inputs, context|
|
|
1217
|
+
products = inputs.product_ids.map { |id| find_product(id) }
|
|
1216
1218
|
if products.all?
|
|
1217
1219
|
Success(products)
|
|
1218
1220
|
else
|
|
@@ -1224,8 +1226,8 @@ class OrderProcessingReactor < RubyReactor::Reactor
|
|
|
1224
1226
|
step :calculate_total do
|
|
1225
1227
|
argument :products, result(:validate_products)
|
|
1226
1228
|
|
|
1227
|
-
run do |
|
|
1228
|
-
total =
|
|
1229
|
+
run do |inputs, context|
|
|
1230
|
+
total = inputs.products.sum { |p| p[:price] }
|
|
1229
1231
|
Success(total)
|
|
1230
1232
|
end
|
|
1231
1233
|
end
|
|
@@ -1233,8 +1235,8 @@ class OrderProcessingReactor < RubyReactor::Reactor
|
|
|
1233
1235
|
step :check_inventory do
|
|
1234
1236
|
argument :products, result(:validate_products)
|
|
1235
1237
|
|
|
1236
|
-
run do |
|
|
1237
|
-
available =
|
|
1238
|
+
run do |inputs, context|
|
|
1239
|
+
available = inputs.products.all? { |p| p[:stock] > 0 }
|
|
1238
1240
|
available ? Success(true) : Failure("Out of stock")
|
|
1239
1241
|
end
|
|
1240
1242
|
end
|
|
@@ -1243,15 +1245,15 @@ class OrderProcessingReactor < RubyReactor::Reactor
|
|
|
1243
1245
|
argument :user, result(:validate_user)
|
|
1244
1246
|
argument :total, result(:calculate_total)
|
|
1245
1247
|
|
|
1246
|
-
run do |
|
|
1248
|
+
run do |inputs, context|
|
|
1247
1249
|
# Process payment logic here
|
|
1248
|
-
payment_id = process_payment(
|
|
1250
|
+
payment_id = process_payment(inputs.user[:id], inputs.total)
|
|
1249
1251
|
Success(payment_id)
|
|
1250
1252
|
end
|
|
1251
1253
|
|
|
1252
|
-
undo do |error,
|
|
1254
|
+
undo do |error, inputs, context|
|
|
1253
1255
|
# Refund payment on failure
|
|
1254
|
-
refund_payment(
|
|
1256
|
+
refund_payment(inputs.payment_id)
|
|
1255
1257
|
Success()
|
|
1256
1258
|
end
|
|
1257
1259
|
end
|
|
@@ -1261,14 +1263,14 @@ class OrderProcessingReactor < RubyReactor::Reactor
|
|
|
1261
1263
|
argument :products, result(:validate_products)
|
|
1262
1264
|
argument :payment_id, result(:process_payment)
|
|
1263
1265
|
|
|
1264
|
-
run do |
|
|
1265
|
-
order = create_order_record(
|
|
1266
|
+
run do |inputs, context|
|
|
1267
|
+
order = create_order_record(inputs.user, inputs.products, inputs.payment_id)
|
|
1266
1268
|
Success(order)
|
|
1267
1269
|
end
|
|
1268
1270
|
|
|
1269
|
-
undo do |error,
|
|
1271
|
+
undo do |error, inputs, context|
|
|
1270
1272
|
# Cancel order and update inventory
|
|
1271
|
-
cancel_order(
|
|
1273
|
+
cancel_order(inputs.order[:id])
|
|
1272
1274
|
Success()
|
|
1273
1275
|
end
|
|
1274
1276
|
end
|
|
@@ -1276,14 +1278,14 @@ class OrderProcessingReactor < RubyReactor::Reactor
|
|
|
1276
1278
|
step :update_inventory do
|
|
1277
1279
|
argument :products, result(:validate_products)
|
|
1278
1280
|
|
|
1279
|
-
run do |
|
|
1280
|
-
|
|
1281
|
+
run do |inputs, context|
|
|
1282
|
+
inputs.products.each { |p| decrement_stock(p[:id]) }
|
|
1281
1283
|
Success(true)
|
|
1282
1284
|
end
|
|
1283
1285
|
|
|
1284
|
-
undo do |error,
|
|
1286
|
+
undo do |error, inputs, context|
|
|
1285
1287
|
# Restock products
|
|
1286
|
-
|
|
1288
|
+
inputs.products.each { |p| increment_stock(p[:id]) }
|
|
1287
1289
|
Success()
|
|
1288
1290
|
end
|
|
1289
1291
|
end
|
|
@@ -1292,8 +1294,8 @@ class OrderProcessingReactor < RubyReactor::Reactor
|
|
|
1292
1294
|
argument :user, result(:validate_user)
|
|
1293
1295
|
argument :order, result(:create_order)
|
|
1294
1296
|
|
|
1295
|
-
run do |
|
|
1296
|
-
send_email(
|
|
1297
|
+
run do |inputs, context|
|
|
1298
|
+
send_email(inputs.user[:email], "Order confirmed", order_details(inputs.order))
|
|
1297
1299
|
Success(true)
|
|
1298
1300
|
end
|
|
1299
1301
|
end
|
|
@@ -1316,9 +1318,9 @@ class TransactionReactor < RubyReactor::Reactor
|
|
|
1316
1318
|
argument :from_account, input(:from_account)
|
|
1317
1319
|
argument :to_account, input(:to_account)
|
|
1318
1320
|
|
|
1319
|
-
run do |
|
|
1320
|
-
from = find_account(
|
|
1321
|
-
to = find_account(
|
|
1321
|
+
run do |inputs, context|
|
|
1322
|
+
from = find_account(inputs.from_account)
|
|
1323
|
+
to = find_account(inputs.to_account)
|
|
1322
1324
|
|
|
1323
1325
|
if from && to && from != to
|
|
1324
1326
|
Success({from: from, to: to})
|
|
@@ -1332,9 +1334,9 @@ class TransactionReactor < RubyReactor::Reactor
|
|
|
1332
1334
|
argument :accounts, result(:validate_accounts)
|
|
1333
1335
|
argument :amount, input(:amount)
|
|
1334
1336
|
|
|
1335
|
-
run do |
|
|
1336
|
-
if
|
|
1337
|
-
Success(
|
|
1337
|
+
run do |inputs, context|
|
|
1338
|
+
if inputs.accounts[:from][:balance] >= inputs.amount
|
|
1339
|
+
Success(inputs.accounts)
|
|
1338
1340
|
else
|
|
1339
1341
|
Failure("Insufficient funds")
|
|
1340
1342
|
end
|
|
@@ -1345,14 +1347,14 @@ class TransactionReactor < RubyReactor::Reactor
|
|
|
1345
1347
|
argument :accounts, result(:check_balance)
|
|
1346
1348
|
argument :amount, input(:amount)
|
|
1347
1349
|
|
|
1348
|
-
run do |
|
|
1349
|
-
debit(
|
|
1350
|
-
Success(
|
|
1350
|
+
run do |inputs, context|
|
|
1351
|
+
debit(inputs.accounts[:from][:id], inputs.amount)
|
|
1352
|
+
Success(inputs.accounts)
|
|
1351
1353
|
end
|
|
1352
1354
|
|
|
1353
|
-
undo do |error,
|
|
1355
|
+
undo do |error, inputs, context|
|
|
1354
1356
|
# Credit the amount back
|
|
1355
|
-
credit(
|
|
1357
|
+
credit(inputs.accounts[:from][:id], inputs.amount)
|
|
1356
1358
|
Success()
|
|
1357
1359
|
end
|
|
1358
1360
|
end
|
|
@@ -1361,14 +1363,14 @@ class TransactionReactor < RubyReactor::Reactor
|
|
|
1361
1363
|
argument :accounts, result(:debit_account)
|
|
1362
1364
|
argument :amount, input(:amount)
|
|
1363
1365
|
|
|
1364
|
-
run do |
|
|
1365
|
-
credit(
|
|
1366
|
+
run do |inputs, context|
|
|
1367
|
+
credit(inputs.accounts[:to][:id], inputs.amount)
|
|
1366
1368
|
Success({transaction_id: generate_transaction_id()})
|
|
1367
1369
|
end
|
|
1368
1370
|
|
|
1369
|
-
undo do |error,
|
|
1371
|
+
undo do |error, inputs, context|
|
|
1370
1372
|
# Debit the amount back from recipient
|
|
1371
|
-
debit(
|
|
1373
|
+
debit(inputs.accounts[:to][:id], inputs.amount)
|
|
1372
1374
|
Success()
|
|
1373
1375
|
end
|
|
1374
1376
|
end
|
|
@@ -1377,9 +1379,9 @@ class TransactionReactor < RubyReactor::Reactor
|
|
|
1377
1379
|
argument :accounts, result(:validate_accounts)
|
|
1378
1380
|
wait_for :credit_account, :debit_account
|
|
1379
1381
|
|
|
1380
|
-
run do |
|
|
1381
|
-
Notify.to(
|
|
1382
|
-
Notify.to(
|
|
1382
|
+
run do |inputs, context|
|
|
1383
|
+
Notify.to(inputs.accounts[:from])
|
|
1384
|
+
Notify.to(inputs.accounts[:to])
|
|
1383
1385
|
end
|
|
1384
1386
|
|
|
1385
1387
|
end
|
|
@@ -1430,8 +1432,8 @@ class SchemaValidatedReactor < RubyReactor::Reactor
|
|
|
1430
1432
|
step :process_user do
|
|
1431
1433
|
argument :user, input(:user)
|
|
1432
1434
|
|
|
1433
|
-
run do |
|
|
1434
|
-
Success(
|
|
1435
|
+
run do |inputs, context|
|
|
1436
|
+
Success(inputs.user)
|
|
1435
1437
|
end
|
|
1436
1438
|
end
|
|
1437
1439
|
|
|
@@ -1483,7 +1485,7 @@ Discover how to build complex, modular workflows by composing reactors within ot
|
|
|
1483
1485
|
Master the `map` feature for processing collections. Learn about parallel execution, batch processing for large datasets, and error handling strategies like fail-fast vs. partial result collection.
|
|
1484
1486
|
|
|
1485
1487
|
### [Retry Configuration](documentation/retry_configuration.md)
|
|
1486
|
-
Configure robust retry policies for your steps. This guide details the available backoff strategies (exponential, linear, fixed), how to
|
|
1488
|
+
Configure robust retry policies for your steps. This guide details the available backoff strategies (exponential, linear, fixed), how to declare retries on a step class or inline step, and how background retries work without blocking workers.
|
|
1487
1489
|
|
|
1488
1490
|
### [Interrupts](documentation/interrupts.md)
|
|
1489
1491
|
Learn how to pause and resume reactors to handle long-running processes, manual approvals, and asynchronous callbacks. Patterns for correlation IDs, timeouts, and payload validation.
|
|
@@ -10,6 +10,7 @@ module RubyReactor
|
|
|
10
10
|
# ordering nonce.
|
|
11
11
|
class AsyncReactorBuilder
|
|
12
12
|
include RubyReactor::Dsl::TemplateHelpers
|
|
13
|
+
include RubyReactor::Dsl::Retryable
|
|
13
14
|
|
|
14
15
|
attr_accessor :name, :child_reactor_class, :argument_mappings
|
|
15
16
|
|
|
@@ -18,17 +19,13 @@ module RubyReactor
|
|
|
18
19
|
@child_reactor_class = child_reactor_class
|
|
19
20
|
@reactor = reactor
|
|
20
21
|
@argument_mappings = {}
|
|
21
|
-
@retry_config =
|
|
22
|
+
@retry_config = nil
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def argument(child_input_name, source)
|
|
25
26
|
@argument_mappings[child_input_name] = source
|
|
26
27
|
end
|
|
27
28
|
|
|
28
|
-
def retries(max_attempts: 3, backoff: :exponential, base_delay: 1)
|
|
29
|
-
@retry_config = { max_attempts: max_attempts, backoff: backoff, base_delay: base_delay }
|
|
30
|
-
end
|
|
31
|
-
|
|
32
29
|
def build
|
|
33
30
|
RubyReactor::Dsl::StepConfig.new(
|
|
34
31
|
async_dispatch: :reactor,
|
|
@@ -49,7 +46,7 @@ module RubyReactor
|
|
|
49
46
|
dependencies: dependencies_from_mappings,
|
|
50
47
|
args_validator: nil,
|
|
51
48
|
output_validator: nil,
|
|
52
|
-
retry_config: @retry_config
|
|
49
|
+
retry_config: @retry_config
|
|
53
50
|
)
|
|
54
51
|
end
|
|
55
52
|
|
|
@@ -4,6 +4,7 @@ module RubyReactor
|
|
|
4
4
|
module Dsl
|
|
5
5
|
class ComposeBuilder
|
|
6
6
|
include RubyReactor::Dsl::TemplateHelpers
|
|
7
|
+
include RubyReactor::Dsl::Retryable
|
|
7
8
|
|
|
8
9
|
attr_accessor :name, :composed_reactor_class, :argument_mappings
|
|
9
10
|
|
|
@@ -20,7 +21,7 @@ module RubyReactor
|
|
|
20
21
|
end
|
|
21
22
|
@reactor = reactor
|
|
22
23
|
@argument_mappings = {}
|
|
23
|
-
@retry_config =
|
|
24
|
+
@retry_config = nil
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def argument(composed_input_name, source)
|
|
@@ -44,14 +45,6 @@ module RubyReactor
|
|
|
44
45
|
)
|
|
45
46
|
end
|
|
46
47
|
|
|
47
|
-
def retries(max_attempts: 3, backoff: :exponential, base_delay: 1)
|
|
48
|
-
@retry_config = {
|
|
49
|
-
max_attempts: max_attempts,
|
|
50
|
-
backoff: backoff,
|
|
51
|
-
base_delay: base_delay
|
|
52
|
-
}
|
|
53
|
-
end
|
|
54
|
-
|
|
55
48
|
def build
|
|
56
49
|
warn_if_child_has_ordered_lock!
|
|
57
50
|
dependencies = extract_dependencies_from_mappings
|
|
@@ -71,7 +64,7 @@ module RubyReactor
|
|
|
71
64
|
dependencies: dependencies,
|
|
72
65
|
args_validator: nil,
|
|
73
66
|
output_validator: nil,
|
|
74
|
-
retry_config: @retry_config
|
|
67
|
+
retry_config: @retry_config
|
|
75
68
|
}
|
|
76
69
|
|
|
77
70
|
RubyReactor::Dsl::StepConfig.new(step_config)
|
|
@@ -11,7 +11,6 @@ module RubyReactor
|
|
|
11
11
|
base.instance_variable_set(:@middlewares, [])
|
|
12
12
|
base.instance_variable_set(:@input_validations, {})
|
|
13
13
|
base.instance_variable_set(:@background_handoff, nil)
|
|
14
|
-
base.instance_variable_set(:@retry_defaults, { max_attempts: 3, backoff: :exponential, base_delay: 1 })
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
module ClassMethods
|
|
@@ -55,16 +54,14 @@ module RubyReactor
|
|
|
55
54
|
"inputs inside the worker."
|
|
56
55
|
end
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
end
|
|
57
|
+
# Reactor-wide retry defaults are gone: the builders snapshotted them at
|
|
58
|
+
# `step` time, so they silently applied only to steps declared AFTER
|
|
59
|
+
# the `retry_defaults` line. A step's retry policy now lives on the step.
|
|
60
|
+
def retry_defaults(*, **)
|
|
61
|
+
raise RubyReactor::Error::DeprecatedDslError,
|
|
62
|
+
"`retry_defaults` has been removed from #{name || "this reactor"}: reactor-wide defaults " \
|
|
63
|
+
"silently applied only to steps declared after them. Declare `retries` on each step class " \
|
|
64
|
+
"(or step block) that should retry; a step with no `retries` runs once."
|
|
68
65
|
end
|
|
69
66
|
|
|
70
67
|
# rubocop:disable Metrics/ParameterLists
|