fix(slots): bound the restart teardown, converge config on load, kill drift false positives (#1224, #1226) - #1361
Conversation
|
This PR is being reconciled against it. It will either be closed as superseded or reduced to the genuinely unique delta (e.g. the |
3c3f881 to
07eb0db
Compare
Reduced after reconciliation with
|
07eb0db to
fc55b71
Compare
… the seam route (#1226, #1224) Reduced onto main after 610150d (#1357) landed its own #1224 fix. Dropped as duplicate: the whole `SlotManager` rewrite — main's `_should_converge` covers load-converges-a-drifted-unit, and its `terminate` bound is the better of the two (it `shield()`s the executor future instead of cancelling it, and raises a typed `SlotTerminateTimeout` (504) rather than a bare TimeoutError). Two things survive because 610150d does not implement them. existing tests". It is not: both tests added here FAIL on main. * `_resolve_drift_flags` substitutes the on-disk path for a bare registry id, but compared the ids with `==`. The slot TOML keeps the catalog spelling while the registry key (and the running container's `--alias`) is the slug, so the substitution never fired and status printed a permanent, entirely bogus `--model` drift. Compare normalised ids instead; a resolved path never normalises onto a bare id, so this cannot mask real path drift. * `compute_config_drift` looked up the RAW TOML id while `load()` launches from `_resolve_servable_model(...)`. For the slots this matters for — a catalog id that landed locally under a different id — the registry lookup missed, the renderer fell back to the bare id, and the comparison warned forever. Resolve the same way the launch path does; a raising heuristic falls back to the raw id, so status can never fail because of this. This also makes main's new `_should_converge` more accurate, since it reads the same comparator. the executor thread, so on the seamed route (`SystemCtlSeam` -> `sudo -n hal0-systemctl`, i.e. a real hal0-service-user install, which is the deployment the wedge was observed on) the thread stays on the child forever. The leak is the lesser problem: `unload_sync` never reaches the Quadlet-source removal + `daemon-reload`, so the generated `.service` stays on disk in `failed` and main's "the subsequent load still converges" does not hold. * `SystemCtlSeam.systemctl` forwards `timeout` on BOTH routes (default None = unbounded, unchanged behaviour). * `ContainerProvider.unload_sync` bounds the stop at 20s — deliberately under `SlotManager._terminate_timeout_s` (30s) so the worker unwinds first — logs it, and CONTINUES the teardown instead of aborting it. Non-vacuity checked for all three: the #1226 tests fail against main's drift.py, and the seam/container tests fail against main's seam.py and container.py. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fc55b71 to
3ba1dd6
Compare
Closes #1224. Closes #1226.
#1224a — restart wedges on errored slots
SlotManager.terminate()documentedtimeout_sas "preserved in the signature for caller compatibility" and awaitedrun_in_executor(provider.unload_sync)with no bound at all. Underneath,ContainerProvider.unload_syncransystemctl stop <unit>with no child timeout.Against a unit systemd had already parked in
failed, that stop never returns — sorestart()blocked forever, the REST caller ReadTimeout"d, andload()was never reached (hence zero journald activity for the unit). The pre-existingcontextlib.suppress(Exception)only catches exceptions; it does nothing against a hang.#1224b —
slot loadno-ops, config never convergesload()short-circuits on READY/SERVING/IDLE and returnsstatus()without touching the unit. The unit bakes its launch config at render time, soPUT /config {"port": 8091}→slot loadleft the container on 8089.#1226 — false drift, two compounding bugs
compute_config_driftresolved the model from the raw TOML id, whileload()first runs it through_resolve_servable_model— so for a catalog id that landed locally under a different id, the registry lookup missed and the bare id was compared against the running containers real--modelpath==, which never fires when the TOML holds the catalog spelling (Qwopus3.5-4B-Coder-MTP-Q6_K) and the registry the slug (qwopus3-5-4b-coder-mtp-q6-k)Fixes
terminate()enforcestimeout_sviaasyncio.wait_for(caller-side bound);unload_syncbounds its systemd verbs (_UNIT_STOP_TIMEOUT_S = 20.0, worker-side) and on an abandoned stop still removes the Quadlet source +daemon-reloads, dropping the failed generated.serviceSystemCtlSeam.systemctlgained atimeoutpassthrough so the bound also applies on the sudo-seam route — the deployment where the bug was seen. Default staysNoneeverywhere elserestart()takes the bounded force path for ERROR or any slot whose unit is not active; teardown is best-effort-with-logging so restart always convergesload()s short-circuit is gated on_unit_needs_convergence(), which re-renders throughrerender_unit_sync— the same rendererload_syncuses, whose byte-identical guarantee is already pinned bytests/providers/test_container.py. Compares the whole effective launch config rather than a hand-maintained key list. Conservative: NPU trio shadows, adopted containers with no unit on disk, and render failures all keep the fast no-op_resolve_model_infoand matches ids on_normalise_idDid not touch name-keying, the state graph, or decompose
manager.py/slots.py.Verification
tests/slots tests/api tests/providers tests/system -m "not podman and not systemd and not network"→ 2470 passed, 2 deselectedtest_terminate_enforces_its_timeout→assert (36648.76 - 36633.74) < 5.0— 15s elapsed against a 0.25s boundOne for a reviewers eye:
restart()now calls_is_active()before choosing its path and coerces probe errors to False, routing to the bounded force path. Safe and convergent, but a probe outage makes every restart take the force path rather than the graceful drain.🤖 Generated with Claude Code