ruby_reactor 0.8.4 → 0.8.5
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 +196 -0
- data/CLAUDE.md +1 -1
- data/README.md +47 -11
- data/lib/ruby_reactor/dsl/async_macros.rb +30 -1
- data/lib/ruby_reactor/dsl/async_reactor_builder.rb +12 -6
- data/lib/ruby_reactor/dsl/compose_builder.rb +12 -6
- data/lib/ruby_reactor/dsl/interrupt_builder.rb +1 -3
- data/lib/ruby_reactor/dsl/map_builder.rb +0 -2
- data/lib/ruby_reactor/dsl/step_builder.rb +91 -19
- data/lib/ruby_reactor/error/argument_resolution_error.rb +19 -0
- data/lib/ruby_reactor/error/rescuable.rb +28 -0
- data/lib/ruby_reactor/executor/compensation_manager.rb +30 -26
- data/lib/ruby_reactor/executor/result_handler.rb +18 -16
- data/lib/ruby_reactor/executor/step_coordination.rb +11 -8
- data/lib/ruby_reactor/executor/step_executor.rb +59 -49
- data/lib/ruby_reactor/executor.rb +38 -4
- data/lib/ruby_reactor/map/collector.rb +21 -11
- data/lib/ruby_reactor/map/dispatcher.rb +29 -3
- data/lib/ruby_reactor/map/element_executor.rb +9 -3
- data/lib/ruby_reactor/map/helpers.rb +32 -2
- data/lib/ruby_reactor/map/result_enumerator.rb +18 -12
- data/lib/ruby_reactor/reactor.rb +24 -0
- data/lib/ruby_reactor/rspec/matchers.rb +19 -3
- data/lib/ruby_reactor/step/compose_step.rb +7 -1
- data/lib/ruby_reactor/step/map_step.rb +109 -4
- data/lib/ruby_reactor/step.rb +7 -0
- data/lib/ruby_reactor/step_worker.rb +46 -22
- data/lib/ruby_reactor/storage/adapter.rb +4 -0
- data/lib/ruby_reactor/storage/redis_adapter.rb +9 -0
- data/lib/ruby_reactor/storage/redis_reactor_scan.rb +1 -1
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/api.rb +1 -1
- data/lib/ruby_reactor/web/public/assets/{index-CeZU-ESu.js → index-CQbgHtd0.js} +10 -10
- data/lib/ruby_reactor/web/public/index.html +1 -1
- data/lib/ruby_reactor/worker.rb +3 -1
- data/lib/ruby_reactor.rb +17 -6
- data/specs/007-execution-flow-analysis/analysis/README.md +147 -0
- data/specs/007-execution-flow-analysis/analysis/execution-order.md +359 -0
- data/specs/007-execution-flow-analysis/analysis/findings-and-options.md +502 -0
- data/specs/007-execution-flow-analysis/analysis/invariants.md +109 -0
- data/specs/007-execution-flow-analysis/checklists/requirements.md +39 -0
- data/specs/007-execution-flow-analysis/contracts/report-structure.md +71 -0
- data/specs/007-execution-flow-analysis/data-model.md +83 -0
- data/specs/007-execution-flow-analysis/evidence/harness.rb +229 -0
- data/specs/007-execution-flow-analysis/evidence/output.txt +333 -0
- data/specs/007-execution-flow-analysis/evidence/probes/01_plain.rb +122 -0
- data/specs/007-execution-flow-analysis/evidence/probes/02_compose.rb +182 -0
- data/specs/007-execution-flow-analysis/evidence/probes/03_map.rb +232 -0
- data/specs/007-execution-flow-analysis/evidence/probes/04_async.rb +132 -0
- data/specs/007-execution-flow-analysis/evidence/probes/05_background.rb +58 -0
- data/specs/007-execution-flow-analysis/evidence/probes/06_coordination.rb +158 -0
- data/specs/007-execution-flow-analysis/evidence/probes/07_interrupts_manual.rb +185 -0
- data/specs/007-execution-flow-analysis/evidence/run.rb +15 -0
- data/specs/007-execution-flow-analysis/plan.md +127 -0
- data/specs/007-execution-flow-analysis/quickstart.md +51 -0
- data/specs/007-execution-flow-analysis/research.md +202 -0
- data/specs/007-execution-flow-analysis/spec.md +270 -0
- data/specs/007-execution-flow-analysis/tasks.md +257 -0
- data/specs/008-rollback-reliability/checklists/requirements.md +43 -0
- data/specs/008-rollback-reliability/contracts/api-surface.md +126 -0
- data/specs/008-rollback-reliability/contracts/rollback-semantics.md +76 -0
- data/specs/008-rollback-reliability/data-model.md +139 -0
- data/specs/008-rollback-reliability/plan.md +233 -0
- data/specs/008-rollback-reliability/quickstart.md +105 -0
- data/specs/008-rollback-reliability/research.md +653 -0
- data/specs/008-rollback-reliability/spec.md +561 -0
- data/specs/008-rollback-reliability/tasks.md +1110 -0
- data/specs/future_improvements.md +48 -0
- metadata +35 -2
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# map: research H14–H18, user questions Q1 and Q3. Every element reactor runs
|
|
4
|
+
# e1 → e2 and both declare undo/compensate; element i == 2 fails at e2.
|
|
5
|
+
|
|
6
|
+
module P
|
|
7
|
+
FAILS_AT_TWO = ->(inputs) { inputs.i == 2 }
|
|
8
|
+
ITEMS = { items: [0, 1, 2, 3] }.freeze
|
|
9
|
+
|
|
10
|
+
class Elem < Base
|
|
11
|
+
tag "e"
|
|
12
|
+
input :i
|
|
13
|
+
input :from_a, optional: true
|
|
14
|
+
pstep :e1, idx: true
|
|
15
|
+
pstep :e2, after: :e1, idx: true, fail: FAILS_AT_TWO
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class ElemOk < Base
|
|
19
|
+
tag "e"
|
|
20
|
+
input :i
|
|
21
|
+
input :from_a, optional: true
|
|
22
|
+
pstep :e1, idx: true
|
|
23
|
+
pstep :e2, after: :e1, idx: true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Shared parent shape: a → map :m → b. Fan-out, fail_fast and whether b
|
|
27
|
+
# fails vary per probe. (Parameter names must not shadow the MapBuilder
|
|
28
|
+
# methods `fan_out` / `fail_fast` / `element` used inside the block.)
|
|
29
|
+
def self.map_parent(name, element_class, fanned: false, tolerant: false, b_fails: false)
|
|
30
|
+
klass = Class.new(Base) do
|
|
31
|
+
input :items
|
|
32
|
+
pstep :a
|
|
33
|
+
map :m, element_class do
|
|
34
|
+
source input(:items)
|
|
35
|
+
argument :i, element(:m)
|
|
36
|
+
argument :from_a, result(:a)
|
|
37
|
+
fan_out(fanned)
|
|
38
|
+
fail_fast(!tolerant)
|
|
39
|
+
end
|
|
40
|
+
pstep :b, after: :m, fail: b_fails
|
|
41
|
+
end
|
|
42
|
+
const_set(name, klass)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
map_parent(:Map01, Elem)
|
|
46
|
+
map_parent(:Map02, Elem, tolerant: true)
|
|
47
|
+
map_parent(:Map03, ElemOk, b_fails: true)
|
|
48
|
+
map_parent(:Map04, Elem, fanned: true)
|
|
49
|
+
map_parent(:Map05, Elem, fanned: true, tolerant: true)
|
|
50
|
+
map_parent(:Map06, ElemOk, fanned: true, b_fails: true)
|
|
51
|
+
|
|
52
|
+
class ElemRetry < Base
|
|
53
|
+
tag "e"
|
|
54
|
+
input :i
|
|
55
|
+
input :from_a, optional: true
|
|
56
|
+
pstep :e1, idx: true
|
|
57
|
+
pstep :e2, after: :e1, idx: true, fail_times: 1, retries: { max_attempts: 2, base_delay: 0 }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
map_parent(:Map09, ElemRetry, fanned: true)
|
|
61
|
+
map_parent(:Map10, ElemRetry)
|
|
62
|
+
|
|
63
|
+
class ElemHalts < Base
|
|
64
|
+
tag "e"
|
|
65
|
+
input :i
|
|
66
|
+
input :from_a, optional: true
|
|
67
|
+
pstep :e1, idx: true
|
|
68
|
+
pstep :e2, after: :e1, idx: true do
|
|
69
|
+
run do |inputs, _ctx|
|
|
70
|
+
Probe.rec("run:e.e2[#{inputs.i}]")
|
|
71
|
+
inputs.i == 1 ? RubyReactor.Halt(reason: "element done early") : RubyReactor.Success(inputs.i)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
map_parent(:Map11, ElemHalts)
|
|
77
|
+
|
|
78
|
+
class ElemWithUnit < Base
|
|
79
|
+
tag "e"
|
|
80
|
+
input :i
|
|
81
|
+
input :from_a, optional: true
|
|
82
|
+
pstep :e1, idx: true
|
|
83
|
+
pstep :u, kind: :async_step, after: :e1, idx: true
|
|
84
|
+
pstep :e2, after: :e1, idx: true, fail: ->(inputs) { inputs.i == 1 }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
map_parent(:Map12, ElemWithUnit, fanned: true)
|
|
88
|
+
|
|
89
|
+
class ChildWithMap < Base
|
|
90
|
+
tag "child"
|
|
91
|
+
input :items
|
|
92
|
+
pstep :c0
|
|
93
|
+
map :m, Elem do
|
|
94
|
+
source input(:items)
|
|
95
|
+
argument :i, element(:m)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class Map07 < Base
|
|
100
|
+
input :items
|
|
101
|
+
pstep :a
|
|
102
|
+
compose(:child, ChildWithMap) { argument :items, input(:items) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class Kid < Base
|
|
106
|
+
tag "k"
|
|
107
|
+
pstep :k1
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
class ElemComposes < Base
|
|
111
|
+
tag "e"
|
|
112
|
+
input :i
|
|
113
|
+
pstep :e1, idx: true
|
|
114
|
+
compose :kid, Kid
|
|
115
|
+
pstep :e2, after: %i[e1 kid], idx: true, fail: FAILS_AT_TWO
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
class Map08 < Base
|
|
119
|
+
input :items
|
|
120
|
+
map :m, ElemComposes do
|
|
121
|
+
source input(:items)
|
|
122
|
+
argument :i, element(:m)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def elements(*indexes, fail_at: nil)
|
|
128
|
+
indexes.flat_map do |i|
|
|
129
|
+
base = ["run:e.e1[#{i}]", "run:e.e2[#{i}]"]
|
|
130
|
+
i == fail_at ? base + ["compensate:e.e2[#{i}]", "undo:e.e1[#{i}]"] : base
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
Probe.scenario "S-map-01", "a → map(4 elems, inline, fail_fast; elem 2 fails) → b [Q1]",
|
|
135
|
+
mode: :inline,
|
|
136
|
+
expected: %w[run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2] run:e.e2[2]
|
|
137
|
+
compensate:e.e2[2] undo:e.e1[2] undo:e.e2[1] undo:e.e1[1] undo:e.e2[0] undo:e.e1[0]
|
|
138
|
+
undo:a => failure(m)] do
|
|
139
|
+
P::Map01.run(P::ITEMS)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
Probe.scenario "S-map-02", "a → map(inline, fail_fast false; elem 2 fails) → b",
|
|
143
|
+
mode: :inline,
|
|
144
|
+
expected: ["run:a", *elements(0, 1, 2, 3, fail_at: 2), "run:b", "=>", "success"] do
|
|
145
|
+
P::Map02.run(P::ITEMS)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
Probe.scenario "S-map-03", "a → map(inline, all ok) → b(fails) [Q1: later failure]",
|
|
149
|
+
mode: :inline,
|
|
150
|
+
expected: %w[run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2] run:e.e2[2]
|
|
151
|
+
run:e.e1[3] run:e.e2[3] run:b compensate:b undo:e.e2[3] undo:e.e1[3] undo:e.e2[2]
|
|
152
|
+
undo:e.e1[2] undo:e.e2[1] undo:e.e1[1] undo:e.e2[0] undo:e.e1[0] undo:a =>
|
|
153
|
+
failure(b)] do
|
|
154
|
+
P::Map03.run(P::ITEMS)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
Probe.scenario "S-map-04", "a → map(fan_out, fail_fast; elem 2 fails) → b",
|
|
158
|
+
mode: :worker,
|
|
159
|
+
expected: %w[run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2] run:e.e2[2]
|
|
160
|
+
compensate:e.e2[2] undo:e.e1[2] undo:e.e2[1] undo:e.e1[1] undo:e.e2[0] undo:e.e1[0]
|
|
161
|
+
undo:a => failure(m)] do
|
|
162
|
+
Probe.run_async(P::Map04, P::ITEMS)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
Probe.scenario "S-map-05", "a → map(fan_out, fail_fast false; elem 2 fails) → b",
|
|
166
|
+
mode: :worker,
|
|
167
|
+
expected: ["run:a", *elements(0, 1, 2, 3, fail_at: 2), "run:b", "=>", "success"] do
|
|
168
|
+
Probe.run_async(P::Map05, P::ITEMS)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
Probe.scenario "S-map-06", "a → map(fan_out, all ok) → b(fails)",
|
|
172
|
+
mode: :worker,
|
|
173
|
+
expected: %w[run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2] run:e.e2[2]
|
|
174
|
+
run:e.e1[3] run:e.e2[3] run:b compensate:b undo:e.e2[3] undo:e.e1[3] undo:e.e2[2]
|
|
175
|
+
undo:e.e1[2] undo:e.e2[1] undo:e.e1[1] undo:e.e2[0] undo:e.e1[0] undo:a =>
|
|
176
|
+
failure(b)] do
|
|
177
|
+
Probe.run_async(P::Map06, P::ITEMS)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
Probe.scenario "S-map-07", "a → compose(c0 → map(elem 2 fails))",
|
|
181
|
+
mode: :inline,
|
|
182
|
+
expected: %w[run:a run:child.c0 run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2]
|
|
183
|
+
run:e.e2[2] compensate:e.e2[2] undo:e.e1[2] undo:e.e2[1] undo:e.e1[1] undo:e.e2[0]
|
|
184
|
+
undo:e.e1[0] undo:child.c0 undo:a => failure(child)] do
|
|
185
|
+
P::Map07.run(P::ITEMS)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
Probe.scenario "S-map-08", "map(elem: e1 → compose(k1) → e2; elem 2 fails)",
|
|
189
|
+
mode: :inline,
|
|
190
|
+
expected: %w[run:e.e1[0] run:k.k1 run:e.e2[0] run:e.e1[1] run:k.k1 run:e.e2[1] run:e.e1[2]
|
|
191
|
+
run:k.k1 run:e.e2[2] compensate:e.e2[2] undo:k.k1 undo:e.e1[2] undo:e.e2[1]
|
|
192
|
+
undo:k.k1 undo:e.e1[1] undo:e.e2[0] undo:k.k1 undo:e.e1[0] => failure(m)] do
|
|
193
|
+
P::Map08.run(P::ITEMS)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
Probe.scenario "S-map-04b", "map(fan_out, fail_fast; elem 2 fails), element jobs performed 3,2,1,0",
|
|
197
|
+
mode: :worker,
|
|
198
|
+
expected: %w[run:a run:e.e1[3] run:e.e2[3] run:e.e1[2] run:e.e2[2] compensate:e.e2[2]
|
|
199
|
+
undo:e.e1[2] undo:e.e2[3] undo:e.e1[3] undo:a => failure(m)] do
|
|
200
|
+
dispatched = P::Map04.run(P::ITEMS)
|
|
201
|
+
RubyReactor::Adapters::Sidekiq::MapElementWorker.jobs.reverse!
|
|
202
|
+
Probe.drain
|
|
203
|
+
P::Map04.find(dispatched.execution_id)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
Probe.scenario "S-map-09", "a → map(fan_out; each e2 fails once, retries 2) → b",
|
|
207
|
+
mode: :worker,
|
|
208
|
+
expected: %w[run:a run:e.e1[0] run:e.e2[0] retry:e2#1 run:e.e1[1] run:e.e2[1] retry:e2#1
|
|
209
|
+
run:e.e2[0] run:e.e2[1] run:b => success] do
|
|
210
|
+
Probe.run_async(P::Map09, { items: [0, 1] })
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
Probe.scenario "S-map-10", "a → map(inline; each e2 fails once, retries 2) → b",
|
|
214
|
+
mode: :inline,
|
|
215
|
+
expected: %w[run:a run:e.e1[0] run:e.e2[0] retry:e2#1 run:e.e2[0] run:e.e1[1] run:e.e2[1] retry:e2#1
|
|
216
|
+
run:e.e2[1] run:b => success] do
|
|
217
|
+
P::Map10.run({ items: [0, 1] })
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
Probe.scenario "S-map-11", "a → map(inline; elem 1 returns Halt) → b",
|
|
221
|
+
mode: :inline, expected: %w[run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] => halt] do
|
|
222
|
+
P::Map11.run({ items: [0, 1, 2] })
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
Probe.scenario "S-map-12", "a → map(fan_out; element e1 → async_step u, e2; elem 1 fails) → b",
|
|
226
|
+
mode: :worker,
|
|
227
|
+
expected: %w[run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] compensate:e.e2[1]
|
|
228
|
+
undo:e.e1[1] undo:e.e2[0] undo:e.e1[0] undo:a run:e.u[0] run:e.u[1] => failure(m)] do
|
|
229
|
+
result = Probe.run_async(P::Map12, { items: [0, 1] })
|
|
230
|
+
Probe.note("StepWorker jobs left after drain: #{RubyReactor::Adapters::Sidekiq::StepWorker.jobs.size}")
|
|
231
|
+
result
|
|
232
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# async_step / async_reactor: research H19–H22. Parents run inline; units run
|
|
4
|
+
# in their own jobs. Reader scenarios use Sidekiq inline! (the unit completes at
|
|
5
|
+
# enqueue) because a same-process reader in fake mode would wait out
|
|
6
|
+
# async_wait_timeout for a job nobody performs.
|
|
7
|
+
|
|
8
|
+
module P
|
|
9
|
+
# r reads result(:u); opts in to compensation by failing on a Failure.
|
|
10
|
+
module Reader
|
|
11
|
+
def reader(name, source)
|
|
12
|
+
pstep name do
|
|
13
|
+
argument :seen, result(source)
|
|
14
|
+
run do |inputs, _ctx|
|
|
15
|
+
Probe.rec("run:#{name}")
|
|
16
|
+
failed = inputs.seen.is_a?(RubyReactor::Failure)
|
|
17
|
+
failed ? RubyReactor.Failure("#{name} saw a failed unit") : RubyReactor.Success(inputs.seen)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class Async01 < Base
|
|
24
|
+
pstep :a
|
|
25
|
+
pstep :u, kind: :async_step, after: :a, fail: true
|
|
26
|
+
pstep :b, after: :a
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class Async02 < Base
|
|
30
|
+
extend Reader
|
|
31
|
+
|
|
32
|
+
pstep :a
|
|
33
|
+
pstep :u, kind: :async_step, after: :a, fail: true
|
|
34
|
+
reader :r, :u
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class Async03 < Base
|
|
38
|
+
pstep :a
|
|
39
|
+
pstep :u, kind: :async_step, after: :a
|
|
40
|
+
pstep :b, after: :a, fail: true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class Async04 < Base
|
|
44
|
+
pstep :a
|
|
45
|
+
async_reactor(:child, ChildFails) { argument :from_a, result(:a) }
|
|
46
|
+
pstep :b, after: :a
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class Async05 < Base
|
|
50
|
+
extend Reader
|
|
51
|
+
|
|
52
|
+
pstep :a
|
|
53
|
+
async_reactor(:child, ChildFails) { argument :from_a, result(:a) }
|
|
54
|
+
reader :r, :child
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class Async06 < Base
|
|
58
|
+
pstep :a
|
|
59
|
+
async_reactor(:child, ChildOk) { argument :from_a, result(:a) }
|
|
60
|
+
pstep :b, after: :a, fail: true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class Async07 < Base
|
|
64
|
+
pstep :a
|
|
65
|
+
pstep :u, kind: :async_step, after: :a, fail: true, retries: { max_attempts: 3, base_delay: 0 }
|
|
66
|
+
pstep :b, after: :a
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
Probe.scenario "S-async-01", "a → async_step u(fails), no reader; b",
|
|
71
|
+
mode: :"inline parent + drained StepWorker",
|
|
72
|
+
expected: %w[run:a run:b run:u compensate:u => success] do
|
|
73
|
+
result = P::Async01.run({})
|
|
74
|
+
Probe.drain
|
|
75
|
+
result
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
Probe.scenario "S-async-02", "a → async_step u(fails) → r reads u and fails",
|
|
79
|
+
mode: :"inline parent + Sidekiq inline!",
|
|
80
|
+
expected: %w[run:a run:u compensate:u run:r compensate:r undo:a => failure(r)] do
|
|
81
|
+
Probe.inline_jobs { P::Async02.run({}) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
Probe.scenario "S-async-03", "a → async_step u(ok); b(fails), no reader",
|
|
85
|
+
mode: :"inline parent + drained StepWorker",
|
|
86
|
+
expected: %w[run:a run:b compensate:b undo:a run:u => failure(b)] do
|
|
87
|
+
result = P::Async03.run({})
|
|
88
|
+
Probe.drain
|
|
89
|
+
result
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
Probe.scenario "S-async-04", "a → async_reactor child(c1 → c2 fails), no reader; b",
|
|
93
|
+
mode: :"inline parent + drained Worker",
|
|
94
|
+
expected: %w[run:a run:b run:child.c1 run:child.c2 compensate:child.c2 undo:child.c1 => success] do
|
|
95
|
+
result = P::Async04.run({})
|
|
96
|
+
Probe.drain
|
|
97
|
+
result
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
Probe.scenario "S-async-05", "a → async_reactor child(c1 → c2 fails) → r reads child and fails",
|
|
101
|
+
mode: :"inline parent + Sidekiq inline!",
|
|
102
|
+
expected: %w[run:a run:child.c1 run:child.c2 compensate:child.c2 undo:child.c1 run:r compensate:r
|
|
103
|
+
undo:a => failure(r)] do
|
|
104
|
+
Probe.inline_jobs { P::Async05.run({}) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
Probe.scenario "S-async-06", "a → async_reactor child(ok); b(fails)",
|
|
108
|
+
mode: :"inline parent + drained Worker",
|
|
109
|
+
expected: %w[run:a run:b compensate:b undo:a run:child.c1 run:child.c2 => failure(b)] do
|
|
110
|
+
result = P::Async06.run({})
|
|
111
|
+
Probe.drain
|
|
112
|
+
result
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
Probe.scenario "S-async-07", "a → async_step u(always fails, retries 3), no reader; b",
|
|
116
|
+
mode: :"inline parent + drained StepWorker",
|
|
117
|
+
expected: %w[run:a run:b run:u run:u run:u compensate:u => success] do
|
|
118
|
+
result = P::Async07.run({})
|
|
119
|
+
Probe.drain
|
|
120
|
+
result
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Fake mode: nobody performs the unit, so the same-process reader waits out
|
|
124
|
+
# async_wait_timeout (2s here). The unit is drained afterwards.
|
|
125
|
+
Probe.scenario "S-async-08", "a → async_step u → r reads u; unit never finishes in time",
|
|
126
|
+
mode: :"inline parent + drained StepWorker (after)",
|
|
127
|
+
expected: %w[run:a undo:a run:u compensate:u => failure(r)] do
|
|
128
|
+
result = P::Async02.run({})
|
|
129
|
+
Probe.note("error=#{result.error.to_s.lines.first&.strip}")
|
|
130
|
+
Probe.drain
|
|
131
|
+
result
|
|
132
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# background: research H8, H23. The worker path (Worker#perform →
|
|
4
|
+
# Executor#resume_execution) against the same shapes as the inline baseline.
|
|
5
|
+
|
|
6
|
+
module P
|
|
7
|
+
class Bg01 < Base
|
|
8
|
+
background all: true
|
|
9
|
+
pstep :a
|
|
10
|
+
pstep :b, after: :a
|
|
11
|
+
pstep :c, after: :b, fail: true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Bg02 < Base
|
|
15
|
+
pstep :a
|
|
16
|
+
pstep :b, after: :a
|
|
17
|
+
pstep :c, after: :b, fail: true
|
|
18
|
+
background after: :a
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class Bg03 < Base
|
|
22
|
+
background all: true
|
|
23
|
+
pstep :a
|
|
24
|
+
pstep :b, after: :a, fail: true, retries: { max_attempts: 3, base_delay: 0 }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Bg04 < Base
|
|
28
|
+
background all: true
|
|
29
|
+
pstep :a
|
|
30
|
+
pstep :b, after: :a, fail_times: 1, retries: { max_attempts: 2, base_delay: 0 }
|
|
31
|
+
pstep :c, after: :b
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Probe.scenario "S-bg-01", "background all: a → b → c(fails)",
|
|
36
|
+
mode: :worker, expected: %w[run:a run:b run:c compensate:c undo:b undo:a => failure(c)] do
|
|
37
|
+
Probe.run_async(P::Bg01)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Probe.scenario "S-bg-02", "a | background after: :a | b → c(fails) (a ran in the caller)",
|
|
41
|
+
mode: :"caller + worker", expected: %w[run:a run:b run:c compensate:c undo:b undo:a => failure(c)] do
|
|
42
|
+
P::Bg02.run({})
|
|
43
|
+
Probe.note("caller returned with #{Probe.pending_jobs} worker job(s) queued")
|
|
44
|
+
id = RubyReactor::Adapters::Sidekiq::Worker.jobs.first["args"].first
|
|
45
|
+
Probe.drain
|
|
46
|
+
P::Bg02.find(id)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
Probe.scenario "S-bg-03", "background all: a → b(always fails, retries 3)",
|
|
50
|
+
mode: :worker,
|
|
51
|
+
expected: %w[run:a run:b retry:b#1 run:b retry:b#2 run:b compensate:b undo:a => failure(b)] do
|
|
52
|
+
Probe.run_async(P::Bg03)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Probe.scenario "S-bg-04", "background all: a → b(fails once, retries 2) → c",
|
|
56
|
+
mode: :worker, expected: %w[run:a run:b retry:b#1 run:b run:c => success] do
|
|
57
|
+
Probe.run_async(P::Bg04)
|
|
58
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Locks, semaphores and retries relative to rollback: research H2, H7, H13,
|
|
4
|
+
# H24, H25. Inline runs unless stated.
|
|
5
|
+
|
|
6
|
+
module P
|
|
7
|
+
def self.hold_lock(key, owner)
|
|
8
|
+
RubyReactor::Lock.new(key, owner: owner, ttl: 60, wait: 0, auto_extend: false).acquire
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.lock_count(key)
|
|
12
|
+
RubyReactor.configuration.storage_adapter.lock_info("lock:#{key}")&.fetch(:count)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Lock01 < Base
|
|
16
|
+
with_lock { |_inputs| "rk" }
|
|
17
|
+
pstep :a
|
|
18
|
+
pstep :b, after: :a, fail: true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class Lock02 < Base
|
|
22
|
+
pstep :a
|
|
23
|
+
pstep(:b, after: :a) { with_lock { |_args| "sk" } }
|
|
24
|
+
pstep :c, after: :b, fail: true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Lock03 < Base
|
|
28
|
+
pstep :a
|
|
29
|
+
pstep(:b, after: :a) { with_lock { |_args| "sk" } }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Lock04 < Base
|
|
33
|
+
pstep :a
|
|
34
|
+
pstep(:b, after: :a) { with_semaphore(limit: 1) { |_args| "sem" } }
|
|
35
|
+
pstep :c, after: :b, fail: true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# c's body takes b's key as another owner (a concurrent forward run), so b's
|
|
39
|
+
# undo cannot re-take it within rollback_wait.
|
|
40
|
+
class Lock05 < Base
|
|
41
|
+
pstep :a
|
|
42
|
+
pstep(:b, after: :a) { with_lock(rollback_wait: 0.3) { |_args| "sk" } }
|
|
43
|
+
pstep :c, after: :b do
|
|
44
|
+
run do |_inputs, _ctx|
|
|
45
|
+
Probe.rec("run:c")
|
|
46
|
+
P.hold_lock("sk", "intruder")
|
|
47
|
+
RubyReactor.Failure("boom c")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class LockedChild < Base
|
|
53
|
+
tag "child"
|
|
54
|
+
with_lock { |_inputs| "rk" }
|
|
55
|
+
pstep :c1
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class Lock06 < Base
|
|
59
|
+
with_lock { |_inputs| "rk" }
|
|
60
|
+
pstep :a
|
|
61
|
+
compose :child, LockedChild
|
|
62
|
+
pstep :b, after: :child do
|
|
63
|
+
run do |_inputs, _ctx|
|
|
64
|
+
Probe.rec("run:b(rk count=#{P.lock_count("rk")})")
|
|
65
|
+
RubyReactor.Failure("boom b")
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class Retry01 < Base
|
|
71
|
+
pstep :a
|
|
72
|
+
pstep :b, after: :a, fail: true, retries: { max_attempts: 3, base_delay: 0 }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class Retry02 < Base
|
|
76
|
+
pstep :a
|
|
77
|
+
pstep :b, after: :a, retries: { max_attempts: 3, base_delay: 0 } do
|
|
78
|
+
run do |_inputs, _ctx|
|
|
79
|
+
Probe.rec("run:b")
|
|
80
|
+
fail!("permanent", retry: false)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class Retry03 < Base
|
|
86
|
+
pstep :a
|
|
87
|
+
pstep :b, after: :a, fail_times: 1, retries: { max_attempts: 2, base_delay: 0 }
|
|
88
|
+
pstep :c, after: :b
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class Retry04 < Base
|
|
92
|
+
pstep :a
|
|
93
|
+
pstep :b, after: :a, fail: :raise, retries: { max_attempts: 2, base_delay: 0 }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
Probe.scenario "S-lock-01", "reactor with_lock(rk): a → b(fails)",
|
|
98
|
+
mode: :inline,
|
|
99
|
+
expected: %w[lock_acquired:rk run:a run:b compensate:b undo:a lock_released:lock:rk => failure(b)] do
|
|
100
|
+
# Reactor-level lock_released reports the prefixed key; lock_acquired does not.
|
|
101
|
+
P::Lock01.run({})
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
Probe.scenario "S-lock-02", "a → b(step with_lock sk) → c(fails)",
|
|
105
|
+
mode: :inline,
|
|
106
|
+
expected: %w[run:a lock_acquired:sk run:b lock_released:sk run:c compensate:c lock_acquired:sk undo:b
|
|
107
|
+
lock_released:sk undo:a => failure(c)] do
|
|
108
|
+
P::Lock02.run({})
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
Probe.scenario "S-lock-03", "a → b(step with_lock sk, key held by another owner)",
|
|
112
|
+
mode: :inline, expected: %w[run:a undo:a => failure(b)] do
|
|
113
|
+
P.hold_lock("sk", "someone-else")
|
|
114
|
+
P::Lock03.run({})
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
Probe.scenario "S-lock-04", "a → b(step with_semaphore limit 1) → c(fails)",
|
|
118
|
+
mode: :inline,
|
|
119
|
+
expected: %w[run:a semaphore_acquired:sem run:b semaphore_released:sem run:c compensate:c
|
|
120
|
+
semaphore_acquired:sem undo:b semaphore_released:sem undo:a => failure(c)] do
|
|
121
|
+
P::Lock04.run({})
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
Probe.scenario "S-lock-05", "a → b(step lock sk, rollback_wait 0.3) → c(takes sk as intruder, fails)",
|
|
125
|
+
mode: :inline,
|
|
126
|
+
expected: %w[run:a lock_acquired:sk run:b lock_released:sk run:c compensate:c undo:a
|
|
127
|
+
=> failure(c)] do
|
|
128
|
+
Probe.rollback_note(P::Lock05.run({}))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
Probe.scenario "S-lock-06", "reactor with_lock(rk): a → compose(child with_lock rk: c1) → b(fails)",
|
|
132
|
+
mode: :inline,
|
|
133
|
+
expected: ["lock_acquired:rk", "run:a", "lock_acquired:rk", "run:child.c1", "lock_released:lock:rk",
|
|
134
|
+
"run:b(rk count=1)", "compensate:b", "undo:child.c1", "undo:a", "lock_released:lock:rk",
|
|
135
|
+
"=>", "failure(b)"] do
|
|
136
|
+
P::Lock06.run({})
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
Probe.scenario "S-retry-01", "a → b(always fails, retries 3)",
|
|
140
|
+
mode: :inline,
|
|
141
|
+
expected: %w[run:a run:b retry:b#1 run:b retry:b#2 run:b compensate:b undo:a => failure(b)] do
|
|
142
|
+
P::Retry01.run({})
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
Probe.scenario "S-retry-02", "a → b(fail! retry: false, retries 3)",
|
|
146
|
+
mode: :inline, expected: %w[run:a run:b compensate:b undo:a => failure(b)] do
|
|
147
|
+
P::Retry02.run({})
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
Probe.scenario "S-retry-03", "a → b(fails once, retries 2) → c",
|
|
151
|
+
mode: :inline, expected: %w[run:a run:b retry:b#1 run:b run:c => success] do
|
|
152
|
+
P::Retry03.run({})
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
Probe.scenario "S-retry-04", "a → b(raises, retries 2)",
|
|
156
|
+
mode: :inline, expected: %w[run:a run:b retry:b#1 run:b compensate:b undo:a => failure(b)] do
|
|
157
|
+
P::Retry04.run({})
|
|
158
|
+
end
|