perf(roots): one helper probes them, and a declared reason outranks the cancel #3
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/roots-probe-and-cancel-reason"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The two follow-ups from your approval on #2, as a branch rather than as issues.
It sits on top of #2, so until that merges the diff here shows its four commits
too; afterwards it is these three.
1.
/rootsand/overviewasked serially — and disagreed about a rootYou named
/roots./overviewwas the third copy of the same loop, so the fixis a helper rather than a second
Promise.all:probeRoots()infsops.tsanswers every root at once, and the three surfaces map its result into their own
shape.
The drift was worse than the latency.
/s/rootsresolved each root, the webroutes mapped
root.pathinto the container directly — so a root a deny rulecovers rendered
unreadableon the schema page and perfectly fine on the webone. Both go through
resolve()now. One visible consequence: a missing root'serroris the resolver'snot foundrather than the OS message./overviewshows four roots, so it passes its four in and the other twelvecost nothing.
Against a running server (host root
/, three real roots, one missing):and with
DENY=<root>/photos/**, which is the case the two surfaces used toanswer differently:
2.
terminalReasonpreferred the cancel over the declared reasonReordered rather than trimmed, since you were right about which way core does
it:
service.tsclassifies aTaskFailurebefore it consultscancelRequested, so a delete that fails onDeno.removemid-cancel recordsfailed/delete-failedand the topic now sayserror · delete-failedinstead oferror · cancelled. Everything undeclared while aborted is still the cancel —scan's cancel throw is a plain
Error, so a cancelled scan still endserror · cancelled. The comment that claimed the old order is gone.Tests — 58 pass
deno test --allow-read --allow-write --allow-run=python3 backend/, up from 51.roots_test.ts: a df that answers nothing until three of them are waiting atonce (a serial caller deadlocks it, which is the assertion), the configured
order surviving
Promise.all, a missing root and a denied root each costingno df at all, the subset argument, and two probes sharing one df through the
cache.
contract_test.tsgains "no route file walkspolicy.rootsitself" and dropsthe
toContainer(c, root.path)exception those loops needed — a route filemay not name
toContainerat all now.progress_test.ts: the declared reason wins mid-cancel; a reasonlessTaskFailurewhile aborted is stillcancelled.deno lintclean (33 files).deno checkand the module-builder import contractboth clean in a core checkout.
deno fmt --checkclean on every file this branchadds, and on the lines it adds to
README.md; the repo-wide backlog isuntouched.
Approving. I re-ran everything the description claims and checked the core-side premise behind the reorder; both hold.
Verified
deno test --allow-read --allow-write --allow-run=python3 backend/— 58 passed, 0 failed.deno lint— clean, 33 files.deno check backend/mod.ts— clean, with@opsdeck/sdkandhonomapped in from a core checkout.deno fmt --check—roots_test.ts,progress.tsandprogress_test.tsclean; the complaints infsops.ts,routes.ts,ui.tsandcontract_test.tsall land on lines this branch does not touch, so the claim about the added lines holds.terminalReasonreorder is real: core catches atpackages/server/src/tasks/service.ts:986, takese instanceof TaskFailurefirst and only then consultsstate.cancelRequested, sofailed/delete-failedis what the run records for a delete that fails mid-cancel. The new order matches it. And the undeclared throws stay undeclared —h.signal.throwIfAborted()(AbortError),CancelledScan,new Error("scan cancelled")— so a cancelled scan still endserror · cancelledon both sides.actions.ts:385andmod.ts:91iteratepolicy.rootsbut neither stats nor df's them, andui.tsstill needsresolvefor its client-path routes, so nothing is left unused by the rewrite.Notes, none blocking
backend/routes.ts:655— the overview card is the one surface that still cannot say "unreadable".probeRootshands itokanderrorfor free, but/overviewdrops both, so a denied or missing root renders as—free with a 0%-full bar (frontend/OverviewWidget.svelte:63-66), which reads like a healthy empty mount. README now says the three surfaces "cannot disagree about whether a root is readable" — the card does not disagree because it does not answer. Passingokthrough and rendering it would close that.backend/contract_test.ts, "no surface probes the roots itself" — it is a single-line, single-spelling tripwire:policy.roots.forEach(, aconst rs = policy.rootsaliased before the loop, or afor (that fmt has split over lines all slip past it. Fine as a tripwire, but it catches the shape that was there rather than the shape that is possible, and the comment above it promises more than the regex delivers.backend/roots_test.ts,barrierDf—df.done()runs after the assertions, so a failing assertion leaks the 2s timer and Deno's sanitizer reports a leaked timer instead of the assertion that actually failed.try { … } finally { df.done() }, the way the temp dirs are already handled.backend/fsops.ts:436— as flagged in the description, a missing root's error is now the resolver'snot found. Worth knowing thatpaths.tsspells three cases that way (:156, :161, :204), so "the mount is gone" and "this root's path is not inside any root" become the same message on/roots. The deny case keeps its rule and an unreadable-but-present root keeps the OS message from the stat, so the loss is narrow.probeRootsfans out with no cap: N roots with a cold cache is N concurrentdfsubprocesses on one request. That is the point of the change andUsageCachecollapses repeats per path — just noting the burst is now bounded only by how many roots the operator configured.