ruby_reactor 0.1.0

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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +98 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/README.md +570 -0
  6. data/Rakefile +12 -0
  7. data/documentation/DAG.md +457 -0
  8. data/documentation/README.md +123 -0
  9. data/documentation/async_reactors.md +369 -0
  10. data/documentation/composition.md +199 -0
  11. data/documentation/core_concepts.md +662 -0
  12. data/documentation/data_pipelines.md +224 -0
  13. data/documentation/examples/inventory_management.md +749 -0
  14. data/documentation/examples/order_processing.md +365 -0
  15. data/documentation/examples/payment_processing.md +654 -0
  16. data/documentation/getting_started.md +224 -0
  17. data/documentation/retry_configuration.md +357 -0
  18. data/lib/ruby_reactor/async_router.rb +91 -0
  19. data/lib/ruby_reactor/configuration.rb +41 -0
  20. data/lib/ruby_reactor/context.rb +169 -0
  21. data/lib/ruby_reactor/context_serializer.rb +164 -0
  22. data/lib/ruby_reactor/dependency_graph.rb +126 -0
  23. data/lib/ruby_reactor/dsl/compose_builder.rb +86 -0
  24. data/lib/ruby_reactor/dsl/map_builder.rb +112 -0
  25. data/lib/ruby_reactor/dsl/reactor.rb +151 -0
  26. data/lib/ruby_reactor/dsl/step_builder.rb +177 -0
  27. data/lib/ruby_reactor/dsl/template_helpers.rb +36 -0
  28. data/lib/ruby_reactor/dsl/validation_helpers.rb +35 -0
  29. data/lib/ruby_reactor/error/base.rb +16 -0
  30. data/lib/ruby_reactor/error/compensation_error.rb +8 -0
  31. data/lib/ruby_reactor/error/context_too_large_error.rb +11 -0
  32. data/lib/ruby_reactor/error/dependency_error.rb +8 -0
  33. data/lib/ruby_reactor/error/deserialization_error.rb +11 -0
  34. data/lib/ruby_reactor/error/input_validation_error.rb +29 -0
  35. data/lib/ruby_reactor/error/schema_version_error.rb +11 -0
  36. data/lib/ruby_reactor/error/step_failure_error.rb +18 -0
  37. data/lib/ruby_reactor/error/undo_error.rb +8 -0
  38. data/lib/ruby_reactor/error/validation_error.rb +8 -0
  39. data/lib/ruby_reactor/executor/compensation_manager.rb +79 -0
  40. data/lib/ruby_reactor/executor/graph_manager.rb +41 -0
  41. data/lib/ruby_reactor/executor/input_validator.rb +39 -0
  42. data/lib/ruby_reactor/executor/result_handler.rb +103 -0
  43. data/lib/ruby_reactor/executor/retry_manager.rb +156 -0
  44. data/lib/ruby_reactor/executor/step_executor.rb +319 -0
  45. data/lib/ruby_reactor/executor.rb +123 -0
  46. data/lib/ruby_reactor/map/collector.rb +65 -0
  47. data/lib/ruby_reactor/map/element_executor.rb +154 -0
  48. data/lib/ruby_reactor/map/execution.rb +60 -0
  49. data/lib/ruby_reactor/map/helpers.rb +67 -0
  50. data/lib/ruby_reactor/max_retries_exhausted_failure.rb +19 -0
  51. data/lib/ruby_reactor/reactor.rb +75 -0
  52. data/lib/ruby_reactor/retry_context.rb +92 -0
  53. data/lib/ruby_reactor/retry_queued_result.rb +26 -0
  54. data/lib/ruby_reactor/sidekiq_workers/map_collector_worker.rb +13 -0
  55. data/lib/ruby_reactor/sidekiq_workers/map_element_worker.rb +13 -0
  56. data/lib/ruby_reactor/sidekiq_workers/map_execution_worker.rb +15 -0
  57. data/lib/ruby_reactor/sidekiq_workers/worker.rb +55 -0
  58. data/lib/ruby_reactor/step/compose_step.rb +107 -0
  59. data/lib/ruby_reactor/step/map_step.rb +234 -0
  60. data/lib/ruby_reactor/step.rb +33 -0
  61. data/lib/ruby_reactor/storage/adapter.rb +51 -0
  62. data/lib/ruby_reactor/storage/configuration.rb +15 -0
  63. data/lib/ruby_reactor/storage/redis_adapter.rb +140 -0
  64. data/lib/ruby_reactor/template/base.rb +15 -0
  65. data/lib/ruby_reactor/template/element.rb +25 -0
  66. data/lib/ruby_reactor/template/input.rb +48 -0
  67. data/lib/ruby_reactor/template/result.rb +48 -0
  68. data/lib/ruby_reactor/template/value.rb +22 -0
  69. data/lib/ruby_reactor/validation/base.rb +26 -0
  70. data/lib/ruby_reactor/validation/input_validator.rb +62 -0
  71. data/lib/ruby_reactor/validation/schema_builder.rb +17 -0
  72. data/lib/ruby_reactor/version.rb +5 -0
  73. data/lib/ruby_reactor.rb +159 -0
  74. data/sig/ruby_reactor.rbs +4 -0
  75. metadata +178 -0
