ruby_reactor 0.7.0 → 0.8.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.
- checksums.yaml +4 -4
- data/.claude/skills/demo-app-e2e-verify/SKILL.md +226 -0
- data/.claude/skills/speckit-demo-tests/SKILL.md +144 -0
- data/.release-please-manifest.json +1 -1
- data/.specify/feature.json +1 -1
- data/.specify/memory/constitution.md +92 -15
- data/.specify/templates/plan-template.md +4 -0
- data/.specify/templates/tasks-template.md +8 -1
- data/CHANGELOG.md +151 -0
- data/CLAUDE.md +2 -2
- data/README.md +149 -40
- data/lib/ruby_reactor/context.rb +9 -2
- data/lib/ruby_reactor/context_serializer.rb +13 -0
- data/lib/ruby_reactor/dsl/interrupt_builder.rb +6 -0
- data/lib/ruby_reactor/dsl/lockable.rb +2 -2
- data/lib/ruby_reactor/dsl/reactor.rb +36 -18
- data/lib/ruby_reactor/dsl/step_builder.rb +95 -2
- data/lib/ruby_reactor/dsl/template_helpers.rb +13 -5
- data/lib/ruby_reactor/dsl/validation_helpers.rb +17 -0
- data/lib/ruby_reactor/error/input_validation_error.rb +4 -0
- data/lib/ruby_reactor/error/step_failure_error.rb +10 -3
- data/lib/ruby_reactor/executor/compensation_manager.rb +58 -45
- data/lib/ruby_reactor/executor/ordered_lock_support.rb +9 -9
- data/lib/ruby_reactor/executor/result_handler.rb +44 -13
- data/lib/ruby_reactor/executor/retry_manager.rb +6 -2
- data/lib/ruby_reactor/executor/step_executor.rb +27 -14
- data/lib/ruby_reactor/executor.rb +20 -15
- data/lib/ruby_reactor/map/element_executor.rb +7 -1
- data/lib/ruby_reactor/map/helpers.rb +9 -7
- data/lib/ruby_reactor/map/result_enumerator.rb +2 -0
- data/lib/ruby_reactor/map/sweeper.rb +1 -1
- data/lib/ruby_reactor/max_retries_exhausted_failure.rb +3 -2
- data/lib/ruby_reactor/open_telemetry.rb +7 -4
- data/lib/ruby_reactor/ordered_lock.rb +3 -3
- data/lib/ruby_reactor/reactor.rb +9 -12
- data/lib/ruby_reactor/rspec/matchers.rb +64 -17
- data/lib/ruby_reactor/rspec/test_subject.rb +8 -8
- data/lib/ruby_reactor/step/async_reactor_step.rb +159 -162
- data/lib/ruby_reactor/step/compose_step.rb +56 -75
- data/lib/ruby_reactor/step/input_contract.rb +128 -0
- data/lib/ruby_reactor/step/map_step.rb +178 -215
- data/lib/ruby_reactor/step.rb +119 -18
- data/lib/ruby_reactor/step_signals.rb +37 -0
- data/lib/ruby_reactor/step_worker.rb +25 -10
- data/lib/ruby_reactor/storage/adapter.rb +4 -0
- data/lib/ruby_reactor/storage/redis_adapter.rb +1 -72
- data/lib/ruby_reactor/storage/redis_reactor_scan.rb +116 -0
- data/lib/ruby_reactor/template/result.rb +9 -2
- data/lib/ruby_reactor/utils/fetch_indifferent.rb +13 -0
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/api.rb +23 -6
- data/lib/ruby_reactor/web/public/assets/index-BQvIWPdx.css +1 -0
- data/lib/ruby_reactor/web/public/assets/index-Dw4KV4QY.js +22 -0
- data/lib/ruby_reactor/web/public/index.html +2 -2
- data/lib/ruby_reactor.rb +61 -9
- data/specs/002-step-input-contracts/checklists/requirements.md +49 -0
- data/specs/002-step-input-contracts/contracts/dsl-surface.md +193 -0
- data/specs/002-step-input-contracts/data-model.md +115 -0
- data/specs/002-step-input-contracts/plan.md +165 -0
- data/specs/002-step-input-contracts/quickstart.md +170 -0
- data/specs/002-step-input-contracts/research.md +233 -0
- data/specs/002-step-input-contracts/spec.md +359 -0
- data/specs/002-step-input-contracts/tasks.md +367 -0
- data/specs/004-inheritable-step-class/checklists/requirements.md +40 -0
- data/specs/004-inheritable-step-class/contracts/step-lifecycle.md +85 -0
- data/specs/004-inheritable-step-class/data-model.md +116 -0
- data/specs/004-inheritable-step-class/plan.md +174 -0
- data/specs/004-inheritable-step-class/quickstart.md +112 -0
- data/specs/004-inheritable-step-class/research.md +308 -0
- data/specs/004-inheritable-step-class/spec.md +316 -0
- data/specs/004-inheritable-step-class/tasks.md +258 -0
- data/specs/deferred-003-step-lock-declarations/checklists/requirements.md +51 -0
- data/specs/deferred-003-step-lock-declarations/contracts/dsl-surface.md +154 -0
- data/specs/deferred-003-step-lock-declarations/data-model.md +131 -0
- data/specs/deferred-003-step-lock-declarations/plan.md +166 -0
- data/specs/deferred-003-step-lock-declarations/quickstart.md +169 -0
- data/specs/deferred-003-step-lock-declarations/research.md +196 -0
- data/specs/deferred-003-step-lock-declarations/spec.md +447 -0
- data/specs/deferred-003-step-lock-declarations/tasks.md +572 -0
- data/specs/possible_feature.md +22 -0
- metadata +34 -11
- data/lib/ruby_reactor/web/public/assets/index-B46p-M6K.css +0 -1
- data/lib/ruby_reactor/web/public/assets/index-DPmP4yXT.js +0 -22
- data/specs/001-background-async-steps/checklists/requirements.md +0 -39
- data/specs/001-background-async-steps/contracts/public-dsl.md +0 -154
- data/specs/001-background-async-steps/data-model.md +0 -117
- data/specs/001-background-async-steps/plan.md +0 -168
- data/specs/001-background-async-steps/quickstart.md +0 -102
- data/specs/001-background-async-steps/research.md +0 -150
- data/specs/001-background-async-steps/spec.md +0 -146
- data/specs/001-background-async-steps/tasks.md +0 -271
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RubyReactor
|
|
4
|
-
|
|
4
|
+
class Step
|
|
5
5
|
# The dispatching half of `async_reactor`: everything that happens in the
|
|
6
6
|
# PARENT's process. The child then runs as an ordinary independently
|
|
7
7
|
# dispatched reactor execution — no new enqueue primitive, no new storage
|
|
@@ -14,193 +14,190 @@ module RubyReactor
|
|
|
14
14
|
# validates, so skipping here starts a child on garbage), assign the
|
|
15
15
|
# ordered-lock nonce at ENQUEUE time (so ordering matches caller order), and
|
|
16
16
|
# persist before enqueueing (F2).
|
|
17
|
-
class AsyncReactorStep
|
|
18
|
-
|
|
17
|
+
class AsyncReactorStep < RubyReactor::Step
|
|
18
|
+
def run
|
|
19
|
+
child_class = inputs[:async_reactor_class]
|
|
20
|
+
child_inputs = build_child_inputs(inputs[:argument_mappings] || {})
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
# A dispatch-time failure fails the DISPATCHING step, i.e. normal saga
|
|
23
|
+
# handling in the parent. That is deliberately outside the
|
|
24
|
+
# no-auto-compensation rule, which governs the child's own execution.
|
|
25
|
+
validation = validate_child_inputs(child_class, child_inputs)
|
|
26
|
+
return validation if validation
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# no-auto-compensation rule, which governs the child's own execution.
|
|
28
|
-
validation = validate_child_inputs(child_class, child_inputs)
|
|
29
|
-
return validation if validation
|
|
28
|
+
deadlock = detect_lock_deadlock(child_class, child_inputs)
|
|
29
|
+
return deadlock if deadlock
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
return deadlock if deadlock
|
|
31
|
+
return run_inline(child_class, child_inputs) if run_inline?
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
dispatch(child_class, child_inputs)
|
|
34
|
+
end
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
end
|
|
36
|
+
private
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
def build_child_inputs(mappings)
|
|
39
|
+
mappings.transform_values { |source| source.resolve(context) }
|
|
40
|
+
end
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
end
|
|
42
|
+
def validate_child_inputs(child_class, child_inputs)
|
|
43
|
+
return nil unless child_class.respond_to?(:validate_inputs)
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
result = child_class.validate_inputs(child_inputs)
|
|
46
|
+
return nil unless result.failure?
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
RubyReactor.Failure(
|
|
49
|
+
"async_reactor child #{child_class.name} rejected its inputs: #{result.error.message}",
|
|
50
|
+
validation_errors: (result.error.field_errors if result.error.respond_to?(:field_errors))
|
|
51
|
+
)
|
|
52
|
+
end
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
# Lock ownership is NOT shared across the async boundary: parent
|
|
55
|
+
# and child run concurrently, so giving the child the parent's owner
|
|
56
|
+
# would put both inside the critical section at once — mutual exclusion
|
|
57
|
+
# silently broken, which is worse than a stall. Instead the one
|
|
58
|
+
# GUARANTEED deadlock (the parent holds a key its child will wait on,
|
|
59
|
+
# while the parent may go on to wait for that child) is caught here,
|
|
60
|
+
# loudly, at dispatch. Ordinary cross-execution contention on the same
|
|
61
|
+
# key is unaffected and still snoozes normally.
|
|
62
|
+
def detect_lock_deadlock(child_class, child_inputs)
|
|
63
|
+
held = held_lock_keys
|
|
64
|
+
return nil if held.empty?
|
|
65
|
+
|
|
66
|
+
collision = child_lock_keys(child_class, child_inputs).find { |key| held.include?(key) }
|
|
67
|
+
return nil unless collision
|
|
68
|
+
|
|
69
|
+
RubyReactor.Failure(deadlock_message(collision, child_class))
|
|
70
|
+
end
|
|
56
71
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# GUARANTEED deadlock (the parent holds a key its child will wait on,
|
|
62
|
-
# while the parent may go on to wait for that child) is caught here,
|
|
63
|
-
# loudly, at dispatch. Ordinary cross-execution contention on the same
|
|
64
|
-
# key is unaffected and still snoozes normally.
|
|
65
|
-
def detect_lock_deadlock(child_class, child_inputs, context)
|
|
66
|
-
held = held_lock_keys(context)
|
|
67
|
-
return nil if held.empty?
|
|
68
|
-
|
|
69
|
-
collision = child_lock_keys(child_class, child_inputs).find { |key| held.include?(key) }
|
|
70
|
-
return nil unless collision
|
|
71
|
-
|
|
72
|
-
RubyReactor.Failure(deadlock_message(collision, child_class, context))
|
|
72
|
+
def child_lock_keys(child_class, child_inputs)
|
|
73
|
+
keys = []
|
|
74
|
+
if child_class.respond_to?(:lock_config) && child_class.lock_config
|
|
75
|
+
keys << child_class.lock_config[:key_proc].call(child_inputs)
|
|
73
76
|
end
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# A semaphore with a single slot has the identical circular-wait
|
|
82
|
-
# shape. Higher limits are contention, not guaranteed deadlock.
|
|
83
|
-
semaphore = child_class.respond_to?(:semaphore_config) ? child_class.semaphore_config : nil
|
|
84
|
-
keys << semaphore[:key_proc].call(child_inputs) if semaphore && semaphore[:limit] == 1
|
|
78
|
+
# A semaphore with a single slot has the identical circular-wait
|
|
79
|
+
# shape. Higher limits are contention, not guaranteed deadlock.
|
|
80
|
+
semaphore = child_class.respond_to?(:semaphore_config) ? child_class.semaphore_config : nil
|
|
81
|
+
keys << semaphore[:key_proc].call(child_inputs) if semaphore && semaphore[:limit] == 1
|
|
85
82
|
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
keys.compact
|
|
84
|
+
end
|
|
88
85
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
def held_lock_keys
|
|
87
|
+
root = context.root_context || context
|
|
88
|
+
Array(root.private_data[:held_lock_keys] || root.private_data["held_lock_keys"])
|
|
89
|
+
end
|
|
93
90
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
91
|
+
def deadlock_message(key, child_class)
|
|
92
|
+
parent = context.reactor_class&.name || "the dispatching reactor"
|
|
93
|
+
<<~MSG.strip
|
|
94
|
+
async_reactor dispatch of #{child_class.name || "<anonymous>"} would deadlock: it declares the \
|
|
95
|
+
lock key '#{key}', which #{parent} currently holds and will not release until it finishes.
|
|
96
|
+
The child would snooze forever, and if #{parent} later reads this child's result it would wait \
|
|
97
|
+
for work that can never start. Lock ownership is never shared across the async boundary — the \
|
|
98
|
+
two run concurrently, so sharing it would break mutual exclusion outright.
|
|
99
|
+
Fix, in order of preference:
|
|
100
|
+
1. Use `compose` instead of `async_reactor` if the child belongs inside #{parent}'s critical \
|
|
101
|
+
section and its result is needed — waiting for it means the work is sequential anyway.
|
|
102
|
+
2. Narrow the lock keys, if parent and child actually protect different resources.
|
|
103
|
+
3. Restructure so the locked reactor never reads the child's result — fire-and-forget, and \
|
|
104
|
+
verify in the child itself or in a successor reactor outside the lock window.
|
|
105
|
+
MSG
|
|
106
|
+
end
|
|
110
107
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
108
|
+
# `RSpec::TestSubject`'s `async: false` clears the dispatch marker to run
|
|
109
|
+
# the whole reactor in one process.
|
|
110
|
+
def run_inline?
|
|
111
|
+
config = context.reactor_class&.steps&.[](context.current_step)
|
|
112
|
+
config.respond_to?(:async_dispatch?) && !config.async_dispatch?
|
|
113
|
+
end
|
|
117
114
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
115
|
+
def run_inline(child_class, child_inputs)
|
|
116
|
+
result = child_class.run(child_inputs)
|
|
117
|
+
store_reference(
|
|
118
|
+
execution_id: (result.execution_id if result.respond_to?(:execution_id)), child_class: child_class
|
|
119
|
+
)
|
|
120
|
+
# Success(nil) for the same reason dispatch returns it: `result(:name)`
|
|
121
|
+
# must route through the reference (which now points at an already
|
|
122
|
+
# terminal child) rather than through a recorded value.
|
|
123
|
+
RubyReactor.Success(nil)
|
|
124
|
+
end
|
|
127
125
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
126
|
+
def dispatch(child_class, child_inputs)
|
|
127
|
+
child_context = build_child_context(child_class, child_inputs)
|
|
128
|
+
assign_ordered_lock_nonce!(child_class, child_context)
|
|
129
|
+
|
|
130
|
+
# Persist BEFORE enqueue (F2) — the payload is identity-only.
|
|
131
|
+
child_context.status = :running
|
|
132
|
+
# Marks the child as owned by a job of its own, which is what makes it
|
|
133
|
+
# sweepable: a compose child carries the same parent_context_id but runs
|
|
134
|
+
# inline in the parent, so re-enqueueing one would double-run it.
|
|
135
|
+
child_context.private_data[:async_dispatched] = true
|
|
136
|
+
save(child_context, child_class)
|
|
137
|
+
|
|
138
|
+
# The reference is written synchronously, by the process that is about
|
|
139
|
+
# to keep running other steps, so there is no cross-process race on it
|
|
140
|
+
# (unlike the child's eventual result, which the child itself writes).
|
|
141
|
+
store_reference(execution_id: child_context.context_id, child_class: child_class)
|
|
142
|
+
log_dispatch(child_class, child_context)
|
|
143
|
+
|
|
144
|
+
RubyReactor.configuration.async_router.perform_async(
|
|
145
|
+
child_context.context_id, RubyReactor.reactor_storage_name(child_class)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
RubyReactor.Success(nil)
|
|
149
|
+
end
|
|
152
150
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
151
|
+
def build_child_context(child_class, child_inputs)
|
|
152
|
+
child_context = RubyReactor::Context.new(child_inputs, child_class)
|
|
153
|
+
# Linked for traceability only — the child is NOT nested inside the
|
|
154
|
+
# parent's context tree, because it must survive the parent finishing.
|
|
155
|
+
child_context.parent_context_id = context.context_id
|
|
156
|
+
child_context
|
|
157
|
+
end
|
|
160
158
|
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
def assign_ordered_lock_nonce!(child_class, child_context)
|
|
160
|
+
return unless child_class.respond_to?(:ordered_lock_config) && child_class.ordered_lock_config
|
|
163
161
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
162
|
+
config = child_class.ordered_lock_config
|
|
163
|
+
key = config[:key_proc].call(child_context.inputs)
|
|
164
|
+
nonce, epoch = RubyReactor::OrderedLock.assign(key, ttl: config[:ttl])
|
|
167
165
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
166
|
+
child_context.private_data[:ordered_lock] = {
|
|
167
|
+
key: key, nonce: nonce, epoch: epoch,
|
|
168
|
+
poison_pill_timeout: config[:poison_pill_timeout],
|
|
169
|
+
ttl: config[:ttl], strict: config.fetch(:strict, true)
|
|
170
|
+
}
|
|
171
|
+
end
|
|
174
172
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
173
|
+
def store_reference(execution_id:, child_class:)
|
|
174
|
+
context.composed_contexts[context.current_step] = {
|
|
175
|
+
name: context.current_step,
|
|
176
|
+
type: :async_reactor_ref,
|
|
177
|
+
execution_id: execution_id,
|
|
178
|
+
reactor_class_name: RubyReactor.reactor_storage_name(child_class),
|
|
179
|
+
dispatched_at: Time.now
|
|
180
|
+
}
|
|
181
|
+
end
|
|
184
182
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
183
|
+
def save(child_context, child_class)
|
|
184
|
+
RubyReactor.configuration.storage_adapter.store_context(
|
|
185
|
+
child_context.context_id,
|
|
186
|
+
RubyReactor::ContextSerializer.serialize(child_context),
|
|
187
|
+
RubyReactor.reactor_storage_name(child_class)
|
|
188
|
+
)
|
|
189
|
+
end
|
|
192
190
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
end
|
|
191
|
+
# The link between parent and child, machine-parseable. Matters
|
|
192
|
+
# most here — a fire-and-forget child's failure may have no other surface
|
|
193
|
+
# in the parent at all.
|
|
194
|
+
def log_dispatch(child_class, child_context)
|
|
195
|
+
RubyReactor.configuration.logger.info(
|
|
196
|
+
"event=\"ruby_reactor.async_reactor.dispatched\" " \
|
|
197
|
+
"reactor=#{context.reactor_class&.name.inspect} step=#{context.current_step.inspect} " \
|
|
198
|
+
"execution_id=#{context.context_id.inspect} child_reactor=#{child_class.name.inspect} " \
|
|
199
|
+
"child_execution_id=#{child_context.context_id.inspect}"
|
|
200
|
+
)
|
|
204
201
|
end
|
|
205
202
|
end
|
|
206
203
|
end
|
|
@@ -1,121 +1,102 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RubyReactor
|
|
4
|
-
|
|
5
|
-
class ComposeStep
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
attr_reader :composed_reactor_class, :argument_mappings
|
|
9
|
-
|
|
10
|
-
def initialize(composed_reactor_class, argument_mappings = {})
|
|
11
|
-
@composed_reactor_class = composed_reactor_class
|
|
12
|
-
@argument_mappings = argument_mappings
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.run(arguments, context)
|
|
4
|
+
class Step
|
|
5
|
+
class ComposeStep < RubyReactor::Step
|
|
6
|
+
def run
|
|
16
7
|
step_name = context.current_step
|
|
17
8
|
composed_data = context.composed_contexts[step_name]
|
|
18
|
-
child_context = prepare_child_context(
|
|
9
|
+
child_context = prepare_child_context(composed_data)
|
|
19
10
|
|
|
20
11
|
# Store the child context in composed_contexts BEFORE execution
|
|
21
|
-
store_child_context(
|
|
12
|
+
store_child_context(step_name, child_context)
|
|
22
13
|
|
|
23
14
|
# Execute the composed reactor
|
|
24
|
-
result = execute_child_reactor(
|
|
15
|
+
result = execute_child_reactor(inputs[:composed_reactor_class], child_context, composed_data)
|
|
25
16
|
|
|
26
17
|
# Update the stored context
|
|
27
|
-
store_child_context(
|
|
18
|
+
store_child_context(step_name, child_context)
|
|
28
19
|
|
|
29
20
|
handle_execution_result(result)
|
|
30
21
|
end
|
|
31
22
|
|
|
32
|
-
|
|
23
|
+
# Compensating a failed compose and undoing a completed one are the same
|
|
24
|
+
# work: roll back whatever the child reactor completed.
|
|
25
|
+
def compensate
|
|
33
26
|
step_name = context.current_step
|
|
34
27
|
composed_data = context.composed_contexts[step_name]
|
|
35
28
|
return RubyReactor.Success() unless composed_data && composed_data[:context]
|
|
36
29
|
|
|
37
30
|
child_context = composed_data[:context]
|
|
38
|
-
executor = RubyReactor::Executor.new(
|
|
31
|
+
executor = RubyReactor::Executor.new(inputs[:composed_reactor_class], {}, child_context)
|
|
39
32
|
executor.undo_all
|
|
40
33
|
executor.save_context
|
|
41
34
|
|
|
42
35
|
RubyReactor.Success()
|
|
43
36
|
end
|
|
44
37
|
|
|
45
|
-
|
|
46
|
-
step_name = context.current_step
|
|
47
|
-
composed_data = context.composed_contexts[step_name]
|
|
48
|
-
return RubyReactor.Success() unless composed_data && composed_data[:context]
|
|
38
|
+
alias undo compensate
|
|
49
39
|
|
|
50
|
-
|
|
51
|
-
executor = RubyReactor::Executor.new(arguments[:composed_reactor_class], {}, child_context)
|
|
52
|
-
executor.undo_all
|
|
53
|
-
executor.save_context
|
|
40
|
+
private
|
|
54
41
|
|
|
55
|
-
|
|
56
|
-
|
|
42
|
+
def build_composed_inputs(mappings)
|
|
43
|
+
built = {}
|
|
57
44
|
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
mappings.each do |composed_input_name, source|
|
|
46
|
+
value = source.resolve(context)
|
|
47
|
+
built[composed_input_name] = value
|
|
48
|
+
end
|
|
60
49
|
|
|
61
|
-
|
|
62
|
-
|
|
50
|
+
built
|
|
51
|
+
end
|
|
63
52
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
inputs[composed_input_name] = value
|
|
67
|
-
end
|
|
53
|
+
def prepare_child_context(composed_data)
|
|
54
|
+
child_context = composed_data ? composed_data[:context] : nil
|
|
68
55
|
|
|
69
|
-
|
|
56
|
+
unless child_context
|
|
57
|
+
composed_inputs = build_composed_inputs(inputs[:argument_mappings] || {})
|
|
58
|
+
child_context = RubyReactor::Context.new(composed_inputs, inputs[:composed_reactor_class])
|
|
70
59
|
end
|
|
71
60
|
|
|
72
|
-
|
|
73
|
-
|
|
61
|
+
link_contexts(child_context, context)
|
|
62
|
+
child_context
|
|
63
|
+
end
|
|
74
64
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
65
|
+
def link_contexts(child_context, parent_context)
|
|
66
|
+
child_context.parent_context = parent_context
|
|
67
|
+
child_context.root_context = parent_context.root_context || parent_context
|
|
68
|
+
child_context.inline_async_execution = parent_context.inline_async_execution
|
|
69
|
+
end
|
|
79
70
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
def store_child_context(step_name, child_context)
|
|
72
|
+
context.composed_contexts[step_name] = {
|
|
73
|
+
name: step_name,
|
|
74
|
+
type: :composed,
|
|
75
|
+
context: child_context
|
|
76
|
+
}
|
|
77
|
+
end
|
|
83
78
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
child_context.root_context = parent_context.root_context || parent_context
|
|
87
|
-
child_context.inline_async_execution = parent_context.inline_async_execution
|
|
88
|
-
end
|
|
79
|
+
def execute_child_reactor(composed_reactor, child_context, composed_data)
|
|
80
|
+
executor = RubyReactor::Executor.new(composed_reactor, {}, child_context)
|
|
89
81
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
context: child_context
|
|
95
|
-
}
|
|
82
|
+
if composed_data && child_context.current_step
|
|
83
|
+
executor.resume_execution
|
|
84
|
+
else
|
|
85
|
+
executor.execute
|
|
96
86
|
end
|
|
97
87
|
|
|
98
|
-
|
|
99
|
-
|
|
88
|
+
executor.result
|
|
89
|
+
end
|
|
100
90
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
else
|
|
104
|
-
executor.execute
|
|
105
|
-
end
|
|
91
|
+
def handle_execution_result(result)
|
|
92
|
+
return result if result.is_a?(RubyReactor::DispatchResult) || result.is_a?(RubyReactor::RetryQueuedResult)
|
|
106
93
|
|
|
107
|
-
|
|
108
|
-
|
|
94
|
+
# Hand the child's Failure through untouched: rebuilding it from
|
|
95
|
+
# `error` alone drops validation_errors, retryability and the rest of
|
|
96
|
+
# the metadata the direct and async paths do propagate.
|
|
97
|
+
return result if result.is_a?(RubyReactor::Failure)
|
|
109
98
|
|
|
110
|
-
|
|
111
|
-
return result if result.is_a?(RubyReactor::DispatchResult) || result.is_a?(RubyReactor::RetryQueuedResult)
|
|
112
|
-
|
|
113
|
-
if result.success?
|
|
114
|
-
RubyReactor.Success(result.value)
|
|
115
|
-
else
|
|
116
|
-
RubyReactor.Failure(result.error)
|
|
117
|
-
end
|
|
118
|
-
end
|
|
99
|
+
result.success? ? RubyReactor.Success(result.value) : RubyReactor.Failure(result.error)
|
|
119
100
|
end
|
|
120
101
|
end
|
|
121
102
|
end
|