gizmoduck: row 271 (264d) design draft 1 in the packet: the threadIdle fence as the drain, the idle stop as the only signal, a session_ops journal and message_cancels in one schema step; Q1-Q4 for Sage and Mos
This commit is contained in:
1 parent
2998b2a4e8
commit
3105de8892
1 file changed
+94
@@ -70,3 +70,97 @@ default where it is.
|
||||
|
||||
On a base without 264d, `session.clear` refuses `unknown-verb`, and the rows have no `state`,
|
||||
`revision` or `successor`.
|
||||
|
||||
## Design (draft 1, for Duckworth's early look before code)
|
||||
|
||||
Measured on 2d0d407ba, which already holds row 194 (ab7377bcf). Nothing has been run.
|
||||
|
||||
### What exists and what doesn't
|
||||
|
||||
- No per-session lifecycle state. `sessions` is append-only
|
||||
(schema.sql:370-372). The nearest per-session state is the latest `thread_claims` row (op and
|
||||
generation). Row 194's lifecycle is per instance, as `action.allowed` events keyed by a random
|
||||
request id (broker.mjs:622-666).
|
||||
- No drain request in the runner. Its only stop is SIGTERM, which aborts a turn in progress
|
||||
(runner.mjs:407-423).
|
||||
- There is a fence. `threadIdle` (broker.mjs:865-877) rechecks `pending` and `unread`
|
||||
for the run's own session and releases the claim in one synchronous call, only when both are 0.
|
||||
After that, the runner's next `message.receive` gets `not-holder` and it exits 22 by itself
|
||||
(runner.mjs:55, :524-530). `unread` 0 means every message delivered to that holder was read, and
|
||||
the runner reads a message only after its turn's RESULT (runner.mjs:570-577). So a release
|
||||
happens only at a completed turn boundary.
|
||||
- The launcher's idle stop (launcher.mjs:683-701) is threadIdle, then a start-time-checked
|
||||
SIGTERM to an idle runner. Exit proof is the run's `close` handler: `endRun`, registry remove,
|
||||
and the workspace lock release (:600-641).
|
||||
- No cancelled state for an undelivered message. `deliveries.op` is a CHECK list on an append-only
|
||||
table.
|
||||
|
||||
### Clear, stage by stage
|
||||
|
||||
1. **Accept** (`session.clear`, the human's write). One transaction:
|
||||
- check owner and grant (the session.create path), the revision, and processing not halted
|
||||
- append journal stage `requested`, with a successor id bound now (`randomUUID`) and a payload
|
||||
digest for the repeat/conflict check
|
||||
Answer `clearing`. From here:
|
||||
- `thread.send` / `thread.open` naming the session refuse `session-clearing`. A thread-only
|
||||
send whose default is that session refuses the same way.
|
||||
- `threadsState` reports the session as `clearing`, so `threadPass` never starts it.
|
||||
- `message.receive` for a run of that session returns `[]`, which fences the next-turn dequeue.
|
||||
2. **Drain** (the launcher, each tick, for each open clear). A trusted op `sessionClearDrain
|
||||
{business, op}` runs in one transaction:
|
||||
- Cancel every undelivered message to the session, with a receipt row per message (new table
|
||||
`message_cancels`). The cancelled messages stop counting as pending.
|
||||
- If the session has a holder: run threadIdle's check. If `unread > 0`, the turn is still
|
||||
running. Answer `{drained: false}` and change nothing else. Otherwise release the claim,
|
||||
generation-bound (`#claimEnd`), and append `drained` with the released run and generation.
|
||||
- If the session has no holder: append `drained` with run null.
|
||||
After `drained` with a run, the launcher sends the idle stop's start-time-checked SIGTERM, the
|
||||
same reviewed path, to an idle runner whose dequeue is already fenced. Even without the signal,
|
||||
the runner exits on its next receive. No KILL, no stopRun, and no signal before `drained`.
|
||||
3. **Proof.** The released run's `close` handler (endRun, registry remove, lock release) is the
|
||||
proof.
|
||||
- The runner is pid 1 of its own pid namespace (sandbox `--unshare-pid --as-pid-1`), so its exit
|
||||
ends every process in it. **To verify in the build** (bwrap's pid-1 behaviour), not assumed.
|
||||
- With no run, the proof is that the launcher takes `lockSessionWorkspace` for the session (no
|
||||
writer alive) and the registry has no run for it.
|
||||
- After 30 s with no proof, append `uncertain` once. Keep waiting, with capacity still held by
|
||||
the live run and no second signal. A launcher restart finds the run among its row 269
|
||||
survivors and keeps waiting on its identity.
|
||||
4. **Retire** (trusted op `sessionClearRetire {business, op, run}`), one transaction:
|
||||
- check the run ended (`#ended`) or that there was none
|
||||
- append `retired`
|
||||
- insert the successor `sessions` row: `predecessor` the old id, `op` the clear op, `name` the
|
||||
old name plus a suffix (`UNIQUE(thread,name)`)
|
||||
- move the default with a `session_defaults` row, only if the old session was the default
|
||||
- insert a `thread_opens` row for the successor
|
||||
The old workspace stays at its path, untouched. The successor gets its own store and workspace
|
||||
from row 269's per-session layout and a fresh approved checkout. Nothing is copied.
|
||||
5. **Start** (D8). `threadPass` starts the successor from its `thread_opens` row through the same
|
||||
`check()`/`start()` as any session. The launcher reports the outcome with `sessionClearStart
|
||||
{business, op, start, refusal}`, appending `start-held`, `start-started` or `start-refused`.
|
||||
A held start keeps its op and spends no attempt.
|
||||
|
||||
`failed`: a refusal at retire (for example the thread's owner lost the grant) appends `failed`
|
||||
with the code. No retirement happens and the old session goes back to `active`. Human cancel is a
|
||||
later row unless Sage wants it in 271.
|
||||
|
||||
### Schema step (new, label by landing order)
|
||||
|
||||
- `session_ops(seq, at, business, thread, session, op, stage, successor, run, generation,
|
||||
digest, refusal)`. Append-only, with `UNIQUE(thread, op, stage)` and a CHECK on `stage`. A
|
||||
trigger refuses a second open op per session. The envelope is derived from the latest stage.
|
||||
`revision` is the count of this session's rows.
|
||||
- `message_cancels(seq, at, message UNIQUE, session, op)`. Append-only.
|
||||
- `threadRows`'s `pending` gains `AND NOT EXISTS (cancel)`. Each row gains `state`.
|
||||
|
||||
### Questions for Sage (and Mos where marked)
|
||||
|
||||
- Q1 (Mos, mos-1292). A real `3l→3m` step collides with comment-migration.test.mjs:76-82, which
|
||||
fabricates a future `3l→3m`. That pre-existing test has to move to `3m→3n`, and the `'3l'` pins
|
||||
in the account, gitea, grant and name migration tests and sessions.test.mjs:435 move too. Does
|
||||
that go to Mos as one change list? And does 271 take `3m`, or whatever label landing order
|
||||
gives it after row 206?
|
||||
- Q2. Stop: fence plus idle-stop SIGTERM, as above, rather than a new `stopRun` reason. That adds
|
||||
no KILL path. OK?
|
||||
- Q3. Human cancel of a clear: out of 271?
|
||||
- Q4. Successor name: `<old name>-2`, `-3` and so on. Or should clear take a name?
|
||||
Reference in new issue
Block a user