@@ -0,0 +1,369 @@
1
+ # Async Reactors
2
+
3
+ RubyReactor supports two asynchronous execution models: **Full Reactor Async** and **Step-Level Async**. Both models use Sidekiq for background processing with non-blocking retry mechanisms.
4
+
5
+ ## Overview
6
+
7
+ Async execution provides several benefits:
8
+
9
+ - **Non-blocking**: Workers are freed during retry delays
10
+ - **Scalable**: Better resource utilization with large worker pools
11
+ - **Reliable**: Automatic retry with configurable backoff strategies
12
+ - **Observable**: Full visibility into execution state and retry attempts
13
+
14
+ ## Full Reactor Async
15
+
16
+ When a reactor is marked as `async true`, the entire execution happens in a Sidekiq worker, including input validation.
17
+
18
+ ### Configuration
19
+
20
+ ```ruby
21
+ class OrderProcessingReactor < RubyReactor::Reactor
22
+ async true # Enable full reactor async
23
+
24
+ step :validate_order do
25
+ run { validate_order_logic }
26
+ end
27
+
28
+ step :process_payment do
29
+ run { process_payment_logic }
30
+ end
31
+
32
+ step :send_confirmation do
33
+ run { send_confirmation_logic }
34
+ end
35
+ end
36
+ ```
37
+
38
+ ### Execution Flow
39
+
40
+ ```ruby
41
+ # Synchronous call returns immediately
42
+ async_result = OrderProcessingReactor.run(order_id: 123)
43
+
44
+ # Check status later
45
+ case async_result.status
46
+ when :pending
47
+ puts "Execution is queued"
48
+ when :running
49
+ puts "Execution is in progress"
50
+ when :success
51
+ puts "Execution completed successfully"
52
+ result = async_result.result
53
+ when :failed
54
+ puts "Execution failed: #{async_result.error}"
55
+ end
56
+ ```
57
+
58
+ ### Architecture
59
+
60
+ ```mermaid
61
+ graph LR
62
+ A[Client] --> B[Reactor.run<br/>async: true]
63
+ B --> C[Validate Inputs<br/>Synchronously]
64
+ C --> D[Queue Sidekiq Job<br/>with Context]
65
+ D --> E[Sidekiq Worker]
66
+ E --> F[Deserialize Context]
67
+ F --> G[Execute All Steps<br/>Sequentially]
68
+ G --> H{Result?}
69
+ H -->|Success| I[Return Success]
70
+ H -->|Failure| J[Run Compensation<br/>in Worker]
71
+ J --> K[Return Failure]
72
+ ```
73
+
74
+ ```
75
+ Client → Reactor.run() → Queue Job → Sidekiq Worker → Execute All Steps
76
+ ```
77
+
78
+ ## Step-Level Async
79
+
80
+ Individual steps can be marked as `async: true`. Execution runs synchronously until the first async step, then hands off to a Sidekiq worker for all remaining execution.
81
+
82
+ ### Configuration
83
+
84
+ ```ruby
85
+ class OrderProcessingReactor < RubyReactor::Reactor
86
+ step :validate_order do
87
+ # Runs synchronously
88
+ run { validate_order_logic }
89
+ end
90
+
91
+ step :process_payment, async: true do
92
+ # First async step - triggers handoff to worker
93
+ run { process_payment_logic }
94
+ end
95
+
96
+ step :update_inventory do
97
+ # Runs in worker after handoff
98
+ run { update_inventory_logic }
99
+ end
100
+
101
+ step :send_confirmation do
102
+ # Runs in same worker
103
+ run { send_confirmation_logic }
104
+ end
105
+ end
106
+ ```
107
+
108
+ ### Execution Flow
109
+
110
+ ```ruby
111
+ # Runs validate_order synchronously, then hands off
112
+ async_result = OrderProcessingReactor.run(order_id: 123)
113
+
114
+ # All remaining steps execute in a single worker
115
+ # Compensation and rollback work within the worker context
116
+ ```
117
+
118
+ ### Architecture
119
+
120
+ ```mermaid
121
+ graph LR
122
+ A[Client] --> B[Reactor.run]
123
+ B --> C[Execute Sync Steps<br/>Until First Async]
124
+ C --> D{First Async<br/>Step Found?}
125
+ D -->|No| E[Execute All Steps<br/>Synchronously]
126
+ D -->|Yes| F[Queue Sidekiq Job<br/>with Context]
127
+ F --> G[Sidekiq Worker]
128
+ G --> H[Execute Remaining Steps<br/>Sequentially]
129
+ H --> I{Result?}
130
+ I -->|Success| J[Return Success]
131
+ I -->|Failure| K[Run Compensation<br/>in Worker]
132
+ K --> L[Return Failure]
133
+ ```
134
+
135
+ ```
136
+ Client → Reactor.run() → Sync Steps → Queue Job → Worker → Remaining Steps
137
+ ```
138
+
139
+ ## Async Steps
140
+
141
+ Individual steps can be configured with `async true`, which changes the execution behavior at the point where the first async step is encountered.
142
+
143
+ ### Key Behavior
144
+
145
+ When an async step is encountered during synchronous execution:
146
+
147
+ 1. **All previous steps have already executed synchronously** in the main thread
148
+ 2. **The Sidekiq job is queued** at the moment the async step would be executed
149
+ 3. **The async step itself and all subsequent steps execute** in the Sidekiq worker
150
+ 4. **The main thread returns immediately** with an async result handle
151
+
152
+ ### Important Distinction
153
+
154
+ - **Before async step**: All execution is synchronous
155
+ - **At async step**: Job is queued instead of executing the step
156
+ - **After async step**: All remaining execution happens in the worker
157
+
158
+ ### Example
159
+
160
+ ```ruby
161
+ class OrderProcessingReactor < RubyReactor::Reactor
162
+ step :validate_input do
163
+ # Executes synchronously in main thread
164
+ run { validate_order_input }
165
+ end
166
+
167
+ step :check_inventory do
168
+ # Executes synchronously in main thread
169
+ run { check_inventory_levels }
170
+ end
171
+
172
+ step :process_payment do
173
+ async true
174
+ # Job is queued here - this step executes in worker
175
+ run { process_payment_logic }
176
+ end
177
+
178
+ step :update_inventory do
179
+ # Executes in worker
180
+ run { update_inventory_records }
181
+ end
182
+
183
+ step :send_notification do
184
+ # Executes in worker
185
+ run { send_order_confirmation }
186
+ end
187
+ end
188
+ ```
189
+
190
+ ### Execution Flow with Mermaid
191
+
192
+ ```mermaid
193
+ sequenceDiagram
194
+ participant Client
195
+ participant Reactor
196
+ participant Sidekiq
197
+ participant Worker
198
+
199
+ Client->>Reactor: run(order_data)
200
+ Reactor->>Reactor: Execute validate_input (sync)
201
+ Reactor->>Reactor: Execute check_inventory (sync)
202
+ Reactor->>Reactor: Reach process_payment (async: true)
203
+ Reactor->>Sidekiq: Queue job with context
204
+ Reactor->>Client: Return AsyncResult (pending)
205
+ Sidekiq->>Worker: Process job
206
+ Worker->>Worker: Execute process_payment
207
+ Worker->>Worker: Execute update_inventory
208
+ Worker->>Worker: Execute send_notification
209
+ Worker->>Sidekiq: Mark job complete
210
+ ```
211
+
212
+ ```mermaid
213
+ graph TD
214
+ A[Client Calls Reactor.run] --> B[Execute Previous Steps Synchronously]
215
+ B --> C[Encounter First Async Step]
216
+ C --> D[Queue Sidekiq Job<br/>with Current Context]
217
+ D --> E[Return AsyncResult to Client<br/>Status: :pending]
218
+ D --> F[Sidekiq Worker Receives Job]
219
+ F --> G[Deserialize Context]
220
+ G --> H[Execute Async Step<br/>and All Subsequent Steps]
221
+ H --> I{Execution<br/>Successful?}
222
+ I -->|Yes| J[Mark AsyncResult<br/>Status: :success]
223
+ I -->|No| K[Run Compensation<br/>in Worker Context]
224
+ K --> L[Mark AsyncResult<br/>Status: :failed]
225
+ ```
226
+
227
+ ### Critical Points
228
+
229
+ - **Synchronous Prefix Guarantee**: Steps before the first async step always complete synchronously
230
+ - **Single Handoff Point**: Only one job is queued per reactor execution
231
+ - **Worker Execution**: The async step and all following steps run in the same worker
232
+ - **Context Preservation**: Execution state is serialized and passed to the worker
233
+ - **Compensation Scope**: All compensation for failed async execution happens in the worker
234
+
235
+ ## Retry Configuration
236
+
237
+ Both async models support sophisticated retry mechanisms with non-blocking job requeuing.
238
+
239
+ ### Step-Level Retry
240
+
241
+ ```ruby
242
+ class PaymentProcessingReactor < RubyReactor::Reactor
243
+ async true
244
+
245
+ step :validate_payment do
246
+ retries max_attempts: 3, backoff: :exponential, base_delay: 1.second
247
+ run { validate_payment_logic }
248
+ end
249
+
250
+ step :charge_card, async: true do
251
+ retries max_attempts: 5, backoff: :linear, base_delay: 5.seconds
252
+ run { charge_card_logic }
253
+ end
254
+
255
+ step :update_records do
256
+ # No retry - critical step
257
+ run { update_records_logic }
258
+ end
259
+ end
260
+ ```
261
+
262
+ ### Reactor-Level Defaults
263
+
264
+ ```ruby
265
+ class PaymentProcessingReactor < RubyReactor::Reactor
266
+ async true
267
+
268
+ # Set defaults for all steps
269
+ retry_defaults max_attempts: 3, backoff: :exponential, base_delay: 2.seconds
270
+
271
+ step :validate_payment do
272
+ # Inherits reactor defaults (3 attempts, exponential backoff)
273
+ run { validate_payment_logic }
274
+ end
275
+
276
+ step :charge_card do
277
+ # Override defaults for this step
278
+ retries max_attempts: 5, backoff: :linear, base_delay: 10.seconds
279
+ run { charge_card_logic }
280
+ end
281
+ end
282
+ ```
283
+
284
+ ## Retry Strategies
285
+
286
+ ### Backoff Algorithms
287
+
288
+ - **`:exponential`**: Delay doubles with each attempt (1s, 2s, 4s, 8s...)
289
+ - **`:linear`**: Delay increases linearly (5s, 10s, 15s, 20s...)
290
+ - **`:fixed`**: Same delay for each attempt (5s, 5s, 5s, 5s...)
291
+
292
+ ## Error Handling and Compensation
293
+
294
+ Async reactors support full compensation and rollback in the worker context:
295
+
296
+ ```ruby
297
+ class OrderProcessingReactor < RubyReactor::Reactor
298
+ async true
299
+
300
+ step :process_payment do
301
+ run { process_payment_logic }
302
+
303
+ undo do |payment_id:, **|
304
+ # Runs in worker if execution fails later
305
+ PaymentService.refund(payment_id)
306
+ end
307
+
308
+ compensate do |payment_id:, **|
309
+ # Runs in worker if execution fails later
310
+ PaymentService.refund(payment_id)
311
+ end
312
+ end
313
+
314
+ step :update_inventory do
315
+ run { update_inventory_logic }
316
+
317
+ compensate do |order:, **|
318
+ # Runs in worker on failure
319
+ InventoryService.restore(order)
320
+ end
321
+ end
322
+ end
323
+ ```
324
+
325
+ ## Monitoring and Observability
326
+
327
+ ### Job Visibility
328
+
329
+ Retries are visible in Sidekiq web UI with:
330
+ - Step name and attempt number
331
+ - Retry delay and timing
332
+ - Success/failure status
333
+ - Execution context
334
+
335
+ ## Configuration
336
+
337
+ ### Sidekiq Worker Setup
338
+
339
+ ```ruby
340
+ # config/sidekiq.rb
341
+ require 'ruby_reactor/worker'
342
+
343
+ RubyReactor.configure do |config|
344
+ config.sidekiq_queue = :default
345
+ config.sidekiq_retry_count = 3
346
+ config.logger = Logger.new('log/ruby_reactor.log')
347
+ end
348
+ ```
349
+
350
+ ## Performance Considerations
351
+
352
+ ### Worker Pool Sizing
353
+
354
+ - **Full Reactor Async**: Size pool based on total reactor throughput
355
+ - **Step-Level Async**: Size pool based on async step frequency
356
+
357
+ ### Context Size Limits
358
+
359
+ - Redis has job size limits (~512MB)
360
+ - TODO: Large contexts are automatically compressed
361
+ - Consider external storage for very large execution states
362
+
363
+ ### Monitoring Metrics
364
+
365
+ Track these key metrics:
366
+ - Retry attempt counts per step
367
+ - Average retry delays
368
+ - Success rates after retries
369
+ - Worker utilization during peak loads
@@ -0,0 +1,199 @@
1
+ # Composition
2
+
3
+ RubyReactor allows you to compose reactors within other reactors using the `compose` DSL. This enables you to build complex workflows by reusing existing reactors or defining sub-workflows inline.
4
+
5
+ ## Inline Composition
6
+
7
+ You can define a composed reactor inline using a block. This is useful for grouping related steps or defining a sub-workflow that doesn't need to be reused elsewhere.
8
+
9
+ ```ruby
10
+ class UpdateUserReactor < RubyReactor::Reactor
11
+ input :user_id
12
+ input :profile_data
13
+
14
+ step :validate_user do
15
+ argument :user_id, input(:user_id)
16
+ run { |args| ... }
17
+ end
18
+
19
+ # Define a sub-workflow inline
20
+ compose :update_profile do
21
+ # You can define inputs for the inline reactor
22
+ argument :user_id, input(:user_id)
23
+ argument :data, input(:profile_data)
24
+
25
+ # Configure async execution for the sub-workflow
26
+ async true
27
+
28
+ # Configure retries for steps within the sub-workflow
29
+ retries max_attempts: 3
30
+
31
+ step :update_bio do
32
+ argument :user_id, input(:user_id)
33
+ argument :bio, input(:data, :bio)
34
+ run { |args| ... }
35
+ end
36
+
37
+ step :update_avatar do
38
+ argument :user_id, input(:user_id)
39
+ argument :avatar, input(:data, :avatar)
40
+ run { |args| ... }
41
+ end
42
+ end
43
+
44
+ step :notify_user do
45
+ wait_for :update_profile
46
+ run { |args| ... }
47
+ end
48
+ end
49
+ ```
50
+
51
+ ## Class-based Composition
52
+
53
+ You can also compose an existing reactor class. This is ideal for reusable workflows.
54
+
55
+ ```ruby
56
+ class ProfileUpdateReactor < RubyReactor::Reactor
57
+ input :user_id
58
+ input :data
59
+
60
+ step :update_bio do ... end
61
+ step :update_avatar do ... end
62
+ end
63
+
64
+ class MainReactor < RubyReactor::Reactor
65
+ input :user_id
66
+ input :profile_data
67
+
68
+ # Compose the existing reactor
69
+ compose :update_profile, ProfileUpdateReactor do
70
+ argument :user_id, input(:user_id)
71
+ argument :data, input(:profile_data)
72
+ end
73
+ end
74
+ ```
75
+
76
+ ## Multiple Compose Declarations
77
+
78
+ A single reactor can include multiple `compose` declarations, allowing you to orchestrate several sub-workflows. You can mix both class-based and inline compositions, and combine them with regular steps.
79
+
80
+ ```ruby
81
+ class OrderProcessingReactor < RubyReactor::Reactor
82
+ input :order_id
83
+ input :customer_data
84
+ input :payment_info
85
+
86
+ step :validate_order do
87
+ argument :order_id, input(:order_id)
88
+ run { |args| ... }
89
+ end
90
+
91
+ # First compose: Class-based reactor
92
+ compose :update_customer_profile, CustomerProfileReactor do
93
+ argument :customer_data, input(:customer_data)
94
+ end
95
+
96
+ # Second compose: Inline reactor
97
+ compose :process_payment do
98
+ argument :order_id, input(:order_id)
99
+ argument :payment_info, input(:payment_info)
100
+
101
+ async true # This sub-workflow can run async
102
+
103
+ step :authorize_payment do
104
+ run { |args| ... }
105
+ end
106
+
107
+ step :capture_payment do
108
+ run { |args| ... }
109
+ end
110
+ end
111
+
112
+ # Third compose: Another inline reactor
113
+ compose :allocate_inventory do
114
+ argument :order_id, input(:order_id)
115
+ argument :order, input(:validate_order)
116
+
117
+ step :check_availability do
118
+ run { |args| ... }
119
+ end
120
+
121
+ step :reserve_items do
122
+ run { |args| ... }
123
+ end
124
+ end
125
+
126
+ step :send_confirmation do
127
+ # Wait for all compose steps to complete
128
+ wait_for :update_customer_profile, :process_payment, :allocate_inventory
129
+
130
+ argument :customer_email, input(:customer_data)
131
+ argument :order_id, input(:order_id)
132
+ run { |args| ... }
133
+ end
134
+ end
135
+ ```
136
+
137
+ ### Execution Flow
138
+
139
+ When you have multiple `compose` declarations:
140
+
141
+ 1. **Execution Order**: Composed reactors execute in topological order based on their dependencies. Dependencies are determined automatically when you reference results from other steps (using `result(:step_name)`), or explicitly using `wait_for`. If no dependencies exist.
142
+
143
+ 2. **Access Compose Results**: A compose step returns the final result of the composed reactor. By default, this is a hash containing all of its step results, unless the composed reactor uses the `returns` DSL to specify a custom return value:
144
+
145
+ ```ruby
146
+ step :final_step do
147
+ # Get the complete result hash from the composed reactor
148
+ argument :payment_result, result(:process_payment)
149
+
150
+ run { |args|
151
+ # args[:payment_result] contains the full hash:
152
+ # { authorize_payment: ..., capture_payment: ... }
153
+ #
154
+ # args[:payment_status] contains just the capture_payment result
155
+ }
156
+ end
157
+ ```
158
+
159
+ 3. **Async Execution**: If a composed reactor is marked with `async true`, execution will pause at that compose step, serialize the entire reactor context, and queue a background job. The worker will resume execution from that compose step and continue sequentially through remaining steps. Only one worker executes the main reactor at a time.
160
+
161
+ 4. **Shared Context**: All composed reactors share access to the parent reactor's inputs and results of previous steps and can be configured with different retry strategies.
162
+
163
+ ### Async Compose Execution Flow
164
+
165
+ When you mark a compose as async:
166
+
167
+ ```ruby
168
+ compose :process_payment do
169
+ async true
170
+ # ... steps
171
+ end
172
+ ```
173
+
174
+ The execution flow is:
175
+
176
+ 1. Parent reactor executes steps up to `process_payment`
177
+ 2. Serializes entire context and queues a background job
178
+ 3. Returns `AsyncResult` to caller
179
+ 4. Worker picks up job and resumes from `process_payment`
180
+ 5. After `process_payment` completes, continues to next step sequentially
181
+ 6. If another async step is encountered, the process repeats
182
+
183
+ This ensures proper ordering and state consistency across async boundaries.
184
+
185
+ ## Nested Async Retries
186
+
187
+ One of the powerful features of composition in RubyReactor is the handling of asynchronous retries within nested reactors.
188
+
189
+ When a step inside a composed reactor fails and is configured to retry asynchronously (e.g., via Sidekiq), RubyReactor ensures that the entire execution context is preserved.
190
+
191
+ 1. **Context Serialization**: The entire reactor tree, including the state of the parent reactor and the composed reactor, is serialized.
192
+ 2. **Resume on Retry**: When the retry job executes, it resumes execution exactly from the failed step within the composed reactor.
193
+ 3. **State Preservation**: All intermediate results and inputs from the parent reactor are available, ensuring that the composed reactor has everything it needs to complete.
194
+
195
+ This behavior works seamlessly whether you are using inline composition or class-based composition.
196
+
197
+ ## Inspection
198
+
199
+ The execution state of composed reactors is stored in the parent context under `composed_contexts`. This allows for inspection of the full execution tree, which is useful for debugging and building monitoring tools.