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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/.specify/feature.json +1 -1
  4. data/CHANGELOG.md +196 -0
  5. data/CLAUDE.md +1 -1
  6. data/README.md +47 -11
  7. data/lib/ruby_reactor/dsl/async_macros.rb +30 -1
  8. data/lib/ruby_reactor/dsl/async_reactor_builder.rb +12 -6
  9. data/lib/ruby_reactor/dsl/compose_builder.rb +12 -6
  10. data/lib/ruby_reactor/dsl/interrupt_builder.rb +1 -3
  11. data/lib/ruby_reactor/dsl/map_builder.rb +0 -2
  12. data/lib/ruby_reactor/dsl/step_builder.rb +91 -19
  13. data/lib/ruby_reactor/error/argument_resolution_error.rb +19 -0
  14. data/lib/ruby_reactor/error/rescuable.rb +28 -0
  15. data/lib/ruby_reactor/executor/compensation_manager.rb +30 -26
  16. data/lib/ruby_reactor/executor/result_handler.rb +18 -16
  17. data/lib/ruby_reactor/executor/step_coordination.rb +11 -8
  18. data/lib/ruby_reactor/executor/step_executor.rb +59 -49
  19. data/lib/ruby_reactor/executor.rb +38 -4
  20. data/lib/ruby_reactor/map/collector.rb +21 -11
  21. data/lib/ruby_reactor/map/dispatcher.rb +29 -3
  22. data/lib/ruby_reactor/map/element_executor.rb +9 -3
  23. data/lib/ruby_reactor/map/helpers.rb +32 -2
  24. data/lib/ruby_reactor/map/result_enumerator.rb +18 -12
  25. data/lib/ruby_reactor/reactor.rb +24 -0
  26. data/lib/ruby_reactor/rspec/matchers.rb +19 -3
  27. data/lib/ruby_reactor/step/compose_step.rb +7 -1
  28. data/lib/ruby_reactor/step/map_step.rb +109 -4
  29. data/lib/ruby_reactor/step.rb +7 -0
  30. data/lib/ruby_reactor/step_worker.rb +46 -22
  31. data/lib/ruby_reactor/storage/adapter.rb +4 -0
  32. data/lib/ruby_reactor/storage/redis_adapter.rb +9 -0
  33. data/lib/ruby_reactor/storage/redis_reactor_scan.rb +1 -1
  34. data/lib/ruby_reactor/version.rb +1 -1
  35. data/lib/ruby_reactor/web/api.rb +1 -1
  36. data/lib/ruby_reactor/web/public/assets/{index-CeZU-ESu.js → index-CQbgHtd0.js} +10 -10
  37. data/lib/ruby_reactor/web/public/index.html +1 -1
  38. data/lib/ruby_reactor/worker.rb +3 -1
  39. data/lib/ruby_reactor.rb +17 -6
  40. data/specs/007-execution-flow-analysis/analysis/README.md +147 -0
  41. data/specs/007-execution-flow-analysis/analysis/execution-order.md +359 -0
  42. data/specs/007-execution-flow-analysis/analysis/findings-and-options.md +502 -0
  43. data/specs/007-execution-flow-analysis/analysis/invariants.md +109 -0
  44. data/specs/007-execution-flow-analysis/checklists/requirements.md +39 -0
  45. data/specs/007-execution-flow-analysis/contracts/report-structure.md +71 -0
  46. data/specs/007-execution-flow-analysis/data-model.md +83 -0
  47. data/specs/007-execution-flow-analysis/evidence/harness.rb +229 -0
  48. data/specs/007-execution-flow-analysis/evidence/output.txt +333 -0
  49. data/specs/007-execution-flow-analysis/evidence/probes/01_plain.rb +122 -0
  50. data/specs/007-execution-flow-analysis/evidence/probes/02_compose.rb +182 -0
  51. data/specs/007-execution-flow-analysis/evidence/probes/03_map.rb +232 -0
  52. data/specs/007-execution-flow-analysis/evidence/probes/04_async.rb +132 -0
  53. data/specs/007-execution-flow-analysis/evidence/probes/05_background.rb +58 -0
  54. data/specs/007-execution-flow-analysis/evidence/probes/06_coordination.rb +158 -0
  55. data/specs/007-execution-flow-analysis/evidence/probes/07_interrupts_manual.rb +185 -0
  56. data/specs/007-execution-flow-analysis/evidence/run.rb +15 -0
  57. data/specs/007-execution-flow-analysis/plan.md +127 -0
  58. data/specs/007-execution-flow-analysis/quickstart.md +51 -0
  59. data/specs/007-execution-flow-analysis/research.md +202 -0
  60. data/specs/007-execution-flow-analysis/spec.md +270 -0
  61. data/specs/007-execution-flow-analysis/tasks.md +257 -0
  62. data/specs/008-rollback-reliability/checklists/requirements.md +43 -0
  63. data/specs/008-rollback-reliability/contracts/api-surface.md +126 -0
  64. data/specs/008-rollback-reliability/contracts/rollback-semantics.md +76 -0
  65. data/specs/008-rollback-reliability/data-model.md +139 -0
  66. data/specs/008-rollback-reliability/plan.md +233 -0
  67. data/specs/008-rollback-reliability/quickstart.md +105 -0
  68. data/specs/008-rollback-reliability/research.md +653 -0
  69. data/specs/008-rollback-reliability/spec.md +561 -0
  70. data/specs/008-rollback-reliability/tasks.md +1110 -0
  71. data/specs/future_improvements.md +48 -0
  72. metadata +35 -2
