ruby_reactor 0.8.2 → 0.8.3
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/speckit-review/SKILL.md +324 -0
- data/.release-please-manifest.json +1 -1
- data/.specify/extensions.yml +10 -0
- data/.specify/feature.json +1 -1
- data/.specify/workflows/speckit/workflow.yml +13 -1
- data/.specify/workflows/workflow-registry.json +2 -2
- data/CHANGELOG.md +82 -0
- data/CLAUDE.md +2 -2
- data/README.md +35 -2
- data/lib/ruby_reactor/adapters/active_job/router.rb +19 -0
- data/lib/ruby_reactor/adapters/sidekiq/router.rb +21 -0
- data/lib/ruby_reactor/context.rb +26 -0
- data/lib/ruby_reactor/context_serializer.rb +4 -2
- data/lib/ruby_reactor/dsl/interrupt_builder.rb +14 -0
- data/lib/ruby_reactor/dsl/lockable.rb +76 -21
- data/lib/ruby_reactor/dsl/step_builder.rb +112 -1
- data/lib/ruby_reactor/error/async_result_pending.rb +1 -1
- data/lib/ruby_reactor/error/execution_parked.rb +16 -0
- data/lib/ruby_reactor/error/reactor_contention_park.rb +26 -0
- data/lib/ruby_reactor/error/step_contention_park.rb +26 -0
- data/lib/ruby_reactor/executor/async_step_dispatch.rb +109 -3
- data/lib/ruby_reactor/executor/compensation_manager.rb +99 -17
- data/lib/ruby_reactor/executor/ordered_lock_support.rb +76 -44
- data/lib/ruby_reactor/executor/result_handler.rb +31 -11
- data/lib/ruby_reactor/executor/retry_manager.rb +9 -1
- data/lib/ruby_reactor/executor/step_coordination.rb +788 -0
- data/lib/ruby_reactor/executor/step_executor.rb +115 -11
- data/lib/ruby_reactor/executor.rb +90 -20
- data/lib/ruby_reactor/map/element_executor.rb +24 -2
- data/lib/ruby_reactor/map/helpers.rb +35 -11
- data/lib/ruby_reactor/max_retries_exhausted_failure.rb +2 -2
- data/lib/ruby_reactor/open_telemetry.rb +61 -24
- data/lib/ruby_reactor/retry_context.rb +31 -2
- data/lib/ruby_reactor/rspec/helpers.rb +15 -0
- data/lib/ruby_reactor/rspec/matchers.rb +92 -0
- data/lib/ruby_reactor/rspec/test_subject.rb +7 -1
- data/lib/ruby_reactor/step/async_reactor_step.rb +40 -24
- data/lib/ruby_reactor/step/compose_step.rb +14 -3
- data/lib/ruby_reactor/step.rb +49 -7
- data/lib/ruby_reactor/step_sweeper.rb +29 -1
- data/lib/ruby_reactor/step_worker.rb +260 -37
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/api.rb +72 -7
- data/lib/ruby_reactor/web/coordination_serializer.rb +120 -2
- data/lib/ruby_reactor/web/public/assets/{index-Dw4KV4QY.js → index-CeZU-ESu.js} +9 -9
- data/lib/ruby_reactor/web/public/index.html +1 -1
- data/lib/ruby_reactor/worker.rb +56 -30
- data/lib/ruby_reactor.rb +27 -5
- data/specs/future_improvements.md +250 -0
- metadata +8 -28
- data/specs/002-step-input-contracts/checklists/requirements.md +0 -49
- data/specs/002-step-input-contracts/contracts/dsl-surface.md +0 -193
- data/specs/002-step-input-contracts/data-model.md +0 -115
- data/specs/002-step-input-contracts/plan.md +0 -165
- data/specs/002-step-input-contracts/quickstart.md +0 -170
- data/specs/002-step-input-contracts/research.md +0 -233
- data/specs/002-step-input-contracts/spec.md +0 -359
- data/specs/002-step-input-contracts/tasks.md +0 -367
- data/specs/004-inheritable-step-class/checklists/requirements.md +0 -40
- data/specs/004-inheritable-step-class/contracts/step-lifecycle.md +0 -85
- data/specs/004-inheritable-step-class/data-model.md +0 -116
- data/specs/004-inheritable-step-class/plan.md +0 -174
- data/specs/004-inheritable-step-class/quickstart.md +0 -112
- data/specs/004-inheritable-step-class/research.md +0 -308
- data/specs/004-inheritable-step-class/spec.md +0 -316
- data/specs/004-inheritable-step-class/tasks.md +0 -258
- data/specs/active_job.md +0 -259
- data/specs/deferred-003-step-lock-declarations/checklists/requirements.md +0 -51
- data/specs/deferred-003-step-lock-declarations/contracts/dsl-surface.md +0 -154
- data/specs/deferred-003-step-lock-declarations/data-model.md +0 -131
- data/specs/deferred-003-step-lock-declarations/plan.md +0 -166
- data/specs/deferred-003-step-lock-declarations/quickstart.md +0 -169
- data/specs/deferred-003-step-lock-declarations/research.md +0 -196
- data/specs/deferred-003-step-lock-declarations/spec.md +0 -447
- data/specs/deferred-003-step-lock-declarations/tasks.md +0 -572
- data/specs/possible_feature.md +0 -22
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ac772fc307621dcf2fba69d726aeb670a34201a0c06e17e1e9213b4779f4cd03
|
|
4
|
+
data.tar.gz: 5ed6d5e4251643fc16fafaf48ecde75aa31f95cc09f9b1fe9d0b879b190fa61b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c61a1ec89ce310154e5bd9d2b09c3dc997e682a83d1bb5b18a87d47128f792133e27e7c8648db699cbfa5de8830522dad9bdfbb7b953addf89273fa3eb3042e0
|
|
7
|
+
data.tar.gz: b78511087cfccec3eb6912000c3d744fa0f1cabca1f87eed9a500d223eb2aa4e6d1e4219a85eb2c6774f54e14493179d3c86ba836e4012cf9a86ec3a553955c1
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "speckit-review"
|
|
3
|
+
description: "Review the code changes produced by /speckit-implement against RubyReactor's runtime invariants: saga unwind/compensation/undo, retry semantics, async isolation, lock safety without deadlock, validation ordering, and the behavioral claims made in README/documentation."
|
|
4
|
+
argument-hint: "Optional: review scope (e.g. 'locks only', a path, or a git ref to diff against)"
|
|
5
|
+
compatibility: "Requires spec-kit project structure with .specify/ directory and a git repository"
|
|
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). Treat it as a scope
|
|
19
|
+
narrowing hint only — it MAY reduce which files are inspected, it MUST NOT disable any gate
|
|
20
|
+
below for files that remain in scope.
|
|
21
|
+
|
|
22
|
+
## Pre-Execution Checks
|
|
23
|
+
|
|
24
|
+
**Check for extension hooks (before review)**:
|
|
25
|
+
|
|
26
|
+
- Check if `.specify/extensions.yml` exists in the project root.
|
|
27
|
+
- If it exists, read it and look for entries under the `hooks.before_review` key
|
|
28
|
+
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
|
|
29
|
+
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
|
|
30
|
+
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
|
|
31
|
+
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
|
|
32
|
+
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
|
|
33
|
+
- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
|
|
34
|
+
- For each executable hook, output the following based on its `optional` flag:
|
|
35
|
+
- **Optional hook** (`optional: true`):
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
## Extension Hooks
|
|
39
|
+
|
|
40
|
+
**Optional Pre-Hook**: {extension}
|
|
41
|
+
Command: `/{command}`
|
|
42
|
+
Description: {description}
|
|
43
|
+
|
|
44
|
+
Prompt: {prompt}
|
|
45
|
+
To execute: `/{command}`
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- **Mandatory hook** (`optional: false`):
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
## Extension Hooks
|
|
52
|
+
|
|
53
|
+
**Automatic Pre-Hook**: {extension}
|
|
54
|
+
Executing: `/{command}`
|
|
55
|
+
EXECUTE_COMMAND: {command}
|
|
56
|
+
|
|
57
|
+
Wait for the result of the hook command before proceeding to the Goal.
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
|
|
61
|
+
|
|
62
|
+
## Goal
|
|
63
|
+
|
|
64
|
+
Review the **code changes** made for the current feature (normally right after
|
|
65
|
+
`/speckit-implement`) and decide whether they are safe to merge. The review answers one
|
|
66
|
+
question per gate, with file/line evidence, and ends in a single verdict: `PASS`,
|
|
67
|
+
`PASS WITH FINDINGS`, or `BLOCKED`.
|
|
68
|
+
|
|
69
|
+
This command is **read-only**. It MUST NOT edit source, specs, docs, `tasks.md`, or git
|
|
70
|
+
state. Remediation is handed off to `/speckit-converge` (append tasks) or
|
|
71
|
+
`/speckit-implement` (fix), not performed here.
|
|
72
|
+
|
|
73
|
+
It differs from its neighbours:
|
|
74
|
+
|
|
75
|
+
- `/speckit-analyze` → consistency **between artifacts** (spec/plan/tasks).
|
|
76
|
+
- `/speckit-converge` → what the artifacts demand that the code does **not yet do**.
|
|
77
|
+
- `/speckit-review` (this one) → whether the code that now exists is **correct, safe, and
|
|
78
|
+
honest** at runtime — saga unwind, retries, async isolation, locks, validation, and the
|
|
79
|
+
claims the docs make about all of it. Constitution/process compliance is out of scope
|
|
80
|
+
here; `/speckit-plan` and `/speckit-converge` already cover it.
|
|
81
|
+
|
|
82
|
+
## Execution Steps
|
|
83
|
+
|
|
84
|
+
### 1. Establish Review Scope
|
|
85
|
+
|
|
86
|
+
Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks`
|
|
87
|
+
from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS. Derive:
|
|
88
|
+
|
|
89
|
+
- SPEC = FEATURE_DIR/spec.md, PLAN = FEATURE_DIR/plan.md, TASKS = FEATURE_DIR/tasks.md
|
|
90
|
+
|
|
91
|
+
These are context for *what the change was meant to do* — the gates below judge the code,
|
|
92
|
+
not the artifacts.
|
|
93
|
+
|
|
94
|
+
If the prerequisites script fails (no active feature), do **not** stop: fall back to a
|
|
95
|
+
pure diff review and say so in the report header — none of the gates below require spec
|
|
96
|
+
artifacts.
|
|
97
|
+
|
|
98
|
+
Compute the change set:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
git rev-parse --abbrev-ref HEAD
|
|
102
|
+
git merge-base HEAD main
|
|
103
|
+
git diff --stat $(git merge-base HEAD main)...HEAD
|
|
104
|
+
git diff $(git merge-base HEAD main)...HEAD
|
|
105
|
+
git status --porcelain # uncommitted work is in scope too
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
If the user supplied a git ref in `$ARGUMENTS`, diff against that instead of `main`.
|
|
109
|
+
Read the **full current content** of every non-trivially changed file under `lib/` —
|
|
110
|
+
a diff hunk alone is not enough to reason about unwind order or lock release paths.
|
|
111
|
+
|
|
112
|
+
Load `README.md` and the files under `./documentation/` that cover the touched areas
|
|
113
|
+
(`DAG.md`, `background_and_async.md`, `locks_and_semaphores.md`, `retry_configuration.md`,
|
|
114
|
+
`interrupts.md`, `composition.md`, `core_concepts.md`, `testing.md`).
|
|
115
|
+
|
|
116
|
+
### 2. Run the Gates
|
|
117
|
+
|
|
118
|
+
Every gate produces: `PASS`, `FINDING`, or `BLOCKER`, each with concrete evidence
|
|
119
|
+
(`path/to/file.rb:LINE`). A gate with no changed code in its area is `N/A` — say so
|
|
120
|
+
explicitly rather than silently passing it.
|
|
121
|
+
|
|
122
|
+
#### G1 — Distributed Rules and Documented Claims Hold
|
|
123
|
+
|
|
124
|
+
The gem is consumed as a distributed coordinator; the docs are a contract.
|
|
125
|
+
|
|
126
|
+
- Every behavioral claim in `README.md` and `./documentation/` that touches changed code is
|
|
127
|
+
still literally true. Quote the claim, then cite the code that satisfies or breaks it.
|
|
128
|
+
A claim the code no longer honors is a **BLOCKER** (fix the code or fix the claim —
|
|
129
|
+
the report says which, it does not do either).
|
|
130
|
+
- Multi-process correctness: no state that must be shared lives in a process-local ivar,
|
|
131
|
+
class variable, or memoized constant where a second worker process would miss it;
|
|
132
|
+
Redis keys carry the discriminators (reactor/step/run/lock identity) needed to avoid
|
|
133
|
+
cross-run or cross-worker collision; TTLs exist wherever a crashed process would
|
|
134
|
+
otherwise leak a key.
|
|
135
|
+
- Crash safety: for each new Redis write, ask what happens if the process dies
|
|
136
|
+
immediately before and immediately after it. A window that strands a run with no
|
|
137
|
+
sweeper/recovery path (`step_sweeper.rb`, `sweeper.rb`) is a BLOCKER.
|
|
138
|
+
- Serialization: anything crossing a process boundary round-trips through
|
|
139
|
+
`context_serializer.rb` without silent loss (watch `false`/`nil` handling, symbol vs
|
|
140
|
+
string keys, and context size limits).
|
|
141
|
+
|
|
142
|
+
#### G2 — Saga Robustness: Predictable Unwind, Compensation, Undo
|
|
143
|
+
|
|
144
|
+
The core promise of the library: a run either completes or unwinds cleanly. For every step
|
|
145
|
+
touched or added:
|
|
146
|
+
|
|
147
|
+
- A step producing a side effect declares rollback (`compensate`/`undo`) — and the
|
|
148
|
+
reviewed code actually routes failures into it.
|
|
149
|
+
- **Unwind order is deterministic**: compensation runs in reverse completion order per the
|
|
150
|
+
DAG, and is reproducible across runs of the same failure. Any dependence on hash order,
|
|
151
|
+
thread completion order, or wall-clock ordering is a BLOCKER.
|
|
152
|
+
- **Compensation is idempotent and re-entrant**: running it twice (crash mid-unwind, then
|
|
153
|
+
sweeper retry) leaves the same state.
|
|
154
|
+
- A failure *inside* compensation/undo is handled explicitly (`compensation_error.rb`,
|
|
155
|
+
`undo_error.rb`) — never swallowed, never allowed to abort the remaining unwind silently.
|
|
156
|
+
- No orphaned steps: a step that started cannot end with neither a result nor a
|
|
157
|
+
compensation record. Partial execution with no recovery path is forbidden.
|
|
158
|
+
- Interrupts/pause/resume remain first-class: a paused run resumes to the same DAG position
|
|
159
|
+
with the same context, and unwinding a paused or resumed run compensates exactly the
|
|
160
|
+
steps that actually ran — not more, not fewer.
|
|
161
|
+
- Async steps that fail after dispatch still reach compensation of their upstream steps.
|
|
162
|
+
|
|
163
|
+
#### G3 — Retries Do What They Claim
|
|
164
|
+
|
|
165
|
+
- Retry counting is per-step and survives process boundaries (`retry_context.rb`,
|
|
166
|
+
`retry_queued_result.rb`); a retry that re-enters through a worker does not reset the
|
|
167
|
+
counter or double-count it.
|
|
168
|
+
- `max_retries` exhaustion produces `max_retries_exhausted_failure.rb` and then flows into
|
|
169
|
+
the same compensation path as any other failure.
|
|
170
|
+
- Backoff/delay semantics match `documentation/retry_configuration.md` exactly (units,
|
|
171
|
+
first-attempt-vs-retry counting, jitter, caps).
|
|
172
|
+
- A retried step re-acquires whatever locks/semaphores/rate-limit tokens it needs, and
|
|
173
|
+
releases those from the failed attempt first — no token or lock leak per retry.
|
|
174
|
+
- Retries do not re-run already-successful upstream steps and do not skip validation.
|
|
175
|
+
- Retry of a step with side effects either compensates the failed attempt first or is
|
|
176
|
+
documented as idempotent — whichever the docs claim, the code must do.
|
|
177
|
+
|
|
178
|
+
#### G4 — Async Reactors and Steps Are Isolated
|
|
179
|
+
|
|
180
|
+
- No shared mutable state between concurrently executing steps: context writes are
|
|
181
|
+
per-step and merged, not mutated in place across threads/processes.
|
|
182
|
+
- An async step's failure is contained — it fails its own step and the run's declared
|
|
183
|
+
policy, and cannot corrupt or cross-talk into a sibling async step or another reactor
|
|
184
|
+
run (`executor/async_step_dispatch.rb`, `async_waiter.rb`, `step_worker.rb`).
|
|
185
|
+
- Job payloads carry full identity (run id, step name, attempt) so a worker cannot act on
|
|
186
|
+
the wrong run; late/duplicate job delivery is a no-op, not a second execution.
|
|
187
|
+
- Waiting is bounded: every wait has a timeout path (`async_wait_timeout_error.rb`) and
|
|
188
|
+
`async_result_pending.rb` handling that cannot park forever.
|
|
189
|
+
- Adapter isolation holds: `adapters/sidekiq` and `adapters/active_job` stay
|
|
190
|
+
interchangeable; no adapter-specific behavior leaks into the executor.
|
|
191
|
+
|
|
192
|
+
#### G5 — Locks Protect Resources Without Deadlocking
|
|
193
|
+
|
|
194
|
+
Covers `lock.rb`, `ordered_lock.rb`, `semaphore.rb`, `rate_limit*.rb`, `period.rb`,
|
|
195
|
+
`dsl/lockable.rb`, and this feature's independent step-lock work.
|
|
196
|
+
|
|
197
|
+
- Every acquire has a release on **all** paths: success, failure, compensation, undo,
|
|
198
|
+
exception, pause, and process crash (TTL). Show the release path for each new acquire.
|
|
199
|
+
- **Deadlock freedom**: multi-lock acquisition follows one global deterministic order
|
|
200
|
+
(that is what `ordered_lock.rb` is for) — no step acquires A-then-B while another can
|
|
201
|
+
acquire B-then-A. Any new acquisition site must be shown to respect the order.
|
|
202
|
+
- No lock is held across an async dispatch or a blocking wait unless that is explicitly
|
|
203
|
+
designed, documented, and TTL-bounded.
|
|
204
|
+
- Ownership tokens: release only releases a lock this run still owns (fencing/ownership
|
|
205
|
+
check), so a TTL-expired holder cannot release a lock now held by someone else.
|
|
206
|
+
- Semaphore/rate-limit accounting cannot drift: crashed holders are reclaimed, and
|
|
207
|
+
double-release cannot inflate available tokens.
|
|
208
|
+
- Behavior matches `documentation/locks_and_semaphores.md` (blocking vs non-blocking,
|
|
209
|
+
timeout, what a contended run returns).
|
|
210
|
+
|
|
211
|
+
#### G6 — Validations Predictable and Run Before `run`
|
|
212
|
+
|
|
213
|
+
- Input validation executes **before** the step's `run` body — always, including on retry,
|
|
214
|
+
resume-from-pause, and async re-entry paths. A path that reaches `run` with unvalidated
|
|
215
|
+
input is a BLOCKER.
|
|
216
|
+
- Validation failures produce `input_validation_error.rb` / `validation_error.rb` as
|
|
217
|
+
failures with the documented shape — never exceptions escaping the executor, never a
|
|
218
|
+
silent skip.
|
|
219
|
+
- A validation failure produces **no side effects** and unwinds any already-completed steps
|
|
220
|
+
per G2.
|
|
221
|
+
- dry-validation schemas stay inside the reactor/step DSL (`dsl/validation_helpers.rb`,
|
|
222
|
+
`validation/`), not scattered into application code.
|
|
223
|
+
- Validation is deterministic: same input → same outcome, no I/O, no clock, no network
|
|
224
|
+
inside a schema.
|
|
225
|
+
- Defaults/coercions applied by validation are visible to `run` and to serialization
|
|
226
|
+
consistently (this is where `false`/`nil` bugs hide).
|
|
227
|
+
|
|
228
|
+
### 3. Verify Dynamically Where Cheap
|
|
229
|
+
|
|
230
|
+
Evidence beats reasoning. When the change touches runtime behavior, run what already
|
|
231
|
+
exists rather than asserting:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
bundle exec rubocop
|
|
235
|
+
bundle exec rspec <specs covering the changed area>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
For user-facing behavior, run it end to end against real Redis:
|
|
239
|
+
`docker compose run --rm demo-app bin/rails demo:<task>` (or `/demo-app-e2e-verify`).
|
|
240
|
+
Report command output honestly — a failing or skipped check is a finding with its output
|
|
241
|
+
quoted, never "should pass". If a check is not run, say which and why.
|
|
242
|
+
|
|
243
|
+
### 4. Report
|
|
244
|
+
|
|
245
|
+
Output in-session (no file writes):
|
|
246
|
+
|
|
247
|
+
```markdown
|
|
248
|
+
## Review — <feature> (<base>...<head>)
|
|
249
|
+
|
|
250
|
+
**Verdict**: BLOCKED | PASS WITH FINDINGS | PASS
|
|
251
|
+
|
|
252
|
+
| Gate | Result | Evidence |
|
|
253
|
+
|------|--------|----------|
|
|
254
|
+
| G1 Distributed + claims | BLOCKER | documentation/locks_and_semaphores.md:88 claims X; lib/ruby_reactor/lock.rb:142 does Y |
|
|
255
|
+
| G2 Saga unwind/compensation | ... | ... |
|
|
256
|
+
| G3 Retries | ... | ... |
|
|
257
|
+
| G4 Async isolation | ... | ... |
|
|
258
|
+
| G5 Locks / deadlock | ... | ... |
|
|
259
|
+
| G6 Validations | ... | ... |
|
|
260
|
+
|
|
261
|
+
### Findings
|
|
262
|
+
|
|
263
|
+
**[BLOCKER] F1 — <one-line defect>** (G5, `lib/ruby_reactor/lock.rb:142`)
|
|
264
|
+
Failure scenario: <concrete interleaving/inputs → wrong state or hang>
|
|
265
|
+
Fix: <smallest change that closes it>
|
|
266
|
+
|
|
267
|
+
### Checks Run
|
|
268
|
+
- `bundle exec rubocop` → <result>
|
|
269
|
+
- `bundle exec rspec …` → <result>
|
|
270
|
+
- not run: <what, why>
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Rules for the report:
|
|
274
|
+
|
|
275
|
+
- Order findings BLOCKER → FINDING, most severe first.
|
|
276
|
+
- Every finding names a **concrete failure scenario** (interleaving, crash point, or input
|
|
277
|
+
that produces the wrong result). A finding that cannot state one is speculation — drop it.
|
|
278
|
+
- No finding without a file:line. No gate row without either evidence or `N/A`.
|
|
279
|
+
- Verdict is `BLOCKED` if any gate is a BLOCKER, `PASS WITH FINDINGS` if only findings
|
|
280
|
+
remain, `PASS` only when every in-scope gate passed **and** the checks in Step 3 ran.
|
|
281
|
+
|
|
282
|
+
### 5. Handoff
|
|
283
|
+
|
|
284
|
+
- `BLOCKED` / `PASS WITH FINDINGS`: recommend `/speckit-converge` to append the remediation
|
|
285
|
+
work as traceable tasks, then `/speckit-implement`. Do not fix anything here.
|
|
286
|
+
- `PASS`: recommend proceeding to PR, carrying the gate table into the PR description as
|
|
287
|
+
the reviewer-facing evidence.
|
|
288
|
+
|
|
289
|
+
### 6. Check for extension hooks
|
|
290
|
+
|
|
291
|
+
After producing the report, check if `.specify/extensions.yml` exists in the project root.
|
|
292
|
+
|
|
293
|
+
- If it exists, read it and look for entries under the `hooks.after_review` key
|
|
294
|
+
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
|
|
295
|
+
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
|
|
296
|
+
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
|
|
297
|
+
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
|
|
298
|
+
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
|
|
299
|
+
- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`.
|
|
300
|
+
- For each executable hook, output the following based on its `optional` flag:
|
|
301
|
+
- **Optional hook** (`optional: true`):
|
|
302
|
+
|
|
303
|
+
```text
|
|
304
|
+
## Extension Hooks
|
|
305
|
+
|
|
306
|
+
**Optional Hook**: {extension}
|
|
307
|
+
Command: `/{command}`
|
|
308
|
+
Description: {description}
|
|
309
|
+
|
|
310
|
+
Prompt: {prompt}
|
|
311
|
+
To execute: `/{command}`
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
- **Mandatory hook** (`optional: false`):
|
|
315
|
+
|
|
316
|
+
```text
|
|
317
|
+
## Extension Hooks
|
|
318
|
+
|
|
319
|
+
**Automatic Hook**: {extension}
|
|
320
|
+
Executing: `/{command}`
|
|
321
|
+
EXECUTE_COMMAND: {command}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
|
data/.specify/extensions.yml
CHANGED
|
@@ -21,3 +21,13 @@ hooks:
|
|
|
21
21
|
prompt: Execute speckit.agent-context.update?
|
|
22
22
|
description: Refresh agent context after planning
|
|
23
23
|
condition: null
|
|
24
|
+
after_implement:
|
|
25
|
+
- extension: project
|
|
26
|
+
command: speckit.review
|
|
27
|
+
enabled: true
|
|
28
|
+
optional: false
|
|
29
|
+
priority: 10
|
|
30
|
+
prompt: Execute speckit.review?
|
|
31
|
+
description: Review the implemented changes against RubyReactor
|
|
32
|
+
runtime invariants (saga, retries, async isolation, locks, validation, documented claims)
|
|
33
|
+
condition: null
|
data/.specify/feature.json
CHANGED
|
@@ -4,7 +4,7 @@ workflow:
|
|
|
4
4
|
name: "Full SDD Cycle"
|
|
5
5
|
version: "1.0.0"
|
|
6
6
|
author: "GitHub"
|
|
7
|
-
description: "Runs specify → plan → tasks → implement with review gates"
|
|
7
|
+
description: "Runs specify → plan → tasks → implement → review with review gates"
|
|
8
8
|
|
|
9
9
|
requires:
|
|
10
10
|
# 0.8.5 is the first release with engine-side resolution of the
|
|
@@ -75,3 +75,15 @@ steps:
|
|
|
75
75
|
integration: "{{ inputs.integration }}"
|
|
76
76
|
input:
|
|
77
77
|
args: "{{ inputs.spec }}"
|
|
78
|
+
|
|
79
|
+
- id: review
|
|
80
|
+
command: speckit.review
|
|
81
|
+
integration: "{{ inputs.integration }}"
|
|
82
|
+
input:
|
|
83
|
+
args: "{{ inputs.spec }}"
|
|
84
|
+
|
|
85
|
+
- id: review-gate
|
|
86
|
+
type: gate
|
|
87
|
+
message: "Review findings above (constitution + saga/retry/async/lock/validation gates). Approve to finish, reject to send remaining work back through converge/implement."
|
|
88
|
+
options: [approve, reject]
|
|
89
|
+
on_reject: abort
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
"speckit": {
|
|
5
5
|
"name": "Full SDD Cycle",
|
|
6
6
|
"version": "1.0.0",
|
|
7
|
-
"description": "Runs specify
|
|
7
|
+
"description": "Runs specify → plan → tasks → implement → review with review gates",
|
|
8
8
|
"source": "bundled",
|
|
9
9
|
"installed_at": "2026-06-23T23:29:12.184843+00:00",
|
|
10
10
|
"updated_at": "2026-06-23T23:29:12.184848+00:00"
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
-
}
|
|
13
|
+
}
|
data/CHANGELOG.md
CHANGED
|
@@ -80,6 +80,19 @@
|
|
|
80
80
|
|
|
81
81
|
### Features
|
|
82
82
|
|
|
83
|
+
* **Step-scoped coordination.** Steps can declare `with_lock`, `with_semaphore`, `with_rate_limit`,
|
|
84
|
+
`with_period`, and `with_ordered_lock` — the same macros as the reactor form, keyed on the step's
|
|
85
|
+
own resolved arguments instead of the reactor's inputs — so one step of a workflow can be
|
|
86
|
+
serialized (or rate-limited, deduped, or strictly ordered) without serializing the whole
|
|
87
|
+
workflow. Works on class steps and inline `step :x do ... end` blocks (or both on one step —
|
|
88
|
+
taken once, in one fixed order); a direct `MyStep.run(args)` call is protected too, as its own
|
|
89
|
+
unit of work that waits then fails. Contention parks the execution in a worker (bounded by
|
|
90
|
+
`lock_snooze_max_attempts`, never consuming the step's own `retries` budget; the parked step
|
|
91
|
+
releases what it took and never spends a rate-limit slot on a park) and waits-then-fails
|
|
92
|
+
synchronously. Re-entrancy, the async dispatch deadlock guard, and rollback
|
|
93
|
+
(`compensate`/`undo` re-take lock/semaphore only) all follow the same rules as reactor-level
|
|
94
|
+
coordination. See
|
|
95
|
+
[Step-Scoped Coordination](documentation/locks_and_semaphores.md#step-scoped-coordination).
|
|
83
96
|
* **Step input contracts.** A step class declares its own inputs with `input :name, :type, **predicates`
|
|
84
97
|
(plus `optional:`, `default:`, `redact:`, the `do |i| ... end` macro block and `validate:`) and
|
|
85
98
|
cross-field rules with `validate_inputs`. The contract is enforced before `run` on every path
|
|
@@ -103,6 +116,20 @@
|
|
|
103
116
|
* A step that returns another unit's validation failure (e.g. an `async_step` reader propagating
|
|
104
117
|
the worker's `Failure`) keeps its `validation_errors` on the reactor's final failure.
|
|
105
118
|
|
|
119
|
+
* **`rollback_wait:` on step `with_lock` / `with_semaphore`.** How long a step's `undo` /
|
|
120
|
+
`compensate` waits to re-take its key. Defaults to the lock's `ttl` (60 s for a semaphore);
|
|
121
|
+
rollback still never parks, so in a worker the wait blocks the thread.
|
|
122
|
+
* **`Failure#rollback_failures`.** Every undo or compensation that did not complete —
|
|
123
|
+
`{ step:, kind: :undo | :compensate, key:, reason: :coordination_unavailable | :returned_failure | :raised, message: }`,
|
|
124
|
+
including composed children's (flattened). Always an Array; part of `Failure#to_h` and the
|
|
125
|
+
stored failure of a background run.
|
|
126
|
+
* **`:snooze_step` middleware event** (`on_snooze_step(step_name, error, context)`): a step's
|
|
127
|
+
attempt ended in a park — it lost its own contention in a worker, or it is a `compose` step whose
|
|
128
|
+
child parked — at any nesting depth. Never `:failed_step`. A step whose own arguments wait on a
|
|
129
|
+
background result parks before it starts, so it fires neither `:start_step` nor `:snooze_step`.
|
|
130
|
+
The OpenTelemetry middleware closes the span as `step.status = "parked"`.
|
|
131
|
+
* **`have_rollback_failure(step)` matcher**, with `.for_key(key)` and `.because(reason)`.
|
|
132
|
+
|
|
106
133
|
### Deprecations
|
|
107
134
|
|
|
108
135
|
* Rules on `argument` (`argument :x, src, :type, **predicates`) and `validate_args` keep working
|
|
@@ -128,6 +155,61 @@
|
|
|
128
155
|
* A class step that calls `halt!` under `async_step` is recorded as a halt rather than an
|
|
129
156
|
ordinary `nil` success, and `result(:step)` hands the reader the `Halt` — the same way it
|
|
130
157
|
already hands over a `Failure`.
|
|
158
|
+
* Step coordination (F1): a step's undo is no longer dropped because another execution holds its
|
|
159
|
+
key at that moment — it waits up to `rollback_wait`, and one that still cannot run is reported
|
|
160
|
+
on `Failure#rollback_failures` instead of only in the trace.
|
|
161
|
+
* Step coordination (F2): a park inside a composed child no longer releases the parent's reactor
|
|
162
|
+
lock or semaphore, and no longer charges the parent's rate limit again on redelivery — every
|
|
163
|
+
level keeps its own holds and is admitted once, at any depth.
|
|
164
|
+
* Step coordination (F3): a synchronous execution that reaches a strict step-level ordered lock
|
|
165
|
+
out of turn fails without poisoning the chain; later arrivals run instead of being skipped.
|
|
166
|
+
* Step coordination (F4): the docs name `context.coordinating_step` (not `current_step`) as the
|
|
167
|
+
attribution for coordination middleware events.
|
|
168
|
+
* Step coordination (F5): a parked `async_step` no longer overwrites its parent's saved context;
|
|
169
|
+
its park state (ordered-lock position, "waiting" marker) lives on its own step result record.
|
|
170
|
+
* Step coordination (F6): documented the cross-level key-ordering rule that avoids two workflows
|
|
171
|
+
waiting on each other's reactor and step locks.
|
|
172
|
+
* Step coordination (F7): a step whose ordered-lock batch expired before its retry is skipped
|
|
173
|
+
(`Skipped(reason: :ordered_lock_stale_batch)`) instead of running unordered.
|
|
174
|
+
* Step coordination (F8): a step-level ordered-lock heartbeat stops when the step body exits
|
|
175
|
+
abnormally (e.g. `Sidekiq::Shutdown`), so the poison pill can release the position.
|
|
176
|
+
* Step coordination (F9): a step class invoked directly from another step's body names itself in
|
|
177
|
+
contention errors and coordination events, not the calling step.
|
|
178
|
+
* An `async_step` no longer writes its parent's context when it finishes (it already stopped
|
|
179
|
+
doing so on a park). Its older snapshot overwrote whatever the parent saved while the unit ran —
|
|
180
|
+
a parent could revert to "running" and lose later steps' results. The unit's run (arguments,
|
|
181
|
+
attempts, start time) now lives on its Step Result Record, and the dashboard rebuilds it from the
|
|
182
|
+
parent's link, at any composition depth. `context.execution_trace` no longer has a `:run` entry
|
|
183
|
+
for an `async_step`, and changes an `async_step` body makes to `context` are not persisted.
|
|
184
|
+
* A park after a fan-out map (a later step's contention, or an awaited background result) now
|
|
185
|
+
requeues the parent on its own worker instead of escaping the map collector and leaving the run
|
|
186
|
+
"running" forever; the collector also no longer re-saves the parent after resuming it, which
|
|
187
|
+
could overwrite a newer save by the parent's next worker.
|
|
188
|
+
* An `async_step` refused at dispatch because it would deadlock on a key the reactor holds is no
|
|
189
|
+
longer compensated. It was never dispatched or run. The steps before it still roll back.
|
|
190
|
+
* A step of a composed child that reads a not-yet-finished background result in a worker (F10)
|
|
191
|
+
parks the execution, keeping the child's lock, instead of failing the parent with
|
|
192
|
+
"async result … still pending".
|
|
193
|
+
* A background reactor whose own `with_lock` / `with_semaphore` is busy when it starts no longer
|
|
194
|
+
charges its `with_rate_limit` again on every snoozed redelivery.
|
|
195
|
+
* A `compensate` that raises no longer stops the rollback: the completed steps are still undone,
|
|
196
|
+
and the reactor fails with `CompensationError` and a `reason: :raised` rollback failure.
|
|
197
|
+
* A composed child whose own `with_lock` / `with_semaphore` / `with_rate_limit` is busy inside a
|
|
198
|
+
worker now parks the execution and snoozes the job instead of failing the parent. After
|
|
199
|
+
`lock_snooze_max_attempts` parks it fails the `compose` step, which rolls the parent back.
|
|
200
|
+
* A park that comes up through a `compose` step (the child's contention, or its wait on a
|
|
201
|
+
background result) no longer uses up that step's `retries` budget.
|
|
202
|
+
* Docs: `on_snooze_step` is documented for what it covers — a step's own contention park and a
|
|
203
|
+
`compose` step whose child parked, not a step whose own arguments wait on a background result.
|
|
204
|
+
* Docs: a park keeps each level's lock without a second `:lock_acquired` only while the gap stays
|
|
205
|
+
within the lock's `ttl`; a lapsed lock is acquired again.
|
|
206
|
+
|
|
207
|
+
## [0.8.3](https://github.com/arturictus/ruby_reactor/compare/v0.8.2...v0.8.3) (2026-09-25)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
### Features
|
|
211
|
+
|
|
212
|
+
* move locks declarations to step classes ([#56](https://github.com/arturictus/ruby_reactor/issues/56)) ([a7862ca](https://github.com/arturictus/ruby_reactor/commit/a7862ca765ae706aebb361d9eadcf4c0ead12caf))
|
|
131
213
|
|
|
132
214
|
## [0.8.2](https://github.com/arturictus/ruby_reactor/compare/v0.8.1...v0.8.2) (2026-09-22)
|
|
133
215
|
|
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
|
+
at specs/005-step-coordination-remediation/plan.md
|
|
5
5
|
<!-- SPECKIT END -->
|
data/README.md
CHANGED
|
@@ -720,6 +720,23 @@ class RefundOrderReactor < RubyReactor::Reactor
|
|
|
720
720
|
end
|
|
721
721
|
end
|
|
722
722
|
|
|
723
|
+
class ChargeStep < RubyReactor::Step
|
|
724
|
+
input :account_id
|
|
725
|
+
|
|
726
|
+
# All five macros also work declared on a STEP, not just the reactor —
|
|
727
|
+
# keying the critical section down to this one operation instead of the
|
|
728
|
+
# whole workflow. Surrounding steps (audit, notify, ...) keep overlapping
|
|
729
|
+
# across concurrent executions; only :charge serializes. Its undo re-takes
|
|
730
|
+
# the key, waiting up to `rollback_wait:` (default: `ttl`; 60 s for a
|
|
731
|
+
# step `with_semaphore`) — an undo that still can't run is reported on
|
|
732
|
+
# `Failure#rollback_failures`, never dropped.
|
|
733
|
+
with_lock(ttl: 60, rollback_wait: nil) { |args| "acct:#{args[:account_id]}" }
|
|
734
|
+
|
|
735
|
+
def run
|
|
736
|
+
Success(charge!(inputs))
|
|
737
|
+
end
|
|
738
|
+
end
|
|
739
|
+
|
|
723
740
|
class GeocodeReactor < RubyReactor::Reactor
|
|
724
741
|
input :address
|
|
725
742
|
|
|
@@ -843,7 +860,7 @@ step :maybe_sync do
|
|
|
843
860
|
end
|
|
844
861
|
```
|
|
845
862
|
|
|
846
|
-
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.
|
|
863
|
+
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, step-scoped coordination, and operational notes.
|
|
847
864
|
|
|
848
865
|
### Map & Parallel Execution
|
|
849
866
|
|
|
@@ -1376,6 +1393,22 @@ end
|
|
|
1376
1393
|
# Result: Complete rollback of the transaction
|
|
1377
1394
|
```
|
|
1378
1395
|
|
|
1396
|
+
A rollback that did not complete is never silent. `Failure#rollback_failures`
|
|
1397
|
+
lists every undo or compensation that raised, returned a `Failure`, or could not
|
|
1398
|
+
re-take its step's lock/semaphore within `rollback_wait:` — including those of
|
|
1399
|
+
composed children (flattened). It is always an Array, empty when rollback
|
|
1400
|
+
completed, and is part of `Failure#to_h`:
|
|
1401
|
+
|
|
1402
|
+
```ruby
|
|
1403
|
+
result = TransactionReactor.run(from_account: 1, to_account: 2, amount: 10)
|
|
1404
|
+
result.rollback_failures
|
|
1405
|
+
# => [{ step: :debit_account, kind: :undo, key: nil, reason: :raised, message: "gateway timeout" }]
|
|
1406
|
+
```
|
|
1407
|
+
|
|
1408
|
+
`reason` is `:coordination_unavailable`, `:returned_failure`, or `:raised`; `kind`
|
|
1409
|
+
is `:undo` or `:compensate`. See
|
|
1410
|
+
[Step Rollback](documentation/locks_and_semaphores.md#step-rollback).
|
|
1411
|
+
|
|
1379
1412
|
### Using Pre-defined Schemas
|
|
1380
1413
|
|
|
1381
1414
|
You can use existing dry-validation schemas:
|
|
@@ -1460,7 +1493,7 @@ Comprehensive guide to testing reactors with RubyReactor's testing utilities. Le
|
|
|
1460
1493
|
|
|
1461
1494
|
### [Locks, Semaphores, Rate Limits, Periods & Ordered Locks](documentation/locks_and_semaphores.md)
|
|
1462
1495
|
|
|
1463
|
-
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-
|
|
1496
|
+
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`). Every primitive is also available [declared on a step](documentation/locks_and_semaphores.md#step-scoped-coordination) instead of the whole reactor, keying the critical section down to one operation. 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.
|
|
1464
1497
|
|
|
1465
1498
|
### [Middlewares & OpenTelemetry](documentation/middlewares.md)
|
|
1466
1499
|
|
|
@@ -34,6 +34,25 @@ module RubyReactor
|
|
|
34
34
|
RubyReactor::DispatchResult.new(job_id: job_id)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Delayed re-enqueue of an `async_step`'s body — the park path
|
|
38
|
+
# (Finding 6). See Sidekiq::Router#perform_step_in.
|
|
39
|
+
# rubocop:disable Metrics/ParameterLists
|
|
40
|
+
def self.perform_step_in(delay, root_context_id:, reactor_class_name:, step_context_id:, step_name:,
|
|
41
|
+
contention_attempts: 0)
|
|
42
|
+
# rubocop:enable Metrics/ParameterLists
|
|
43
|
+
job_id = RubyReactor::Adapters::ActiveJob::StepWorker.perform_in(
|
|
44
|
+
delay,
|
|
45
|
+
{
|
|
46
|
+
"root_context_id" => root_context_id,
|
|
47
|
+
"reactor_class_name" => reactor_class_name,
|
|
48
|
+
"step_context_id" => step_context_id,
|
|
49
|
+
"step_name" => step_name.to_s,
|
|
50
|
+
"contention_attempts" => contention_attempts
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
RubyReactor::DispatchResult.new(job_id: job_id)
|
|
54
|
+
end
|
|
55
|
+
|
|
37
56
|
# rubocop:disable Metrics/ParameterLists
|
|
38
57
|
def self.perform_map_element_async(map_id:, element_id:, index:, serialized_inputs:, reactor_class_info:,
|
|
39
58
|
strict_ordering:, parent_context_id:, parent_reactor_class_name:,
|
|
@@ -34,6 +34,27 @@ module RubyReactor
|
|
|
34
34
|
RubyReactor::DispatchResult.new(job_id: job_id)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Delayed re-enqueue of an `async_step`'s body — the park path
|
|
38
|
+
# (Finding 6): the worker rescues `StepCoordination::Contended` and
|
|
39
|
+
# reschedules itself instead of failing, carrying the contention
|
|
40
|
+
# attempt count forward so `lock_snooze_max_attempts` still bounds it.
|
|
41
|
+
# rubocop:disable Metrics/ParameterLists
|
|
42
|
+
def self.perform_step_in(delay, root_context_id:, reactor_class_name:, step_context_id:, step_name:,
|
|
43
|
+
contention_attempts: 0)
|
|
44
|
+
# rubocop:enable Metrics/ParameterLists
|
|
45
|
+
job_id = RubyReactor::Adapters::Sidekiq::StepWorker.perform_in(
|
|
46
|
+
delay,
|
|
47
|
+
{
|
|
48
|
+
"root_context_id" => root_context_id,
|
|
49
|
+
"reactor_class_name" => reactor_class_name,
|
|
50
|
+
"step_context_id" => step_context_id,
|
|
51
|
+
"step_name" => step_name.to_s,
|
|
52
|
+
"contention_attempts" => contention_attempts
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
RubyReactor::DispatchResult.new(job_id: job_id)
|
|
56
|
+
end
|
|
57
|
+
|
|
37
58
|
# rubocop:disable Metrics/ParameterLists
|
|
38
59
|
def self.perform_map_element_async(map_id:, element_id:, index:, serialized_inputs:, reactor_class_info:,
|
|
39
60
|
strict_ordering:, parent_context_id:, parent_reactor_class_name:,
|