Skip to content

Commit 62cf051

Browse files
committed
docs(adr): enumerate deficiencies, add lifecycle invariants
1 parent ecc8f89 commit 62cf051

1 file changed

Lines changed: 80 additions & 0 deletions

File tree

docs/adr/0006-xs-lifecycle-topics.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,32 @@ namespace as user data.
4848
This drives the historical-scan cost we measured at ~17us/frame ×
4949
110k frames per dispatcher start.
5050

51+
### Concrete deficiencies enumerated
52+
53+
The problems above surface as eight specific bugs in today's
54+
implementation. Each is referenced by the invariant set below.
55+
56+
1. Service `.stopped {reason=finished}` and `.stopped {reason=error}`
57+
restart on boot. Contrary to "if it chose to stop, it stays stopped."
58+
2. Service hot-replace where the new `.spawn` has a parse error: old
59+
keeps running live (correct), but compaction is latest-wins so the
60+
broken `.spawn` survives and the service vanishes on next boot.
61+
3. Action hot-replace + parse error: same shape as #2 -- broken `.define`
62+
overwrites; previously-good define is lost on restart.
63+
4. Action with a broken `.define` retries the broken version on every
64+
boot. Dispatcher doesn't scan `.error` historically; no "skip broken"
65+
path.
66+
5. Actor hot-replace + parse error kills *both*: the old instance
67+
self-terminates on any duplicate `.register` before the new is
68+
validated; the new then parse-fails. Net: nothing running.
69+
6. Action has no undefine. Once defined, the only way to remove is to
70+
edit history.
71+
7. Action `.error` overloads parse failure (`register_action` Err) and
72+
runtime failure (per-call `execute_action` Err) -- can't tell apart
73+
at the topic level.
74+
8. Actor `.unregistered` overloads parse failure with graceful
75+
teardown -- only `meta.error`'s presence distinguishes them.
76+
5177
## Decision
5278

5379
### Namespace
@@ -163,6 +189,60 @@ if xs died before the `fin.term` ack landed. Acceptable -- a `term` in
163189
the log is a clear user intent, and respecting it without waiting for
164190
the ack matches what the user wanted.
165191

192+
### Invariants
193+
194+
The compaction algorithm and topic vocabulary above exist to honor these
195+
contracts. Each one is testable; together they cover every deficiency in
196+
the previous section.
197+
198+
- **I1. Stop persistence.** Once `term` or any `fin.*` has been observed
199+
for a `<kind>.<name>`, no subsequent restart starts the prior `create`.
200+
- **I2. Run persistence.** A `<kind>.<name>` with an `active` and no
201+
subsequent `fin.*`/`term` resumes on every restart until something
202+
terminal lands.
203+
- **I3. Hot-replace fallback.** When a newer `create₂` follows a
204+
known-good `create₁`, and `create₂` is broken (`parse.error`) or
205+
untested (no ack), restarts fall back to `create₁`. Live behaviour
206+
and post-restart behaviour agree.
207+
- **I4. Bidirectional lifecycle.** Every kind supports a user-driven
208+
`term` that ends the thing and prevents restart.
209+
- **I5. Distinct exit categories.** The topic alone (no meta needed)
210+
distinguishes: failed-to-init vs user-terminated vs runtime-crashed
211+
vs naturally-finished vs replaced vs server-shut-down.
212+
- **I6. Ack traceability.** Every runtime-emitted ack (`active`,
213+
`parse.error`, `fin.*`, `replaced`) carries a meta pointer to its
214+
originating `create` (or `term`).
215+
- **I7. Server-shutdown invisibility.** A `stopped` event does not
216+
affect compaction; the thing resumes on next start.
217+
- **I8. Single live instance.** At most one running instance per
218+
`<kind>.<name>` at any time.
219+
220+
### Coverage check
221+
222+
Each enumerated deficiency would be caught by a test of the named
223+
invariant:
224+
225+
| # | Deficiency | Caught by |
226+
|---|---|---|
227+
| 1 | Service `.stopped {finished/error}` restarts on boot | I1 |
228+
| 2 | Service hot-replace + parse error: old version lost on restart | I3 |
229+
| 3 | Action hot-replace + parse error: same | I3 |
230+
| 4 | Action broken `.define` retries every boot | I3 |
231+
| 5 | Actor hot-replace + parse error kills both | I3 |
232+
| 6 | Action has no undefine | I4 |
233+
| 7 | Action `.error` overloads parse and runtime failure | I5 |
234+
| 8 | Actor `.unregistered` overloads parse-failure with graceful teardown | I5 |
235+
236+
I6 and I8 don't catch one of the enumerated deficiencies directly, but
237+
they're load-bearing for the invariants that do: without ack traceability
238+
(I6) you can't pair `parse.error` to its `create`, so I3 is unenforceable;
239+
without single-instance (I8) you can't unambiguously define "the thing"
240+
that I1/I2 track. They stay in the set as supporting invariants.
241+
242+
I7 is implied by I2 + I1 (`stopped` isn't `fin.*` so it doesn't satisfy
243+
I1's "stop observed"), but stating it explicitly closes a likely
244+
misreading of `stopped` as a terminal event.
245+
166246
### Action subset
167247

168248
Actions don't run long-lived tasks. The events they use:

0 commit comments

Comments
 (0)