@@ -0,0 +1,39 @@
1
+ # Specification Quality Checklist: Execution Flow & Compensation Analysis
2
+
3
+ **Purpose**: Validate specification completeness and quality before proceeding to planning
4
+ **Created**: 2026-09-26
5
+ **Feature**: [spec.md](../spec.md)
6
+
7
+ ## Content Quality
8
+
9
+ - [x] No implementation details (languages, frameworks, APIs)
10
+ - [x] Focused on user value and business needs
11
+ - [x] Written for non-technical stakeholders
12
+ - [x] All mandatory sections completed
13
+
14
+ ## Requirement Completeness
15
+
16
+ - [x] No [NEEDS CLARIFICATION] markers remain
17
+ - [x] Requirements are testable and unambiguous
18
+ - [x] Success criteria are measurable
19
+ - [x] Success criteria are technology-agnostic (no implementation details)
20
+ - [x] All acceptance scenarios are defined
21
+ - [x] Edge cases are identified
22
+ - [x] Scope is clearly bounded
23
+ - [x] Dependencies and assumptions identified
24
+
25
+ ## Feature Readiness
26
+
27
+ - [x] All functional requirements have clear acceptance criteria
28
+ - [x] User scenarios cover primary flows
29
+ - [x] Feature meets measurable outcomes defined in Success Criteria
30
+ - [x] No implementation details leak into specification
31
+
32
+ ## Notes
33
+
34
+ - Stakeholders for this research are library maintainers and reactor authors, so library
35
+ vocabulary (step, compose, map, compensate, undo, inline/background) is the domain language, not
36
+ implementation detail. The `compensate_all` / `compensate_each` names appear only because the
37
+ user proposed them as candidates to evaluate.
38
+ - Validation passed on iteration 1. No clarifications needed: scope ambiguities (interrupts,
39
+ doc edits, evidence method) were resolved as documented assumptions.
@@ -0,0 +1,71 @@
1
+ # Contract: Report Structure
2
+
3
+ The deliverable's "interface" is the set of documents a maintainer reads and the probe output a
4
+ reviewer re-runs. This contract fixes what each must contain so the report can be checked
5
+ mechanically (see quickstart.md).
6
+
7
+ ## `analysis/README.md`
8
+
9
+ 1. **Scope & baseline**: commit, constructs covered, what is out of scope.
10
+ 2. **How to read**: vocabulary (research D4), evidence labels (D5), status/severity scales (D6/D7).
11
+ 3. **Answers**: one subsection per user question, in this order:
12
+ - Q1 *Are already-executed map elements compensated individually?*
13
+ - Q2 *When a composed reactor fails, are previously completed composed reactors compensated?*
14
+ - Q3 *Would `compensate_all` / `compensate_each` on `map` close a real gap?*
15
+
16
+ Each answer MUST open with a one-line verdict (`Yes` / `No` / `Depends: …`), then list the
17
+ conditions, then evidence labels, then links to the matrix rows, invariants and findings.
18
+ 4. **Top findings**: the High-severity findings, one line each with links.
19
+ 5. **File index**.
20
+
21
+ ## `analysis/execution-order.md`
22
+
23
+ 1. **Construct lifecycles**: one subsection per construct (step, compose, map inline, map fan-out,
24
+ async_step, async_reactor, background reactor, interrupt). Each has a numbered lifecycle from
25
+ scheduling to terminal state, saying where rollback hooks attach and where locks are held.
26
+ 2. **Rollback algorithm**: the generic compensate → reverse-undo sequence with a Mermaid diagram.
27
+ 3. **Order matrix**: one table per construct. Columns: `Scenario id | shape | failure at | mode |
28
+ ordered events | left in place | evidence`. No blank cells. Unreachable cells say
29
+ `not reachable: <reason>`.
30
+ 4. **Cross-cutting sections**: locks (lifetime vs rollback), retries (attempts vs rollback),
31
+ failure kinds (which path each takes: rollback / no rollback / never-started), each with its
32
+ own table.
33
+
34
+ ## `analysis/invariants.md`
35
+
36
+ A table or list of `INV-nn` entries with every Invariant field from data-model.md, grouped by
37
+ area. It ends with a **coverage summary**: counts by status, and the list of invariants with
38
+ `coverage: none`.
39
+
40
+ ## `analysis/findings-and-options.md`
41
+
42
+ 1. **Findings**, ranked High → Low, with every Finding field.
43
+ 2. **Documentation audit**: a table `file:line | quoted claim | actual | finding id`.
44
+ 3. **Options**, grouped under the finding they address, with every Option field. The map
45
+ `compensate_all` / `compensate_each` evaluation MUST appear here and compare both shapes on:
46
+ failure semantics (fail_fast on/off), inline vs fan-out, interaction with element retries,
47
+ interaction with a later step failing, and data availability (what `all_elements` / `element`
48
+ would contain).
49
+ 4. A closing note: every option is a proposal pending later analysis.
50
+
51
+ ## `evidence/output.txt`
52
+
53
+ Produced by `evidence/run.rb`. It has one block per scenario:
54
+
55
+ ```text
56
+ == S-plain-01 a → b(fails) → c [inline]
57
+ expected: run:a run:b compensate:b undo:a => failure(b)
58
+ observed: run:a run:b compensate:b undo:a => failure(b)
59
+ MATCH
60
+ ```
61
+
62
+ `MISMATCH` blocks are kept, not hidden. A `MISMATCH` means the report's expectation was wrong and
63
+ the report MUST be corrected to the observed sequence (the observation wins). Scenarios whose
64
+ purpose is to show a counter-example set `expected` to the *observed* behavior and state the
65
+ violation in the report.
66
+
67
+ ## Cross-reference rules
68
+
69
+ - Every `[O: S-…]` label in `analysis/*.md` resolves to a block in `output.txt`.
70
+ - Every finding lists at least one scenario or `[R]` citation.
71
+ - Every High finding has at least two options, each with a stated con (SC-004).
@@ -0,0 +1,83 @@
1
+ # Data Model: Execution Flow & Compensation Analysis
2
+
3
+ The "data" here is the report's own structure. Every entity below has a stable ID so report files
4
+ can cross-reference each other and cite probes.
5
+
6
+ ## Scenario
7
+
8
+ A reactor shape + failure location + conditions. It is one matrix cell, and usually one probe.
9
+
10
+ | Field | Description |
11
+ |---|---|
12
+ | `id` | `S-<area>-<nn>`; area ∈ `plain`, `retry`, `compose`, `map`, `async`, `bg`, `lock`, `intr`, `edge` |
13
+ | `shape` | Constructs and nesting, e.g. `a → compose(c1 → c2) → b` |
14
+ | `failure_at` | Which step fails and how (`returns Failure`, `raises`, `contended`, `validation`, `compensate fails`, …) |
15
+ | `mode` | `inline` or `worker` (+ which worker path) |
16
+ | `conditions` | Locks / retries / fail_fast / fan_out flags in effect |
17
+ | `expected` | Ordered event list the report claims (see Trace event) |
18
+ | `left_in_place` | Completed work that nothing rolls back |
19
+ | `evidence` | `[R]`, `[O]`, `[T]` labels (research D5) |
20
+
21
+ Rule: a matrix cell is **filled** when `expected` and `left_in_place` are both stated, or when it is
22
+ marked `not reachable` with a one-line reason (SC-001).
23
+
24
+ ## Trace event
25
+
26
+ One entry in an observed or expected sequence.
27
+
28
+ | Kind | Format | Source |
29
+ |---|---|---|
30
+ | Body | `run:<step>`, `compensate:<step>`, `undo:<step>` | probe step bodies |
31
+ | Element body | `run:<step>[<i>]`, `undo:<step>[<i>]` | probe map element steps |
32
+ | Middleware | `<event>:<subject>` e.g. `lock_acquired:acct:1`, `retry_attempt:charge#2` | recorder middleware |
33
+ | Outcome | `=> success`, `=> failure(<step>)`, `=> halt`, `=> paused` | probe footer |
34
+
35
+ Nested reactors prefix the step with the child name when they would otherwise be ambiguous,
36
+ e.g. `run:child.c1`.
37
+
38
+ ## Invariant
39
+
40
+ | Field | Description |
41
+ |---|---|
42
+ | `id` | `INV-<nn>` |
43
+ | `statement` | A testable proposition ("Compensation never runs for a step whose body never started") |
44
+ | `scope` | Constructs/modes it applies to |
45
+ | `status` | `HOLDS` · `VIOLATED` · `CONDITIONAL` · `UNDETERMINED` (research D6) |
46
+ | `conditions` | Required when `CONDITIONAL` |
47
+ | `evidence` | `[R]`/`[O]`/`[T]` labels; a `VIOLATED` status MUST cite a reproducible counter-example (`[O]`) |
48
+ | `coverage` | Existing spec(s) exercising it, or `none` |
49
+
50
+ ## Finding
51
+
52
+ | Field | Description |
53
+ |---|---|
54
+ | `id` | `F-<nn>` |
55
+ | `title` | One line |
56
+ | `severity` | `High` · `Medium` · `Low` (research D7) |
57
+ | `scenario` | Scenario id(s) that show it |
58
+ | `reader_expects` | What someone reading the DSL/docs would expect |
59
+ | `actual` | What happens, with evidence |
60
+ | `doc_conflict` | Quoted README/documentation text + `file:line`, or `none` |
61
+ | `related` | Invariant ids |
62
+
63
+ ## Improvement option
64
+
65
+ | Field | Description |
66
+ |---|---|
67
+ | `id` | `O-<finding>-<letter>` e.g. `O-03-a` |
68
+ | `addresses` | Finding id(s) |
69
+ | `sketch` | DSL/behavior shape (pseudo-code allowed) |
70
+ | `pros` / `cons` | At least one con is required (SC-004) |
71
+ | `compatibility` | Breaking? Migration impact? |
72
+ | `open_questions` | What must be decided before choosing |
73
+ | `status` | Always `proposal`, never a decision |
74
+
75
+ ## Relationships
76
+
77
+ ```text
78
+ Scenario 1─* Trace event
79
+ Scenario *─* Invariant (a scenario is evidence for invariants)
80
+ Finding *─* Scenario (shown by)
81
+ Finding *─* Invariant (violates / conditions)
82
+ Option *─1..* Finding (addresses)
83
+ ```
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Evidence harness for specs/007-execution-flow-analysis. Research artifact,
4
+ # NOT library or test-suite code. It runs throwaway reactors against the real
5
+ # test Redis through the real worker bodies (Sidekiq fake mode + drain) and
6
+ # prints the observed forward/rollback event order next to the order the
7
+ # report claims.
8
+
9
+ require "bundler/setup"
10
+ require "logger"
11
+ require "ruby_reactor"
12
+ require "sidekiq/testing"
13
+
14
+ Sidekiq::Testing.fake!
15
+ Sidekiq.configure_client do |config|
16
+ config.redis = { url: ENV.fetch("RUBY_REACTOR_TEST_REDIS_URL", "redis://localhost:6780") }
17
+ config.logger = Logger.new(IO::NULL)
18
+ end
19
+
20
+ module Probe
21
+ REDIS_URL = ENV.fetch("RUBY_REACTOR_TEST_REDIS_URL", "redis://localhost:6780")
22
+
23
+ # Drained in this order until quiet. Delayed jobs (perform_in) run at once.
24
+ WORKERS = [
25
+ RubyReactor::Adapters::Sidekiq::Worker,
26
+ RubyReactor::Adapters::Sidekiq::MapElementWorker,
27
+ RubyReactor::Adapters::Sidekiq::MapCollectorWorker,
28
+ RubyReactor::Adapters::Sidekiq::StepWorker
29
+ ].freeze
30
+
31
+ @events = []
32
+ @notes = []
33
+ @counters = Hash.new(0)
34
+ @tally = { match: 0, mismatch: 0 }
35
+
36
+ class << self
37
+ attr_reader :events, :notes, :counters, :tally
38
+
39
+ def rec(event) = @events << event
40
+ def note(text) = @notes << text
41
+
42
+ # Uniform step labels: "run:a", "undo:child.c1", "run:e1[2]".
43
+ def label(prefix, name, index = nil)
44
+ base = [prefix, name].compact.join(".")
45
+ index.nil? ? base : "#{base}[#{index}]"
46
+ end
47
+
48
+ # The value a probe `compensate` / `undo` body returns.
49
+ def rollback_result(mode, what)
50
+ case mode
51
+ when :fail then RubyReactor.Failure("#{what} failed")
52
+ when :raise then raise "#{what} raised"
53
+ else RubyReactor.Success()
54
+ end
55
+ end
56
+
57
+ # Same loop as RubyReactor::RSpec::SidekiqHelpers.drain_async_jobs, inlined
58
+ # because requiring that file pulls in the RSpec matchers.
59
+ def drain(max_iterations: 100)
60
+ max_iterations.times do
61
+ busy = false
62
+ WORKERS.each do |worker|
63
+ while (job = worker.jobs.shift)
64
+ worker.new.perform(*job["args"])
65
+ busy = true
66
+ end
67
+ end
68
+ return unless busy
69
+ end
70
+ end
71
+
72
+ def pending_jobs = WORKERS.sum { |w| w.jobs.size }
73
+
74
+ # Jobs run synchronously at enqueue (Sidekiq::Testing.inline!). Used only
75
+ # where a same-process reader must see a unit finish; labelled in the mode.
76
+ def inline_jobs(&block) = Sidekiq::Testing.inline!(&block)
77
+
78
+ # Run, drain every worker job, then reload the execution's terminal state.
79
+ def run_async(reactor_class, inputs = {})
80
+ dispatched = reactor_class.run(inputs)
81
+ drain
82
+ reactor_class.find(dispatched.execution_id)
83
+ end
84
+
85
+ # Prints Failure#rollback_failures as step/kind/reason; returns the result.
86
+ def rollback_note(result)
87
+ list = result.respond_to?(:rollback_failures) ? Array(result.rollback_failures) : []
88
+ note("rollback_failures=#{list.map { |f| "#{f[:step]}/#{f[:kind]}/#{f[:reason]}" }}")
89
+ result
90
+ end
91
+
92
+ def outcome(result)
93
+ result = result.result if result.is_a?(RubyReactor::Reactor)
94
+ case result
95
+ when String then result
96
+ when RubyReactor::Halt then "halt"
97
+ when RubyReactor::Success then "success"
98
+ when RubyReactor::Failure then "failure(#{result.step_name || "?"})"
99
+ when RubyReactor::InterruptResult then "paused"
100
+ when RubyReactor::DispatchResult then "dispatched"
101
+ when :unexecuted then "running"
102
+ else result.inspect
103
+ end
104
+ end
105
+
106
+ def reset!
107
+ RubyReactor.configuration.storage_adapter.instance_variable_get(:@redis).flushdb
108
+ WORKERS.each { |w| w.jobs.clear }
109
+ [@events, @notes].each(&:clear)
110
+ @counters.clear
111
+ end
112
+
113
+ # Runs one scenario and prints expected vs observed. The block returns the
114
+ # final result (a Result, a reloaded Reactor, or a String).
115
+ def scenario(id, title, mode:, expected:, &block)
116
+ return if ENV["PROBE"] && !id.include?(ENV["PROBE"])
117
+
118
+ reset!
119
+ observed = @events + ["=>", outcome(run_block(&block))]
120
+ report(id, title, mode, expected, observed)
121
+ end
122
+
123
+ private
124
+
125
+ def run_block(&block)
126
+ block.call
127
+ rescue StandardError => e
128
+ "raised(#{e.class}: #{e.message.lines.first&.strip})"
129
+ end
130
+
131
+ def report(id, title, mode, expected, observed)
132
+ ok = observed == expected
133
+ @tally[ok ? :match : :mismatch] += 1
134
+ puts "== #{id} #{title} [#{mode}]"
135
+ puts "expected: #{expected.join(" ")}"
136
+ puts "observed: #{observed.join(" ")}"
137
+ @notes.each { |n| puts "note: #{n}" }
138
+ puts ok ? "MATCH" : "MISMATCH"
139
+ puts
140
+ end
141
+ end
142
+
143
+ # Recording middleware: the shipped hook surface, no patching. Only events
144
+ # that say something about ordering relative to rollback are kept.
145
+ class Recorder < RubyReactor::Middleware
146
+ def on(event, *args)
147
+ case event
148
+ when :lock_acquired, :lock_released, :semaphore_acquired, :semaphore_released
149
+ Probe.rec("#{event}:#{args[0]}")
150
+ when :retry_attempt
151
+ Probe.rec("retry:#{args[0]}##{args[1]}")
152
+ end
153
+ end
154
+ end
155
+
156
+ # Reactor-class DSL sugar so a probe step is one line:
157
+ #
158
+ # pstep :b, after: :a, fail: true
159
+ # pstep :e1, idx: true, fail: ->(inputs) { inputs.i == 2 }
160
+ # pstep :c, fail: :raise, compensate: :fail, undo: :raise
161
+ # pstep :d, fail_times: 1, retries: { max_attempts: 2, base_delay: 0 }
162
+ module Steps
163
+ def tag(value = nil)
164
+ value ? @probe_tag = value : @probe_tag
165
+ end
166
+
167
+ # opts: fail:, fail_times:, retries: {…}, compensate: :ok|:fail|:raise, undo: (same),
168
+ # kind: :step (default) | :async_step
169
+ def pstep(name, after: nil, idx: false, **opts, &extra)
170
+ body = Steps.body(tag, name, idx, opts)
171
+ on_compensate = Steps.rollback(tag, name, idx, "compensate", opts[:compensate])
172
+ on_undo = Steps.rollback(tag, name, idx, "undo", opts[:undo])
173
+ public_send(opts.fetch(:kind, :step), name) do
174
+ wait_for(*Array(after)) if after
175
+ argument :i, input(:i) if idx
176
+ retries(**opts[:retries]) if opts[:retries]
177
+ run(&body)
178
+ compensate(&on_compensate)
179
+ # 008 R-09: an async_step is never undone, so `undo` on one is rejected.
180
+ undo(&on_undo) unless opts[:kind] == :async_step
181
+ instance_eval(&extra) if extra
182
+ end
183
+ end
184
+
185
+ # `failure`: `fail: true | :raise | ->(inputs) {}` or `fail_times: n`.
186
+ def self.body(prefix, name, idx, failure)
187
+ lambda do |inputs, _ctx|
188
+ lbl = Probe.label(prefix, name, idx ? inputs.i : nil)
189
+ Probe.rec("run:#{lbl}")
190
+ failing = failing?(lbl, inputs, failure)
191
+ raise "boom #{lbl}" if failing && failure[:fail] == :raise
192
+
193
+ failing ? RubyReactor.Failure("boom #{lbl}") : RubyReactor.Success("#{lbl}-value")
194
+ end
195
+ end
196
+
197
+ def self.failing?(lbl, inputs, failure)
198
+ return (Probe.counters[lbl] += 1) <= failure[:fail_times] if failure[:fail_times]
199
+
200
+ fail = failure[:fail]
201
+ fail.respond_to?(:call) ? fail.call(inputs) : fail
202
+ end
203
+
204
+ def self.rollback(prefix, name, idx, kind, mode)
205
+ lambda do |_error_or_result, inputs, _ctx|
206
+ lbl = Probe.label(prefix, name, idx ? inputs.i : nil)
207
+ Probe.rec("#{kind}:#{lbl}")
208
+ Probe.rollback_result(mode, "#{kind} #{lbl}")
209
+ end
210
+ end
211
+ end
212
+ end
213
+
214
+ RubyReactor.configure do |config|
215
+ config.storage.adapter = :redis
216
+ config.storage.redis_url = Probe::REDIS_URL
217
+ config.async_router = RubyReactor::Adapters::Sidekiq::Router
218
+ config.logger = Logger.new(IO::NULL)
219
+ config.middlewares = [Probe::Recorder]
220
+ config.lock_snooze_jitter = 0
221
+ config.async_wait_timeout = 2
222
+ end
223
+
224
+ # Probe reactors live under P so workers can resolve them by constant name.
225
+ module P
226
+ class Base < RubyReactor::Reactor
227
+ extend Probe::Steps
228
+ end
229
+ end