From c65e664c2913f6bcffa2c1192fee290372599a2e Mon Sep 17 00:00:00 2001 From: sami Date: Thu, 10 Sep 2026 00:02:02 +0400 Subject: [PATCH] core: sign off on ADR 0013 (task state-machine ownership) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Accept as written — no amendments. Answers to the four open items: 1. tasks_starved / starved_tasks() count ONLY tasks in {connecting, downloading} with segments_active == 0 (the allocator owes a slot to a task that is asking). retry_wait and paused (either-initiated) are outside that state set, so they are excluded by construction, not by a special case. Design commitment; the accessor is stage 6/8. 2. pause() is idempotent: no-op success on an already-paused or terminal task; resume() no-op success on a non-paused task; only task_not_found errors. No state-change event for a no-op. 3. PROTO's item — CORE confirms its half: auto-pause reports the transition with ErrorInfo populated (auth_required / server_file_changed / disk_full / path_rejected), already in util/error.hpp. Ready once PROTO permits error on state=="paused". 4. CORE adopts "auto-pause"; the wire/API discriminator stays state==paused + presence of the Error code. Notes back: pause during verifying re-hashes from scratch on resume; pause during assembling is M4; restart handling in §5 agreed — start(TaskId) re-derives resume position from .veloxpart.meta + If-Range. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS --- core/docs/adr-0013-core-response.md | 97 +++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 core/docs/adr-0013-core-response.md diff --git a/core/docs/adr-0013-core-response.md b/core/docs/adr-0013-core-response.md new file mode 100644 index 0000000..791ac75 --- /dev/null +++ b/core/docs/adr-0013-core-response.md @@ -0,0 +1,97 @@ +# CORE response to ADR 0013 (task state-machine ownership) + +**Verdict: accept as written.** No amendments to the decision. `daemon/src/sched/`'s +pause/resume logic is unblocked from CORE's side once PROTO lands open item 3 (the +`error`-on-`paused` widening — see below). + +The split in §1, the shared-`paused` + idempotency contract in §2, the cross-reason resume +rule in §3, the `retry_wait ≠ starvation` resolution in §4, and the restart handling in §5 +all match what CORE agreed in `contracts/proto-answers-m1.md` D1 and the ADR 0011 response. + +Everything below is a **design commitment** — CORE's state machine is stage 8 and +`starved_tasks()` is stage 6/8; none of it is built yet. So "does CORE already do X" is +answered as "CORE will do X, and here is the spec it will be built to", not as a report on +existing code. + +--- + +## Answers to the four open items + +### 1 — `starved_tasks()` / `tasks_starved` exclude `retry_wait` and auto-paused, by construction + +Not a behavior change (nothing to change yet); a commitment baked into the accessor's +definition. Pinning ADR 0011's "running task with `segments_active == 0`": + +`tasks_starved` counts **only tasks whose task-state is `connecting` or `downloading` and +whose `segments_active == 0`.** That is precisely "the segment allocator has not granted a +slot to a task that is asking for one." Consequences: + +| Task state | In `tasks_starved`? | Why | +|---|---|---| +| `probing` | no | uses the probe pool (ADR 0011 §5), not the segment budget | +| `connecting`, `segments_active == 0` | **yes** | admitted + probed, waiting on the allocator's first grant — the real starvation case | +| `connecting`/`downloading`, `segments_active ≥ 1` | no | a segment in its own `connecting` sub-state counts as a held slot (ADR 0011 A3) | +| `downloading`, `segments_active == 0` | **yes** | held slots, lost them all (e.g. every segment failed and is being re-requested) — transient, still a real "allocator owes this task a slot" | +| `retry_wait` | no | CORE's backoff timer holds it at zero *deliberately*; it is not asking the allocator for anything until it re-enters `connecting` | +| `paused` (DAEMON- **or** CORE-initiated) | no | a paused task is not asking for a slot | +| `new`, `queued`, `assembling`, `verifying`, `complete`, `failed`, `cancelled` | no | not in the `{connecting, downloading}` set | + +So `retry_wait` and auto-paused tasks fall outside `tasks_starved` **because they are not +in the counted state set**, not because of a special case that could rot. `starved_tasks()` +returns exactly the TaskIds in that count; `starved_since(id)` is defined only for them. + +A `retry_wait` task still counts against `connection.maxConcurrentDownloads` from DAEMON's +side (it is running, not requeued) and contributes nothing to the allocator's guarantee +pass until it re-enters `connecting` — matches §4 exactly. + +### 2 — `pause()` is idempotent (committed) + +- `pause(TaskId)` on a task already in `paused` (however it got there) → **no-op, returns + success**. DAEMON never needs to know CORE auto-paused first. +- `pause(TaskId)` on a task in a terminal state (`complete`/`failed`/`cancelled`) → + **no-op, returns success** — a pause racing a completion is not an error. +- `resume(TaskId)` on a task that is not paused → **no-op, returns success**. +- The only error `pause()`/`resume()` return is `task_not_found`. +- CORE does not report a state-change event for a no-op pause (nothing changed). DAEMON + reads the resulting state via the normal state-change callback / `TaskDetail`, not from + `pause()`'s return. +- If CORE is mid-transition to `paused` (its own auto-pause) when DAEMON's `pause()` + arrives, the task ends up `paused` once, with one state-change event. + +### 3 — PROTO: `error`-on-`paused` widening + +Not CORE's to land, but CORE confirms its half: when CORE auto-pauses, it reports the +transition through the same state-change callback every CORE transition uses, with the +`ErrorInfo` populated — `auth_required` (401/407), `server_file_changed`, `disk_full`, +`path_rejected` for a mid-run destination failure. Those are the `vdm::Error` values from +the B1 taxonomy (`core/docs/proto-requests-m1.md`), already implemented in +`core/include/vdm/util/error.hpp`. **CORE is ready**; PROTO only needs to permit `error` to +be present on `event.task.state` when `state == "paused"`, and DAEMON to project CORE's +`ErrorInfo` onto it. `error: null` on a DAEMON-initiated pause is correct and sufficient. + +Until PROTO lands it, DAEMON cannot implement §3's cross-reason resume rule +correctness-preservingly — agree it should not guess from timing. + +### 4 — "auto-pause" naming + +CORE has no established internal term (the state machine is unbuilt). **CORE adopts +"auto-pause"** for the informal concept. On the wire and in the API there is no new term: +the discriminator is `state == paused` plus the `Error` code (present ⇒ CORE-initiated, +absent ⇒ DAEMON-initiated), exactly as the ADR's §2 and the rejected-alternatives section +describe. + +--- + +## Two notes back to DAEMON (not objections) + +- **§1 table, `paused` from `assembling`/`verifying`:** the *state transition* is honoured + from any CORE state as the table says. The *work* interruption is best-effort: a pause + during `verifying` discards the in-progress hash and re-hashes from the start of the + file on resume (cheap, bounded); a pause during `assembling` (HLS/DASH mux) is an M4 + concern and may not be cleanly interruptible mid-mux. Neither affects M1 or the API + shape. +- **§5 restart:** CORE agrees fully. CORE holds no persistent state; `start(TaskId)` + transparently checks for a valid `.veloxpart.meta` sidecar (stage 5), re-validates with + `If-Range` (`docs/04` §5), and either resumes from the recorded offsets or restarts if + the validator failed / the sidecar is corrupt. DAEMON does nothing special on restart + beyond rewriting CORE-owned states to `queued` and re-admitting — which is what §5 says.