ruby_reactor 0.8.5 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/.specify/feature.json +1 -1
- data/.specify/memory/constitution.md +17 -14
- data/CHANGELOG.md +58 -0
- data/CLAUDE.md +1 -1
- data/README.md +23 -8
- data/lib/ruby_reactor/adapters/active_job/map_element_rollback_worker.rb +20 -0
- data/lib/ruby_reactor/adapters/active_job/router.rb +32 -4
- data/lib/ruby_reactor/adapters/sidekiq/map_element_rollback_worker.rb +16 -0
- data/lib/ruby_reactor/adapters/sidekiq/router.rb +32 -4
- data/lib/ruby_reactor/context.rb +14 -1
- data/lib/ruby_reactor/dsl/definition_warnings.rb +30 -0
- data/lib/ruby_reactor/dsl/map_builder.rb +27 -7
- data/lib/ruby_reactor/dsl/step_builder.rb +8 -21
- data/lib/ruby_reactor/error/rescuable.rb +6 -1
- data/lib/ruby_reactor/error/rollback_handed_off.rb +27 -0
- data/lib/ruby_reactor/executor/compensation_manager.rb +33 -9
- data/lib/ruby_reactor/executor/result_handler.rb +72 -52
- data/lib/ruby_reactor/executor/retry_manager.rb +4 -3
- data/lib/ruby_reactor/executor/step_executor.rb +3 -1
- data/lib/ruby_reactor/executor.rb +186 -6
- data/lib/ruby_reactor/map/collector.rb +25 -93
- data/lib/ruby_reactor/map/dispatcher.rb +38 -7
- data/lib/ruby_reactor/map/element_executor.rb +14 -10
- data/lib/ruby_reactor/map/element_rollback.rb +158 -0
- data/lib/ruby_reactor/map/helpers.rb +11 -155
- data/lib/ruby_reactor/map/step_rollback.rb +136 -0
- data/lib/ruby_reactor/map/sweeper.rb +66 -8
- data/lib/ruby_reactor/map.rb +17 -0
- data/lib/ruby_reactor/max_retries_exhausted_failure.rb +4 -2
- data/lib/ruby_reactor/reactor.rb +41 -2
- data/lib/ruby_reactor/rspec/active_job_helpers.rb +13 -2
- data/lib/ruby_reactor/rspec/matchers.rb +40 -21
- data/lib/ruby_reactor/rspec/sidekiq_helpers.rb +7 -3
- data/lib/ruby_reactor/rspec/test_subject.rb +13 -3
- data/lib/ruby_reactor/rspec.rb +3 -6
- data/lib/ruby_reactor/step/compose_step.rb +3 -0
- data/lib/ruby_reactor/step/map_step.rb +117 -114
- data/lib/ruby_reactor/storage/adapter.rb +14 -0
- data/lib/ruby_reactor/storage/redis_adapter.rb +18 -2
- data/lib/ruby_reactor/storage/redis_map_rollback.rb +153 -0
- data/lib/ruby_reactor/storage/redis_reactor_scan.rb +1 -1
- data/lib/ruby_reactor/sweeper.rb +3 -1
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/api.rb +11 -1
- data/lib/ruby_reactor/web/public/assets/{index-CQbgHtd0.js → index-C6ejszCQ.js} +12 -12
- data/lib/ruby_reactor/web/public/assets/index-DM5darhy.css +1 -0
- data/lib/ruby_reactor/web/public/index.html +2 -2
- data/lib/ruby_reactor/worker.rb +4 -2
- data/specs/007-execution-flow-analysis/evidence/harness.rb +2 -0
- data/specs/007-execution-flow-analysis/evidence/probes/03_map.rb +4 -1
- data/specs/009-distributed-map-undo/checklists/requirements.md +53 -0
- data/specs/009-distributed-map-undo/contracts/api-surface.md +125 -0
- data/specs/009-distributed-map-undo/contracts/rollback-protocol.md +157 -0
- data/specs/009-distributed-map-undo/data-model.md +151 -0
- data/specs/009-distributed-map-undo/plan.md +323 -0
- data/specs/009-distributed-map-undo/pr-description.md +78 -0
- data/specs/009-distributed-map-undo/quickstart.md +154 -0
- data/specs/009-distributed-map-undo/research.md +585 -0
- data/specs/009-distributed-map-undo/spec.md +360 -0
- data/specs/009-distributed-map-undo/tasks.md +1060 -0
- data/specs/future_improvements.md +18 -30
- metadata +21 -3
- data/lib/ruby_reactor/web/public/assets/index-BQvIWPdx.css +0 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Quickstart: Validate Distributed Map Rollback and Bounded Fan-out
|
|
2
|
+
|
|
3
|
+
A guide to proving each user story end to end. The behavior is specified in
|
|
4
|
+
[contracts/](contracts/), and the records in [data-model.md](data-model.md).
|
|
5
|
+
|
|
6
|
+
## 0. Prerequisites
|
|
7
|
+
|
|
8
|
+
- Ruby >= 3.0 and `bundle install`.
|
|
9
|
+
- Test Redis reachable at `redis://localhost:6780`. The suite refuses to start without it.
|
|
10
|
+
- Docker, for the demo acceptance runs and the SC-002 benchmark only.
|
|
11
|
+
- If other suites share the test Redis, run the new specs alone first (known flake under load).
|
|
12
|
+
|
|
13
|
+
## 1. User Story 1: distributed rollback (P1)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle exec rspec spec/ruby_reactor/rollback/distributed_map_rollback_spec.rb
|
|
17
|
+
bundle exec rspec spec/ruby_reactor/rollback/map_rollback_recovery_spec.rb
|
|
18
|
+
bundle exec rspec spec/ruby_reactor/rollback/map_rollback_spec.rb # 008 coverage, now via both paths
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Expected:
|
|
22
|
+
|
|
23
|
+
| Scenario | Observable outcome |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| Fan-out map of 20 elements, `batch_size 5`, a later step fails. This is the spec's 1,000/50 scenario scaled down; §1 Scale covers 10,000/50. | Mid-drain (once rollback jobs are pending): `be_rolling_back`. While draining: no job enqueues more than 5 rollback jobs (`max_burst`); queue depth is not bounded (R-02). After draining: `be_failure`; every element undone once; the steps before the map undone after the last element. |
|
|
26
|
+
| Same reactor with an inline map (oracle, I-7) | Same final `Failure` message, step and `rollback_failures` set as the fan-out run. |
|
|
27
|
+
| Element 7 fails under atomic | Every started element gets a rollback job. Element 7's job reports `not_needed` and undoes nothing. Skipped indexes get no job. |
|
|
28
|
+
| An element's lock is held while its rollback job runs | The job requeues itself, then completes once the lock is free, with no `element_in_flight` failure. After `lock_snooze_max_attempts` requeues it reports `element_in_flight`. |
|
|
29
|
+
| An element context expired (row deleted) | Exactly one `context_unavailable` entry for that index, as with the inline map. |
|
|
30
|
+
| An element reactor containing an inline (nested) map | The nested map is rolled back inline inside the outer element's rollback job; the log shows nested undos before the outer element's earlier steps. |
|
|
31
|
+
| One element's undo raises | The run still undoes the steps before the map. `rollback_failures` holds that element's entry with `map_step` and `element_index`. |
|
|
32
|
+
| Interruption raised in an element's second undo, then the job is redelivered | The first undo ran once. The second may run twice. The run finishes its rollback. |
|
|
33
|
+
| Rollback job dropped, then `Map::Sweeper.run_once` | The position is re-dispatched and the run finishes. |
|
|
34
|
+
| Owner resume dropped, then `RubyReactor::Sweeper.run_once` | The owner is re-enqueued and the run finishes. |
|
|
35
|
+
| `Reactor.undo(id)` on a completed run with a fan-out map | Returns with the run `rolling_back`; after draining, `cancelled`. A second `Reactor.undo` while rolling back raises `ValidationError`, and so does `Reactor.cancel`. |
|
|
36
|
+
| Inline map rollback | Elements are read 100 at a time, highest index first. Same outcome as before. |
|
|
37
|
+
|
|
38
|
+
Scale (slow):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bundle exec rspec spec/ruby_reactor/rollback/map_scale_spec.rb --tag slow
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Expected: 10,000 elements roll back, and each rollback job loads exactly one element context.
|
|
45
|
+
|
|
46
|
+
## 2. User Story 2: fan-out map inside a composed child (P2)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
bundle exec rspec spec/map/map_compose_fan_out_spec.rb
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Expected (drain all jobs):
|
|
53
|
+
|
|
54
|
+
- Root composes Child; Child has `map … fan_out batch_size: 1`. The root is `be_success` with the
|
|
55
|
+
expected result. Before this change it stayed `running`.
|
|
56
|
+
- The same with an interrupt after the map in Child: `be_paused_at(...)`, then `resume`, then
|
|
57
|
+
`be_success`.
|
|
58
|
+
- A root step after the compose fails: elements rolled back (distributed), then Child's earlier
|
|
59
|
+
steps, then Root's. The root is `be_failure`.
|
|
60
|
+
- A Child element fails under atomic: the root (not only Child's record) is `be_failure`.
|
|
61
|
+
- Compose nested two levels deep: the top-level root is `be_success`.
|
|
62
|
+
- `Reactor.undo(root id)` on a completed root whose composed child ran a fan-out map: the root is
|
|
63
|
+
`rolling_back`, then `cancelled`. Every element and every earlier step is undone exactly once.
|
|
64
|
+
- A root step after the compose fails, and the child step after the map has an undo that fails.
|
|
65
|
+
That failure is recorded before the child hands off, and it still appears in the root's final
|
|
66
|
+
`rollback_failures` after the resume.
|
|
67
|
+
- The same child step with a failing undo, reached by a manual undo instead: the failure is in the
|
|
68
|
+
trace, as today for manual undo.
|
|
69
|
+
|
|
70
|
+
## 3. User Story 3: default batch size (P2)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
bundle exec rspec spec/map/map_batch_size_spec.rb
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Expected:
|
|
77
|
+
|
|
78
|
+
- `fan_out` with no batch size, 500 elements: no job enqueues more than 50 element jobs
|
|
79
|
+
(`max_burst`), and all 500 outcomes are collected.
|
|
80
|
+
- 20 elements: all 20 enqueued at once.
|
|
81
|
+
- `batch_size: 200`: 200 per throw.
|
|
82
|
+
- Map metadata stores `batch_size` and `atomic`. `Map::Sweeper` re-dispatch keeps both.
|
|
83
|
+
|
|
84
|
+
## 4. User Story 4: `atomic` (P3)
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
bundle exec rspec spec/map/map_atomic_spec.rb spec/map/map_atomic_async_spec.rb spec/map/atomic_inline_spec.rb
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Expected:
|
|
91
|
+
|
|
92
|
+
- No declaration: one element failure fails the map and rolls back the completed elements.
|
|
93
|
+
- `atomic false`: the map completes with one `Failure` among the results.
|
|
94
|
+
- `fail_fast false`: the same behavior, plus exactly one stderr line starting
|
|
95
|
+
`[RubyReactor] DEPRECATION:` that names `atomic`.
|
|
96
|
+
- Both declared: `ValidationError` at class definition.
|
|
97
|
+
- An element job payload carrying `"fail_fast" => true` (legacy) is processed as atomic.
|
|
98
|
+
|
|
99
|
+
## 5. Demo app acceptance (Constitution VI)
|
|
100
|
+
|
|
101
|
+
Locally, without Docker (test Redis DB 5; run `bin/rails db:prepare` once first):
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
cd demo_app
|
|
105
|
+
REDIS_URL=redis://localhost:6780/5 RAILS_ENV=test bundle exec rspec \
|
|
106
|
+
spec/reactors/distributed_refund_demo_reactor_spec.rb \
|
|
107
|
+
spec/reactors/composed_fan_out_demo_reactor_spec.rb \
|
|
108
|
+
spec/reactors/default_batch_fan_out_demo_reactor_spec.rb spec/reactors/ar_map_reactor_not_fail_spec.rb
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Real Sidekiq, in an isolated compose project. `docker-compose.yml` hard-codes container names, so a
|
|
112
|
+
second worktree needs its own project plus an override that gives unique `container_name`s and
|
|
113
|
+
`ports: !reset []`:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
docker compose -p rr_009 -f docker-compose.yml -f override.yml up -d --build demo-redis demo-sidekiq
|
|
117
|
+
docker compose -p rr_009 -f docker-compose.yml -f override.yml run --rm --no-deps demo-app \
|
|
118
|
+
bash -c "bin/rails db:prepare && bin/rails demo:map_execution_undo"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Expected output, in order:
|
|
122
|
+
|
|
123
|
+
- `DistributedRefundDemoReactor`: dispatch, then a `rolling_back` line with progress, then
|
|
124
|
+
`failed`, with refunds equal to charges.
|
|
125
|
+
- `ComposedFanOutDemoReactor`: `completed` on the happy path, and `failed` with everything
|
|
126
|
+
unshipped on the failure path.
|
|
127
|
+
- `DefaultBatchFanOutDemoReactor`: map metadata `batch_size 50`, and 120 elements collected.
|
|
128
|
+
|
|
129
|
+
### SC-002 benchmark (manual)
|
|
130
|
+
|
|
131
|
+
Run `demo-sidekiq` with `command: bundle exec sidekiq -c 10` in the override, then:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
docker compose -p rr_009 -f docker-compose.yml -f override.yml run --rm --no-deps demo-app \
|
|
135
|
+
bin/rails "demo:map_rollback_benchmark[10000]"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The task prints two times and their ratio, each timed from the moment `:notify` fails (the
|
|
139
|
+
rollback's start) to the end of the run, so the forward run is not counted:
|
|
140
|
+
|
|
141
|
+
- the serial baseline: the inline-map rollback of the same 10,000 elements, through
|
|
142
|
+
`InlineRefundBenchmarkReactor`, which is today's serial algorithm;
|
|
143
|
+
- the distributed rollback time.
|
|
144
|
+
|
|
145
|
+
Pass: the distributed time is 5× faster or better (SC-002).
|
|
146
|
+
|
|
147
|
+
## 6. Full checks before PR
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
bundle exec rspec
|
|
151
|
+
bundle exec rubocop
|
|
152
|
+
cd gui && npm test && npm run build # rolling_back in badge/groups/filters + map rollback progress
|
|
153
|
+
grep -rn "fail_fast" lib gui/src documentation README.md demo_app/app demo_app/documentation # only the deprecated alias, its warning, and legacy normalization remain
|
|
154
|
+
```
|