ruby_reactor 0.7.0 → 0.7.1
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 +79 -12
- data/.specify/templates/tasks-template.md +7 -0
- data/CHANGELOG.md +11 -0
- data/CLAUDE.md +2 -2
- data/README.md +24 -9
- data/lib/ruby_reactor/context.rb +7 -0
- data/lib/ruby_reactor/context_serializer.rb +13 -0
- data/lib/ruby_reactor/dsl/lockable.rb +2 -2
- data/lib/ruby_reactor/dsl/template_helpers.rb +11 -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 +35 -10
- data/lib/ruby_reactor/executor/retry_manager.rb +4 -1
- data/lib/ruby_reactor/executor/step_executor.rb +19 -13
- data/lib/ruby_reactor/executor.rb +17 -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/open_telemetry.rb +7 -4
- data/lib/ruby_reactor/ordered_lock.rb +3 -3
- data/lib/ruby_reactor/rspec/matchers.rb +61 -11
- data/lib/ruby_reactor/rspec/test_subject.rb +8 -8
- data/lib/ruby_reactor/step/map_step.rb +5 -1
- data/lib/ruby_reactor/step.rb +10 -4
- data/lib/ruby_reactor/step_signals.rb +33 -0
- 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/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 +56 -7
- metadata +7 -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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: abde4c2dc313b2e5d4a74873f1fc337743e6847e8f47fcbd121fd1a72be1d557
|
|
4
|
+
data.tar.gz: defd25588b78022c339ffd105d4e304d80e676498272c887cafecb60f100dc7e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 595c701f476086c166d0e157c062e0bea0e6fb243b1c5466017bf488461b987b44c70b56e081770c586a1d83519311c58da9769ae83e302fb7d17d1cdc8f37fb
|
|
7
|
+
data.tar.gz: 7dadad987df2ec6d9c1b44e93ed328f07d77660310726dceeb1ee8957cc9e63b73bd3c2338792280bfc780a95650bcac12b384af3d2aad83b3309aa4896f6d0d
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "demo-app-e2e-verify"
|
|
3
|
+
description: "Boot demo_app via docker-compose, run demo_reactors.rake, auto-derive acceptance criteria from the reactor/rake source, and cross-check actual behavior against both the RubyReactor API and dashboard at localhost:[port]/ruby_reactor. Writes findings + a fix plan to a tmp report."
|
|
4
|
+
argument-hint: "Optional: specific rake task(s)/reactor(s) to focus on (default: all)"
|
|
5
|
+
compatibility: "Requires docker compose and the demo_app/lib/tasks/demo_reactors.rake tasks"
|
|
6
|
+
metadata:
|
|
7
|
+
author: "project"
|
|
8
|
+
user-invocable: true
|
|
9
|
+
disable-model-invocation: false
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## User Input
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
$ARGUMENTS
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
If non-empty, scope the run to the named rake task(s)/reactor(s) instead of `demo:all`.
|
|
19
|
+
Otherwise run everything.
|
|
20
|
+
|
|
21
|
+
## Purpose
|
|
22
|
+
|
|
23
|
+
End-to-end confidence check for `demo_app`: does what the rake demos claim to do actually
|
|
24
|
+
show up correctly in the RubyReactor API and dashboard? This is not a spec-kit workflow —
|
|
25
|
+
it derives its own expectations from the current code rather than from `specs/`.
|
|
26
|
+
|
|
27
|
+
## Step 1: Bring the stack up
|
|
28
|
+
|
|
29
|
+
1. `docker compose ps` to see what's already running; avoid tearing down containers the
|
|
30
|
+
user didn't ask to stop.
|
|
31
|
+
2. `docker compose up -d demo-redis demo-sidekiq demo-app` (skip `redis-test`/`teley`
|
|
32
|
+
unless already running — they're not needed for this check). If it fails with a
|
|
33
|
+
container name conflict (`container_name` is static, and this repo runs from multiple
|
|
34
|
+
git worktrees sharing one docker daemon), check `docker ps -a --filter name=ruby_reactor`
|
|
35
|
+
— if the conflicting containers are `Exited` (not running), it's safe to `docker rm`
|
|
36
|
+
them and retry; never `rm` a container that's currently `Up` without asking, it may
|
|
37
|
+
belong to another worktree's active session.
|
|
38
|
+
3. Poll `docker compose ps` / container healthchecks until `demo-redis` and `demo-sidekiq`
|
|
39
|
+
report healthy, and until the Rails server inside `demo-app` responds. Use a
|
|
40
|
+
short polling loop (not a fixed sleep) against `http://localhost:3789/ruby_reactor` —
|
|
41
|
+
treat a non-timeout HTTP response (even a 404/500) as "server is up", empty/refused as
|
|
42
|
+
"still booting". Cap the wait (e.g. ~90s) and report clearly if it never comes up.
|
|
43
|
+
4. Read the exposed host port for `demo-app` from `docker-compose.yml` at runtime rather
|
|
44
|
+
than assuming `3789` — the file may have changed.
|
|
45
|
+
|
|
46
|
+
## Step 2: Auto-learn the expected behavior (before running anything)
|
|
47
|
+
|
|
48
|
+
For each rake task in scope, build an acceptance-criteria list by reading the source, not
|
|
49
|
+
by assuming:
|
|
50
|
+
|
|
51
|
+
1. Read `demo_app/lib/tasks/demo_reactors.rake` — for each in-scope task, note: which
|
|
52
|
+
reactor(s) it calls, what inputs/scenarios it loops over (e.g. `fail_at` values), and
|
|
53
|
+
what output markers it prints for success/failure/background/pause (`✅`, `❌`, `⏳`,
|
|
54
|
+
etc.).
|
|
55
|
+
2. Read the corresponding reactor class(es) in `demo_app/app/reactors/`. For each step,
|
|
56
|
+
note: what triggers it to succeed/fail/halt/skip/retry-veto, what its rollback (undo)
|
|
57
|
+
does if it has side effects, and any locks/rate-limits/periods/interrupts it declares.
|
|
58
|
+
3. From that, derive one acceptance criterion per scenario, e.g.:
|
|
59
|
+
- "`fail_at: nil` → task prints ✅, reactor status is `success`, `execution_trace`
|
|
60
|
+
contains every step in dependency order, `error` is nil."
|
|
61
|
+
- "`fail_at: :capture_payment` → task prints ❌, reactor status is `failed`/`halted`,
|
|
62
|
+
steps after `capture_payment` never ran, steps before it with side effects appear in
|
|
63
|
+
`undo_stack` (or show a compensation trace), `error` mentions the trigger."
|
|
64
|
+
- For background/async reactors: expect a `DispatchResult`/`⏳` print, then (after
|
|
65
|
+
`drain`/wait) a terminal status reachable via the API.
|
|
66
|
+
- For interrupt/pause reactors: expect `be_paused`-equivalent status and
|
|
67
|
+
`ready_interrupts`/`current_step` matching the interrupt step.
|
|
68
|
+
4. Keep this list in memory (or a scratch note) — it is what Step 4 checks results against.
|
|
69
|
+
If a reactor's behavior can't be inferred confidently from its source, say so explicitly
|
|
70
|
+
rather than guessing an expectation.
|
|
71
|
+
|
|
72
|
+
## Step 3: Run the demos
|
|
73
|
+
|
|
74
|
+
1. Execute the in-scope rake task(s) inside the running container:
|
|
75
|
+
`docker compose exec -T demo-app bin/rails demo:<task>` (or `demo:all` by default).
|
|
76
|
+
2. Capture full stdout. Extract, per scenario run: reactor class, inputs used (especially
|
|
77
|
+
any `fail_at`/scenario discriminator), the printed outcome marker, and any
|
|
78
|
+
`execution_id`/`context_id`/`order_id` printed — these are the join key to the API in
|
|
79
|
+
Step 4. If a task doesn't print an id, look up the id via the API list endpoint
|
|
80
|
+
(Step 4.1) by class name + recency instead of skipping verification.
|
|
81
|
+
3. For async/background scenarios, allow Sidekiq (already running as `demo-sidekiq`) time
|
|
82
|
+
to process, then re-check rather than asserting immediately. `demo:all` runs heavy
|
|
83
|
+
background tasks back-to-back (`map`'s ~30 jobs, `ar`'s ~100 product jobs, the lock/
|
|
84
|
+
semaphore/coordination holds) — an async/await step later in the chain (e.g.
|
|
85
|
+
`async_step_demo`, `async_reactor_demo`) can hit `async_wait_timeout` purely from
|
|
86
|
+
Sidekiq queue backlog, not a real bug. Before reporting an async timeout as a finding,
|
|
87
|
+
re-run that one task in isolation (`bin/rails demo:<task>`) once the queue has drained
|
|
88
|
+
— if it passes clean alone, it's a demo-ordering/concurrency artifact (note it under
|
|
89
|
+
Suggestions, e.g. "raise Sidekiq concurrency" or "reorder demo:all"), not a code bug.
|
|
90
|
+
|
|
91
|
+
### Splitting the run across agents (avoid context overload)
|
|
92
|
+
|
|
93
|
+
For `demo:all` (or any multi-task scope), don't run Steps 2-5 for every task in one
|
|
94
|
+
context — the full run is ~19 tasks / 80+ scenarios and will blow past a useful context
|
|
95
|
+
budget long before the report is written. Instead:
|
|
96
|
+
|
|
97
|
+
1. Do Step 1 (stack up) and a first pass of Step 2 yourself: skim
|
|
98
|
+
`demo_reactors.rake` and `git status`/`git log` for the reactors involved to identify
|
|
99
|
+
which ones are new/actively-changing right now (untracked files, recent commits touching
|
|
100
|
+
`app/reactors/`) — those get priority full-depth verification.
|
|
101
|
+
2. Group the remaining tasks (e.g. by rake task, or a few related tasks per group) and
|
|
102
|
+
fan them out to parallel background agents, each self-contained: give it the exact
|
|
103
|
+
`docker compose exec` command, the port, which reactors/tasks it owns, the acceptance-
|
|
104
|
+
criteria method from Step 2, and the Step 4/5 validation + fix instructions verbatim.
|
|
105
|
+
Tell each agent explicitly which scenarios warrant full API depth-check (`GET .../<id>`
|
|
106
|
+
against every field) versus which can be validated via stdout markers plus a 1-2
|
|
107
|
+
scenario spot-check — exhaustively hitting the API for every scenario in a large group
|
|
108
|
+
is what overloads context, not running the scenarios themselves.
|
|
109
|
+
3. Have each agent write its own findings; collect and merge into the single report in
|
|
110
|
+
Step 6 yourself (or have one agent own the merge) rather than re-deriving everything in
|
|
111
|
+
your own context.
|
|
112
|
+
|
|
113
|
+
## Step 4: Validate against both data sources
|
|
114
|
+
|
|
115
|
+
For every scenario captured in Step 3, check it against **both**:
|
|
116
|
+
|
|
117
|
+
**(a) The API** (`http://localhost:<port>/ruby_reactor/api/reactors`):
|
|
118
|
+
- `GET /ruby_reactor/api/reactors` — list; confirm the run's reactor/context appears.
|
|
119
|
+
Known limitation to watch for: the storage adapter's `scan_reactors` caps at `count: 50`
|
|
120
|
+
via Redis `SCAN` with no chronological ordering — on a run with 50+ total executions
|
|
121
|
+
(e.g. `demo:all`), some earlier ones can be genuinely absent from the list endpoint even
|
|
122
|
+
though `GET .../<id>` finds them directly by id. Don't assume "missing from the list" ==
|
|
123
|
+
"reactor didn't run" — try the direct id lookup before concluding it's a bug, but if it
|
|
124
|
+
IS a real gap, it's a known gem-level finding (no `limit`/`cursor` param threaded through
|
|
125
|
+
`lib/ruby_reactor/web/api.rb` → `lib/ruby_reactor/storage/redis_adapter.rb` `scan_reactors`).
|
|
126
|
+
- `GET /ruby_reactor/api/reactors/<id>` — confirm `status`, `current_step`,
|
|
127
|
+
`execution_trace` (`steps`), `undo_stack`, `retry_count`/`step_attempts`, `error`,
|
|
128
|
+
`coordination`, and `structure` match the acceptance criterion from Step 2. Compare
|
|
129
|
+
step ordering and dependency edges against `structure`/`depends_on`, not just final
|
|
130
|
+
status.
|
|
131
|
+
- **Cross-check the rake task's own printer branches against every outcome type the
|
|
132
|
+
reactor can actually produce**, not just its final API status. A common bug class here:
|
|
133
|
+
a `run_*` helper in the rake task branches on `DispatchResult` / `success?` / `failure?`
|
|
134
|
+
but is missing a `halted?` (or `skipped?`) branch, so a reactor that legitimately halts
|
|
135
|
+
(e.g. a dedup/period-lock halt, which is `Halt < Success`) falls through to the
|
|
136
|
+
`success?` branch and prints a misleading `✅ SUCCESS: nil` while the API correctly shows
|
|
137
|
+
`status: "halted"`. Read every `run_*`/`report_demo_result` helper the in-scope tasks use
|
|
138
|
+
and check its branch list covers everything the reactor's `signals`/return values allow.
|
|
139
|
+
|
|
140
|
+
**(b) The dashboard** (`http://localhost:<port>/ruby_reactor`):
|
|
141
|
+
- Confirm the root path serves the SPA (HTTP 200, HTML with its mount point) — a broken
|
|
142
|
+
dashboard build is itself a finding.
|
|
143
|
+
- Where practical, fetch the same reactor's dashboard state the UI would render (it is
|
|
144
|
+
backed by the same API, so this mainly confirms the mount + asset pipeline are healthy
|
|
145
|
+
end-to-end, not just the API module in isolation). Note any divergence between what the
|
|
146
|
+
rake stdout claimed and what the API/dashboard actually recorded — that divergence is
|
|
147
|
+
the primary class of bug this skill hunts for.
|
|
148
|
+
|
|
149
|
+
Also check for **coverage gaps**: any reactor in `demo_app/app/reactors/` with no
|
|
150
|
+
corresponding rake task (nothing exercises it), and any acceptance criterion from Step 2
|
|
151
|
+
that neither data source could confirm (e.g. no id was ever discoverable).
|
|
152
|
+
|
|
153
|
+
## Step 5: For every bug found, propose a fix and a regression test
|
|
154
|
+
|
|
155
|
+
A finding is not done at "here's what's wrong" — for each confirmed bug/error:
|
|
156
|
+
|
|
157
|
+
1. **Root-cause it**: trace the mismatch to the actual code (a step's outcome, the API
|
|
158
|
+
serializer, storage, the dashboard build) rather than stopping at "API returned X,
|
|
159
|
+
expected Y". Name the file(s)/line(s) responsible.
|
|
160
|
+
2. **Propose a concrete fix**: a diff-level description (or, if the fix is small and
|
|
161
|
+
unambiguous, apply it — see below) of what changes and why, referencing the gem's
|
|
162
|
+
principles (`.specify/memory/constitution.md`) where relevant (e.g. don't silently
|
|
163
|
+
widen behavior beyond what the demo actually needs).
|
|
164
|
+
3. **Propose the regression test** that would have caught it, and place it correctly:
|
|
165
|
+
- **Bug in gem behavior** (executor, signals, storage, adapters, DSL, the `Web::API`
|
|
166
|
+
serializer, etc.) → a unit/integration spec under `spec/` (mirror the existing
|
|
167
|
+
layout, e.g. `spec/ruby_reactor/web/` for API bugs, `spec/ruby_reactor/executor/`
|
|
168
|
+
for execution-order bugs) using the gem's normal spec helpers — **not** the
|
|
169
|
+
`RubyReactor::RSpec` demo helpers, those are for `demo_app`.
|
|
170
|
+
- **Bug only reachable through the demo reactor's own definition, the rake task, or
|
|
171
|
+
dashboard integration** → a spec under `demo_app/spec/reactors/`, `type: :reactor`,
|
|
172
|
+
using only the built-in surface from `lib/ruby_reactor/rspec.rb` (see
|
|
173
|
+
`speckit-demo-tests` skill for the exact matcher/helper list and the "no hand-rolled
|
|
174
|
+
scaffolding" rule — apply the same constraint here).
|
|
175
|
+
- If the bug spans both (a gem defect that happens to surface via a demo reactor),
|
|
176
|
+
propose **both**: a gem-level spec pinning the underlying behavior, and a demo spec
|
|
177
|
+
confirming the demo reactor now reports it correctly.
|
|
178
|
+
4. Do not apply gem-behavior fixes automatically without asking — surface them in the
|
|
179
|
+
report. You MAY apply a fix that is confined to `demo_app/` (a demo reactor, the rake
|
|
180
|
+
task, or a demo spec) directly if it's unambiguous, then note that it was applied and
|
|
181
|
+
re-verify it in this same run. Never silently skip proposing the regression test even
|
|
182
|
+
when you don't apply the fix itself.
|
|
183
|
+
|
|
184
|
+
## Step 6: Write the report
|
|
185
|
+
|
|
186
|
+
Write findings to `demo_app/tmp/demo_verification/<UTC timestamp>-report.md` (create the
|
|
187
|
+
directory if needed; it's already gitignored via `demo_app/.gitignore`). Structure:
|
|
188
|
+
|
|
189
|
+
```markdown
|
|
190
|
+
# Demo App E2E Verification — <timestamp>
|
|
191
|
+
|
|
192
|
+
## Scope
|
|
193
|
+
<tasks/reactors run, port used, docker services status>
|
|
194
|
+
|
|
195
|
+
## Acceptance criteria checked
|
|
196
|
+
<the Step 2 list, one line per scenario>
|
|
197
|
+
|
|
198
|
+
## Results
|
|
199
|
+
| Reactor | Scenario | Rake output | API result | Dashboard | Verdict |
|
|
200
|
+
|---|---|---|---|---|---|
|
|
201
|
+
...
|
|
202
|
+
|
|
203
|
+
## Errors found
|
|
204
|
+
<concrete mismatches: expected vs actual, with the reactor/context id and relevant
|
|
205
|
+
API JSON excerpt or rake stdout excerpt, plus root cause (file/line)>
|
|
206
|
+
|
|
207
|
+
## Suggestions / improvements
|
|
208
|
+
<non-broken but worth-fixing observations: missing rake coverage, confusing output,
|
|
209
|
+
missing undo, etc.>
|
|
210
|
+
|
|
211
|
+
## Fix plan
|
|
212
|
+
<per error: ordered, concrete steps to resolve it — file(s) to touch, what changes,
|
|
213
|
+
whether it was already applied — followed by the regression test proposed for it
|
|
214
|
+
(file path + what it asserts) and whether it was added/passing>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Keep the table terse; put raw JSON/log excerpts in fenced code blocks under "Errors
|
|
218
|
+
found", not inline in the table.
|
|
219
|
+
|
|
220
|
+
## Step 7: Summarize to the user
|
|
221
|
+
|
|
222
|
+
Report: report file path, pass/fail counts, and the top few errors (if any) inline —
|
|
223
|
+
each with its proposed fix and regression test location — so the user doesn't have to
|
|
224
|
+
open the file to know whether anything broke or what to do about it. If everything
|
|
225
|
+
passed, say so plainly — don't manufacture findings. Do not tear down the docker services
|
|
226
|
+
unless the user asks; mention they're still running.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "speckit-demo-tests"
|
|
3
|
+
description: "Generate the demo_app example reactor, rake task, and RSpec spec required by Constitution Principle VI for the current spec-kit feature, or for a context the user describes when no feature is active."
|
|
4
|
+
argument-hint: "Optional: what to demo/test (required if no active speckit feature is detected)"
|
|
5
|
+
compatibility: "Requires spec-kit project structure with .specify/ directory; writes into demo_app/"
|
|
6
|
+
metadata:
|
|
7
|
+
author: "project"
|
|
8
|
+
user-invocable: true
|
|
9
|
+
disable-model-invocation: false
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## User Input
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
$ARGUMENTS
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
You **MUST** consider the user input before proceeding (if not empty).
|
|
19
|
+
|
|
20
|
+
## Purpose
|
|
21
|
+
|
|
22
|
+
Constitution Principle VI ("Demo-App Proof of Feature", `.specify/memory/constitution.md`)
|
|
23
|
+
requires every feature to ship three artifacts together: an example reactor in
|
|
24
|
+
`demo_app/app/reactors/`, a task entry in `demo_app/lib/tasks/demo_reactors.rake`, and a
|
|
25
|
+
spec in `demo_app/spec/reactors/` written **only** with the built-in RSpec surface from
|
|
26
|
+
`lib/ruby_reactor/rspec.rb`. This skill produces all three in one pass.
|
|
27
|
+
|
|
28
|
+
## Step 1: Determine the context
|
|
29
|
+
|
|
30
|
+
Try to detect the current spec-kit feature first; only fall back to the user for context
|
|
31
|
+
if detection fails.
|
|
32
|
+
|
|
33
|
+
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --paths-only` from the repo
|
|
34
|
+
root and parse `FEATURE_DIR` and `BRANCH`.
|
|
35
|
+
2. **Speckit detected** if `FEATURE_DIR` resolves to an existing directory under `specs/`
|
|
36
|
+
containing a `spec.md`. In that case:
|
|
37
|
+
- Read `spec.md` (and `plan.md`/`data-model.md` if present) in `FEATURE_DIR`.
|
|
38
|
+
- Extract the functional requirements (`FR-###`) and Given/When/Then acceptance
|
|
39
|
+
scenarios that describe observable, runnable behavior — the ones a reactor run can
|
|
40
|
+
demonstrate (happy path, a failure/compensation path, and any distinctive semantics
|
|
41
|
+
like pause/resume, skip, halt, retry).
|
|
42
|
+
- This extracted behavior is the context for Step 2. Do not ask the user anything
|
|
43
|
+
unless the spec is ambiguous about what a runnable example should exercise.
|
|
44
|
+
3. **No speckit detected** (script fails, no `FEATURE_DIR`, or no `spec.md`) — the
|
|
45
|
+
feature being tested must come from the user:
|
|
46
|
+
- If `$ARGUMENTS` is non-empty, treat it as the context describing what to build a
|
|
47
|
+
demo/test for.
|
|
48
|
+
- If `$ARGUMENTS` is empty, use `AskUserQuestion` (or a plain question if that tool is
|
|
49
|
+
unavailable) to ask what reactor behavior/feature the demo and spec should cover.
|
|
50
|
+
Do not invent a feature — block here until the user answers.
|
|
51
|
+
|
|
52
|
+
## Step 2: Study existing conventions before writing anything
|
|
53
|
+
|
|
54
|
+
Read 1-2 existing examples so the new files match house style exactly:
|
|
55
|
+
|
|
56
|
+
- One existing reactor from `demo_app/app/reactors/` whose shape is closest to the new
|
|
57
|
+
one (e.g. a failure/compensation demo, a background demo, an interrupt demo — pick by
|
|
58
|
+
relevance to the context from Step 1).
|
|
59
|
+
- Its matching spec in `demo_app/spec/reactors/`.
|
|
60
|
+
- The full built-in matcher/helper surface: skim `lib/ruby_reactor/rspec.rb` and its
|
|
61
|
+
`lib/ruby_reactor/rspec/*.rb` files (`helpers.rb`, `test_subject.rb`, `matchers.rb`,
|
|
62
|
+
`sidekiq_helpers.rb`, `async_test_helpers.rb`). Confirm the exact set currently
|
|
63
|
+
available — do not rely on a memorized list, the gem may have grown matchers since.
|
|
64
|
+
- The tail of `demo_app/lib/tasks/demo_reactors.rake`, in particular the `desc "All demo
|
|
65
|
+
reactors"` / `task all: [...]` entry — the new task name must be appended to that
|
|
66
|
+
dependency list.
|
|
67
|
+
|
|
68
|
+
## Step 3: Generate the example reactor
|
|
69
|
+
|
|
70
|
+
Create `demo_app/app/reactors/<name>_reactor.rb` (snake_case file matching the
|
|
71
|
+
`CamelCase` class name), where `<name>` is derived from the context (ask the user only
|
|
72
|
+
if the right name is genuinely ambiguous). Requirements:
|
|
73
|
+
|
|
74
|
+
- Class-based step definitions, matching Development Workflow conventions (no inline
|
|
75
|
+
lambdas as the primary authoring style).
|
|
76
|
+
- Exercises the feature end-to-end: the happy path, and — where the feature has one — a
|
|
77
|
+
failure/compensation path, typically toggled via a `fail_at:`-style input like other
|
|
78
|
+
demo reactors already do.
|
|
79
|
+
- If the feature under test is signal-specific (Halt, Skipped, retry veto, pause/resume,
|
|
80
|
+
locks, rate limits, periods), the example MUST produce that outcome observably (e.g. a
|
|
81
|
+
step that calls `halt!`/`skip!`/`fail!(..., retry: false)` under a controllable input),
|
|
82
|
+
not just something that happens to compile.
|
|
83
|
+
- Do not duplicate an existing demo reactor's purpose — if one already covers this
|
|
84
|
+
behavior, extend it or say so instead of creating a near-duplicate file.
|
|
85
|
+
|
|
86
|
+
## Step 4: Register the rake task
|
|
87
|
+
|
|
88
|
+
Edit `demo_app/lib/tasks/demo_reactors.rake`:
|
|
89
|
+
|
|
90
|
+
- Add a `desc "<Reactor> — <one-line of what it demonstrates>"` /
|
|
91
|
+
`task <name>: [:environment, :flush_redis] do ... end` block, following the existing
|
|
92
|
+
print-the-outcome pattern (`✅ SUCCESS`, `❌ FAILED`, `⏳ BACKGROUND`, etc. — match
|
|
93
|
+
whatever branch the existing tasks use for `DispatchResult`/`success?`/`failure?`).
|
|
94
|
+
- Run through the same scenarios the spec will assert (e.g. loop over `fail_at`
|
|
95
|
+
values) so `rake demo:<name>` is a working live demonstration on its own.
|
|
96
|
+
- Append `:<name>` to the `task all: [...]` dependency list so it runs as part of
|
|
97
|
+
`rake demo:all`.
|
|
98
|
+
|
|
99
|
+
## Step 5: Write the spec using only the built-in RSpec surface
|
|
100
|
+
|
|
101
|
+
Create `demo_app/spec/reactors/<name>_reactor_spec.rb`:
|
|
102
|
+
|
|
103
|
+
- `RSpec.describe <ReactorClass>, type: :reactor do ... end`.
|
|
104
|
+
- Build the subject with `test_reactor(described_class, inputs)` (see
|
|
105
|
+
`lib/ruby_reactor/rspec/helpers.rb`).
|
|
106
|
+
- Use only the `TestSubject` API (`lib/ruby_reactor/rspec/test_subject.rb`) — e.g.
|
|
107
|
+
`mock_step`, `failing_at`, `map`, `composed`, `async_step`, `async_reactor`, `resume`,
|
|
108
|
+
`step_result`, `ensure_executed!`, `process_pending_jobs` — and only the matchers
|
|
109
|
+
defined in `lib/ruby_reactor/rspec/matchers.rb` (`be_success`, `be_failure`,
|
|
110
|
+
`have_run_step(...).after(...)`, `have_retried_step`, `have_validation_error`,
|
|
111
|
+
`be_paused`, `be_paused_at`, `have_ready_interrupts`, `be_halted`, `be_skipped`,
|
|
112
|
+
`be_locked`, `have_available_tokens`, `have_held_tokens`, `have_rate_limit_count`,
|
|
113
|
+
`be_period_marked`, and the ordered-lock matchers) plus `drain_async_jobs` /
|
|
114
|
+
`pending_async_jobs` from `sidekiq_helpers.rb` for async paths.
|
|
115
|
+
- **Forbidden**: any direct `RubyReactor::Executor`/`Storage`/Redis call, manual Sidekiq
|
|
116
|
+
job draining, stubbing reactor internals, or any other hand-rolled scaffolding in this
|
|
117
|
+
spec file.
|
|
118
|
+
- **If an assertion needs a matcher or helper that does not exist yet**: add it to the
|
|
119
|
+
appropriate file under `lib/ruby_reactor/rspec/` in this same change (with its own
|
|
120
|
+
unit coverage if the gem's own spec suite conventions call for it), then use it from
|
|
121
|
+
the demo spec. Do not work around the gap with ad-hoc internals — that is what
|
|
122
|
+
Principle VI explicitly forbids.
|
|
123
|
+
- Cover at least: the happy path (`be_success`, step ordering via `have_run_step`), and
|
|
124
|
+
the failure/compensation or signal-specific path identified in Step 1.
|
|
125
|
+
|
|
126
|
+
## Step 6: Validate
|
|
127
|
+
|
|
128
|
+
Run, from `demo_app/`:
|
|
129
|
+
|
|
130
|
+
- `bundle exec rspec spec/reactors/<name>_reactor_spec.rb` — must pass.
|
|
131
|
+
- `bundle exec rake demo:<name>` — must run cleanly and print observable outcomes.
|
|
132
|
+
- `bundle exec rubocop app/reactors/<name>_reactor.rb lib/tasks/demo_reactors.rake spec/reactors/<name>_reactor_spec.rb` (from the repo root, or wherever the project's rubocop config resolves) — fix violations before finishing.
|
|
133
|
+
|
|
134
|
+
If any command fails, fix the generated files and re-run — do not report success on
|
|
135
|
+
files that don't pass.
|
|
136
|
+
|
|
137
|
+
## Step 7: Summarize
|
|
138
|
+
|
|
139
|
+
Report to the user:
|
|
140
|
+
|
|
141
|
+
- Files created/edited (reactor, rake task block + `all` list update, spec).
|
|
142
|
+
- How to run the demo (`rake demo:<name>`) and the spec.
|
|
143
|
+
- Whether any new matcher/helper was added to `lib/ruby_reactor/rspec/` and why.
|
|
144
|
+
- Confirmation that validation (Step 6) passed.
|
data/.specify/feature.json
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
SYNC IMPACT REPORT
|
|
3
3
|
==================
|
|
4
|
-
Version change:
|
|
4
|
+
Version change: 1.1.0 → 1.2.0 (MINOR: Principle VI expanded with a 4th requirement —
|
|
5
|
+
docker-compose.yml currency + docker-run acceptance tests)
|
|
5
6
|
|
|
6
|
-
Modified principles:
|
|
7
|
+
Modified principles:
|
|
8
|
+
- Principle VI: Demo-App Proof of Feature — added requirement 4, "Docker acceptance
|
|
9
|
+
run" (docker-compose.yml MUST track demo_app's services; demo: rake tasks are the
|
|
10
|
+
project's acceptance suite, runnable via `docker compose run`)
|
|
7
11
|
|
|
8
|
-
Added sections:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- Development Workflow
|
|
12
|
-
- Governance
|
|
12
|
+
Added sections: none (existing Principle VI section extended)
|
|
13
|
+
|
|
14
|
+
Removed sections: none
|
|
13
15
|
|
|
14
16
|
Templates checked:
|
|
15
|
-
- .specify/templates/plan-template.md ✅ Constitution Check gate
|
|
16
|
-
|
|
17
|
-
- .specify/templates/
|
|
17
|
+
- .specify/templates/plan-template.md ✅ Constitution Check gate is generic ("Gates determined
|
|
18
|
+
based on constitution file") — no edit required
|
|
19
|
+
- .specify/templates/spec-template.md ✅ No principle-specific content — no edit required
|
|
20
|
+
- .specify/templates/tasks-template.md ✅ Already covers demo-app polish tasks — no edit required
|
|
21
|
+
- .specify/templates/checklist-template.md ✅ Generic — no edit required
|
|
22
|
+
- .specify/extensions.yml ✅ No before/after_constitution hooks registered
|
|
18
23
|
|
|
19
24
|
Deferred TODOs: none
|
|
20
25
|
-->
|
|
@@ -89,6 +94,58 @@ surprise breaking change in a MINOR bump costs downstream teams debugging time
|
|
|
89
94
|
they did not budget for. Simplicity keeps the library auditable and the
|
|
90
95
|
upgrade path predictable.
|
|
91
96
|
|
|
97
|
+
### VI. Demo-App Proof of Feature (NON-NEGOTIABLE)
|
|
98
|
+
|
|
99
|
+
Every user-facing feature or public API change MUST ship with a runnable example
|
|
100
|
+
in `demo_app/`. Three artifacts are required together — a change is incomplete if
|
|
101
|
+
any one is missing:
|
|
102
|
+
|
|
103
|
+
1. **Example reactor**: a reactor (or step) demonstrating the feature MUST live in
|
|
104
|
+
`demo_app/app/reactors/`, one file per reactor, named `<snake_case>_reactor.rb`
|
|
105
|
+
matching its class name. The example MUST exercise the feature end to end —
|
|
106
|
+
including its failure and compensation path where the feature has one — and MUST
|
|
107
|
+
use class-based step definitions per the Development Workflow rule.
|
|
108
|
+
2. **Rake entry**: the example MUST be registered as a task in
|
|
109
|
+
`demo_app/lib/tasks/demo_reactors.rake` under the `demo:` namespace, with a `desc`
|
|
110
|
+
line describing what it demonstrates, and depending on `[:environment, :flush_redis]`
|
|
111
|
+
so each run starts from clean Redis state. The task MUST print observable outcomes
|
|
112
|
+
(success, failure, background dispatch, pause) so an operator can verify behavior
|
|
113
|
+
without a debugger.
|
|
114
|
+
3. **Spec**: a matching spec MUST live at
|
|
115
|
+
`demo_app/spec/reactors/<reactor>_spec.rb`, declared `type: :reactor`, and MUST use
|
|
116
|
+
**only** the built-in test surface exported by `lib/ruby_reactor/rspec.rb` — the
|
|
117
|
+
helpers (`test_reactor`, `drain_async_jobs`), the `TestSubject` API
|
|
118
|
+
(`mock_step`, `failing_at`, `map`, `composed`, `async_step`, `resume`,
|
|
119
|
+
`step_result`, `ensure_executed!`), and the matchers (`be_success`, `be_failure`,
|
|
120
|
+
`have_run_step(...).after(...)`, `have_retried_step`, `have_validation_error`,
|
|
121
|
+
`be_paused`, `be_paused_at`, `have_ready_interrupts`, `be_halted`, `be_skipped`,
|
|
122
|
+
`be_locked`, `have_available_tokens`, `have_held_tokens`, `have_rate_limit_count`,
|
|
123
|
+
`be_period_marked`, and the ordered-lock matchers).
|
|
124
|
+
|
|
125
|
+
Hand-rolled test scaffolding is forbidden in `demo_app/spec/reactors/`: no direct
|
|
126
|
+
`RubyReactor::Executor`/`Storage` calls, no bespoke Sidekiq draining, no manual Redis
|
|
127
|
+
assertions, no stubbing of reactor internals. If an assertion cannot be expressed with
|
|
128
|
+
the built-in surface, the missing matcher or helper MUST be added to
|
|
129
|
+
`lib/ruby_reactor/rspec/` in the same change — extending the shared test API, not
|
|
130
|
+
bypassing it.
|
|
131
|
+
|
|
132
|
+
4. **Docker acceptance run**: `docker-compose.yml` MUST stay current with `demo_app`'s
|
|
133
|
+
runtime dependencies (Redis, Sidekiq, the Rails service itself) so that
|
|
134
|
+
`docker compose run --rm demo-app bin/rails demo:<task>` runs the new rake task
|
|
135
|
+
end to end against real Redis, with no manual setup beyond `docker compose up`.
|
|
136
|
+
A new demo service or environment variable required by a feature MUST be added to
|
|
137
|
+
`docker-compose.yml` in the same change. The `demo:` rake tasks in
|
|
138
|
+
`demo_app/lib/tasks/demo_reactors.rake` constitute the project's acceptance test
|
|
139
|
+
suite for user-facing behavior — CI or a release checklist MAY invoke them via
|
|
140
|
+
`docker compose run` to confirm the demo still passes before a MINOR/MAJOR release.
|
|
141
|
+
|
|
142
|
+
**Rationale**: `demo_app/` is the only place the gem is consumed the way users consume
|
|
143
|
+
it. An example that is written but never listed is never run; a spec written with
|
|
144
|
+
private internals passes while the public API is broken. Forcing every feature through
|
|
145
|
+
the public reactor DSL, a runnable rake task, and the shipped matchers means the
|
|
146
|
+
documented API, the demo, and the test surface are validated by the same change — and
|
|
147
|
+
gaps in the matcher library surface as work instead of as workarounds.
|
|
148
|
+
|
|
92
149
|
## Technical Constraints
|
|
93
150
|
|
|
94
151
|
- **Ruby**: >= 3.0.0 required. No polyfills for older Rubies.
|
|
@@ -113,7 +170,17 @@ upgrade path predictable.
|
|
|
113
170
|
- Class-based step definitions are the preferred authoring style (not inline
|
|
114
171
|
lambdas). Documentation and examples MUST reflect this.
|
|
115
172
|
- The `demo_app/` directory serves as a living integration example. Changes to
|
|
116
|
-
public API surface MUST be reflected there
|
|
173
|
+
public API surface MUST be reflected there per Principle VI — example reactor in
|
|
174
|
+
`demo_app/app/reactors/`, rake task in `demo_app/lib/tasks/demo_reactors.rake`,
|
|
175
|
+
and a spec in `demo_app/spec/reactors/` using only the built-in RSpec matchers
|
|
176
|
+
and helpers from `lib/ruby_reactor/rspec.rb`.
|
|
177
|
+
- PR reviews MUST reject any feature change whose demo example is missing, unlisted
|
|
178
|
+
in the rake file, or tested with hand-rolled scaffolding instead of the shipped
|
|
179
|
+
matcher library.
|
|
180
|
+
- `docker-compose.yml` MUST be kept current with `demo_app`'s services (Redis,
|
|
181
|
+
Sidekiq, Rails) so `docker compose up` and `docker compose run --rm demo-app
|
|
182
|
+
bin/rails demo:<task>` are the supported way to run the `demo:` rake tasks as
|
|
183
|
+
acceptance tests, with no host-side Ruby/Redis setup required.
|
|
117
184
|
|
|
118
185
|
## Governance
|
|
119
186
|
|
|
@@ -131,4 +198,4 @@ justified in the `Complexity Tracking` table of the plan.
|
|
|
131
198
|
Compliance review: at each MINOR or MAJOR gem release, confirm this constitution
|
|
132
199
|
still accurately reflects the codebase and update as needed.
|
|
133
200
|
|
|
134
|
-
**Version**: 1.
|
|
201
|
+
**Version**: 1.2.0 | **Ratified**: 2025-10-02 | **Last Amended**: 2026-09-09
|
|
@@ -158,6 +158,13 @@ Examples of foundational tasks (adjust based on your project):
|
|
|
158
158
|
- [ ] TXXX Security hardening
|
|
159
159
|
- [ ] TXXX Run quickstart.md validation
|
|
160
160
|
|
|
161
|
+
**Constitution Principle VI — Demo-App Proof of Feature (required for any public API change):**
|
|
162
|
+
|
|
163
|
+
- [ ] TXXX [P] Example reactor in `demo_app/app/reactors/<name>_reactor.rb`
|
|
164
|
+
- [ ] TXXX Register `demo:<name>` task in `demo_app/lib/tasks/demo_reactors.rake` (desc + `[:environment, :flush_redis]`)
|
|
165
|
+
- [ ] TXXX Spec in `demo_app/spec/reactors/<name>_reactor_spec.rb` (`type: :reactor`), using only built-in matchers/helpers from `lib/ruby_reactor/rspec.rb`
|
|
166
|
+
- [ ] TXXX If an assertion needs a matcher that does not exist, add it to `lib/ruby_reactor/rspec/` — never bypass the shared test API
|
|
167
|
+
|
|
161
168
|
---
|
|
162
169
|
|
|
163
170
|
## Dependencies & Execution Order
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.1](https://github.com/arturictus/ruby_reactor/compare/v0.7.0...v0.7.1) (2026-09-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* reactor signal semantics ([#52](https://github.com/arturictus/ruby_reactor/issues/52)) ([9147d06](https://github.com/arturictus/ruby_reactor/commit/9147d066da603c0afc1af6536b6afac7e165a034))
|
|
9
|
+
|
|
3
10
|
## [0.7.0](https://github.com/arturictus/ruby_reactor/compare/v0.6.0...v0.7.0) (2026-09-08)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -120,6 +127,10 @@ was dispatched precisely so the parent would not depend on it. A later step that
|
|
|
120
127
|
reads the result and returns `Failure` triggers compensation normally, so no
|
|
121
128
|
failure is unrecoverable, just not automatic.
|
|
122
129
|
|
|
130
|
+
* Reactor signal semantics: `Skipped` is renamed to `Halt` (the existing clean-stop behaviour, unchanged otherwise), and `Skipped` is reused with new meaning — marking a single step skipped while the reactor continues, with its value flowing to dependants exactly like `Success`. One-line outcome helpers `success!`, `fail!`, `skip!`, and `halt!` end a step immediately from any call depth. `Failure` (and `fail!`) accept a `retry:` spelling alongside the existing `retryable:`. `compensate`/`undo` now default to `Skipped` instead of `Success`, so the execution trace distinguishes rollback logic that ran from rollback logic that was never written.
|
|
131
|
+
|
|
132
|
+
**Migration**: `Skipped(reason: "...")` (the old halt) is now `Halt(reason: "...")`; `result.skipped?` for a clean halt is now `result.halted?`; the `be_skipped` matcher for a clean halt is now `be_halted`. The old call shape raises `ArgumentError` naming `Halt` — there is no silent compatibility path. Run status `:skipped` is renamed `:halted`; contexts persisted by a pre-upgrade version with status `"skipped"` are still read back correctly as halted.
|
|
133
|
+
|
|
123
134
|
## [0.5.4](https://github.com/arturictus/ruby_reactor/compare/v0.5.3...v0.5.4) (2026-06-18)
|
|
124
135
|
|
|
125
136
|
|
data/CLAUDE.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!-- SPECKIT START -->
|
|
2
2
|
For additional context about technologies to be used, project structure,
|
|
3
|
-
shell commands, and other important information, read the current plan
|
|
4
|
-
|
|
3
|
+
shell commands, and other important information, read the current plan:
|
|
4
|
+
`specs/001-reactor-signal-semantics/plan.md`
|
|
5
5
|
<!-- SPECKIT END -->
|
data/README.md
CHANGED
|
@@ -230,11 +230,14 @@ RubyReactor supports two ways to define step logic:
|
|
|
230
230
|
| **Class steps** (preferred) | Real business logic, compensation/undo, shared steps, testability |
|
|
231
231
|
| **Inline blocks** | Quick prototypes, trivial one-liners, documentation examples |
|
|
232
232
|
|
|
233
|
-
Whichever style you use, a step's `run` returns one of
|
|
233
|
+
Whichever style you use, a step's `run` returns one of four signals — all exposed as bare helpers in both class steps and inline blocks:
|
|
234
234
|
|
|
235
235
|
- **`Success(value)`** — step succeeded; `value` flows to dependent steps.
|
|
236
236
|
- **`Failure(error)`** — step failed; the reactor rolls back completed steps (compensate/undo).
|
|
237
|
-
- **`
|
|
237
|
+
- **`Halt(reason:)`** — clean halt: stop the reactor, keep partial progress, **no rollback**. See [Halting a reactor cleanly](documentation/core_concepts.md#halting-a-reactor-cleanly).
|
|
238
|
+
- **`Skipped(value)`** — mark this one step skipped; the reactor continues and `value` flows to dependants exactly like `Success`. See [Skipping a single step](documentation/core_concepts.md#skipping-a-single-step).
|
|
239
|
+
|
|
240
|
+
One-line helpers end a step immediately from any call depth: `success!(value)`, `fail!(error, retry: true)`, `halt!(reason:)`, `skip!(value)` — equivalent to `return`ing the matching signal, usable in `run`, `compensate`, and `undo` bodies.
|
|
238
241
|
|
|
239
242
|
**Class steps** are plain Ruby classes that include `RubyReactor::Step` and implement `run`, and optionally `compensate` and `undo`:
|
|
240
243
|
|
|
@@ -726,7 +729,7 @@ class MonthlyBillingReactor < RubyReactor::Reactor
|
|
|
726
729
|
input :org_id
|
|
727
730
|
|
|
728
731
|
# Run at most once per UTC month per org. Subsequent calls in the same month
|
|
729
|
-
# return RubyReactor::
|
|
732
|
+
# return RubyReactor::Halt without executing any step. Pair with
|
|
730
733
|
# with_lock for strict at-most-one even under concurrent racers.
|
|
731
734
|
with_period(every: :month) { |inputs| "monthly_billing:#{inputs[:org_id]}" }
|
|
732
735
|
|
|
@@ -801,26 +804,38 @@ On contention:
|
|
|
801
804
|
- **Inline** (`Reactor.run`) raises `RubyReactor::Lock::AcquisitionError` / `RubyReactor::Semaphore::AcquisitionError` / `RubyReactor::RateLimit::ExceededError` / `RubyReactor::OrderedLock::WaitError`.
|
|
802
805
|
- **Async** (Sidekiq or ActiveJob) snoozes the job via `perform_in(delay, ...)`. For rate limits the delay uses the error's `retry_after_seconds` hint (precise wakeup — the bucket roll time is known exactly); for locks, semaphores, and ordered-lock waits it's `lock_snooze_base_delay + jitter` (a short re-poll, since a held lock or a live blocker nonce typically clears in milliseconds). Snoozes do not count against the backend's retry budget. After `lock_snooze_max_attempts` snoozes the context is marked failed (ordered-lock waits bypass the cap — see the ordered-lock docs).
|
|
803
806
|
|
|
804
|
-
On dedup hits (period gate already marked), the reactor returns a `RubyReactor::
|
|
807
|
+
On dedup hits (period gate already marked), the reactor returns a `RubyReactor::Halt` result instead — no steps run, no exception:
|
|
805
808
|
|
|
806
809
|
```ruby
|
|
807
810
|
result = MonthlyBillingReactor.run(org_id: 42)
|
|
808
|
-
result.success? # true (
|
|
809
|
-
result.
|
|
811
|
+
result.success? # true (Halt is a Success subclass)
|
|
812
|
+
result.halted? # true on dedup hit, false otherwise
|
|
810
813
|
```
|
|
811
814
|
|
|
812
|
-
A step's `run` block can also return `
|
|
815
|
+
A step's `run` block can also return `Halt(reason: "...")` to stop the reactor cleanly — remaining steps don't execute, **and already-completed steps are NOT compensated**. Use it when the rest of the workflow is unnecessary and partial progress should be kept (`Failure` is for "stop and roll back"). `Halt` is a bare helper just like `Success`/`Failure` (or use the fully-qualified `RubyReactor.Halt(...)`).
|
|
813
816
|
|
|
814
817
|
```ruby
|
|
815
818
|
step :ensure_active do
|
|
816
819
|
argument :user, result(:fetch_user)
|
|
817
820
|
run do |args, _ctx|
|
|
818
|
-
next
|
|
821
|
+
next Halt(reason: "user_opted_out") if args[:user].opted_out?
|
|
819
822
|
Success(args[:user])
|
|
820
823
|
end
|
|
821
824
|
end
|
|
822
825
|
```
|
|
823
826
|
|
|
827
|
+
To skip a *single* step while the reactor continues — the step did nothing, but the rest of the workflow should still run — return `Skipped(value)` instead. The value flows to dependants exactly like a `Success` value, and the step is not enrolled for rollback:
|
|
828
|
+
|
|
829
|
+
```ruby
|
|
830
|
+
step :maybe_sync do
|
|
831
|
+
argument :user, result(:fetch_user)
|
|
832
|
+
run do |args, _ctx|
|
|
833
|
+
next Skipped(args[:user]) if args[:user].already_synced?
|
|
834
|
+
Success(sync!(args[:user]))
|
|
835
|
+
end
|
|
836
|
+
end
|
|
837
|
+
```
|
|
838
|
+
|
|
824
839
|
See [Locks, Semaphores, Rate Limits, Periods & Ordered Locks](documentation/locks_and_semaphores.md) for re-entrancy, auto-extend, multi-window quotas, bucket semantics, owner identity, snooze tuning, ordered-lock assignment + poison-pill semantics, and operational notes.
|
|
825
840
|
|
|
826
841
|
### Map & Parallel Execution
|
|
@@ -1337,7 +1352,7 @@ Comprehensive guide to testing reactors with RubyReactor's testing utilities. Le
|
|
|
1337
1352
|
|
|
1338
1353
|
### [Locks, Semaphores, Rate Limits, Periods & Ordered Locks](documentation/locks_and_semaphores.md)
|
|
1339
1354
|
|
|
1340
|
-
Coordinate access to shared resources across processes with Redis-backed primitives: exclusive locks (`with_lock`), concurrency-limiting semaphores (`with_semaphore`), fixed-window rate limits with multi-window quotas (`with_rate_limit`), calendar-bucketed dedup (`with_period`, returning `
|
|
1355
|
+
Coordinate access to shared resources across processes with Redis-backed primitives: exclusive locks (`with_lock`), concurrency-limiting semaphores (`with_semaphore`), fixed-window rate limits with multi-window quotas (`with_rate_limit`), calendar-bucketed dedup (`with_period`, returning `Halt` results), and strict sequential ordering via a monotonically increasing nonce assigned at enqueue (`with_ordered_lock`). Covers re-entrancy across composed reactors, TTL auto-extend, inline-vs-async contention behavior, smart `retry_after` snoozes for rate limits, snooze tuning, the token-based semaphore safety model, once-per-day/month/year scheduling patterns, ordered-lock counter reset on drain, poison-pill timeouts, and deadlock-safe composition rules.
|
|
1341
1356
|
|
|
1342
1357
|
### [Middlewares & OpenTelemetry](documentation/middlewares.md)
|
|
1343
1358
|
|