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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/.specify/feature.json +1 -1
  4. data/.specify/memory/constitution.md +17 -14
  5. data/CHANGELOG.md +58 -0
  6. data/CLAUDE.md +1 -1
  7. data/README.md +23 -8
  8. data/lib/ruby_reactor/adapters/active_job/map_element_rollback_worker.rb +20 -0
  9. data/lib/ruby_reactor/adapters/active_job/router.rb +32 -4
  10. data/lib/ruby_reactor/adapters/sidekiq/map_element_rollback_worker.rb +16 -0
  11. data/lib/ruby_reactor/adapters/sidekiq/router.rb +32 -4
  12. data/lib/ruby_reactor/context.rb +14 -1
  13. data/lib/ruby_reactor/dsl/definition_warnings.rb +30 -0
  14. data/lib/ruby_reactor/dsl/map_builder.rb +27 -7
  15. data/lib/ruby_reactor/dsl/step_builder.rb +8 -21
  16. data/lib/ruby_reactor/error/rescuable.rb +6 -1
  17. data/lib/ruby_reactor/error/rollback_handed_off.rb +27 -0
  18. data/lib/ruby_reactor/executor/compensation_manager.rb +33 -9
  19. data/lib/ruby_reactor/executor/result_handler.rb +72 -52
  20. data/lib/ruby_reactor/executor/retry_manager.rb +4 -3
  21. data/lib/ruby_reactor/executor/step_executor.rb +3 -1
  22. data/lib/ruby_reactor/executor.rb +186 -6
  23. data/lib/ruby_reactor/map/collector.rb +25 -93
  24. data/lib/ruby_reactor/map/dispatcher.rb +38 -7
  25. data/lib/ruby_reactor/map/element_executor.rb +14 -10
  26. data/lib/ruby_reactor/map/element_rollback.rb +158 -0
  27. data/lib/ruby_reactor/map/helpers.rb +11 -155
  28. data/lib/ruby_reactor/map/step_rollback.rb +136 -0
  29. data/lib/ruby_reactor/map/sweeper.rb +66 -8
  30. data/lib/ruby_reactor/map.rb +17 -0
  31. data/lib/ruby_reactor/max_retries_exhausted_failure.rb +4 -2
  32. data/lib/ruby_reactor/reactor.rb +41 -2
  33. data/lib/ruby_reactor/rspec/active_job_helpers.rb +13 -2
  34. data/lib/ruby_reactor/rspec/matchers.rb +40 -21
  35. data/lib/ruby_reactor/rspec/sidekiq_helpers.rb +7 -3
  36. data/lib/ruby_reactor/rspec/test_subject.rb +13 -3
  37. data/lib/ruby_reactor/rspec.rb +3 -6
  38. data/lib/ruby_reactor/step/compose_step.rb +3 -0
  39. data/lib/ruby_reactor/step/map_step.rb +117 -114
  40. data/lib/ruby_reactor/storage/adapter.rb +14 -0
  41. data/lib/ruby_reactor/storage/redis_adapter.rb +18 -2
  42. data/lib/ruby_reactor/storage/redis_map_rollback.rb +153 -0
  43. data/lib/ruby_reactor/storage/redis_reactor_scan.rb +1 -1
  44. data/lib/ruby_reactor/sweeper.rb +3 -1
  45. data/lib/ruby_reactor/version.rb +1 -1
  46. data/lib/ruby_reactor/web/api.rb +11 -1
  47. data/lib/ruby_reactor/web/public/assets/{index-CQbgHtd0.js → index-C6ejszCQ.js} +12 -12
  48. data/lib/ruby_reactor/web/public/assets/index-DM5darhy.css +1 -0
  49. data/lib/ruby_reactor/web/public/index.html +2 -2
  50. data/lib/ruby_reactor/worker.rb +4 -2
  51. data/specs/007-execution-flow-analysis/evidence/harness.rb +2 -0
  52. data/specs/007-execution-flow-analysis/evidence/probes/03_map.rb +4 -1
  53. data/specs/009-distributed-map-undo/checklists/requirements.md +53 -0
  54. data/specs/009-distributed-map-undo/contracts/api-surface.md +125 -0
  55. data/specs/009-distributed-map-undo/contracts/rollback-protocol.md +157 -0
  56. data/specs/009-distributed-map-undo/data-model.md +151 -0
  57. data/specs/009-distributed-map-undo/plan.md +323 -0
  58. data/specs/009-distributed-map-undo/pr-description.md +78 -0
  59. data/specs/009-distributed-map-undo/quickstart.md +154 -0
  60. data/specs/009-distributed-map-undo/research.md +585 -0
  61. data/specs/009-distributed-map-undo/spec.md +360 -0
  62. data/specs/009-distributed-map-undo/tasks.md +1060 -0
  63. data/specs/future_improvements.md +18 -30
  64. metadata +21 -3
  65. 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
+ ```