Repository navigation
tests: laya_models_e2e and laya_route_e2e take the port serve reports; census moves with them (refs #1485) - #1561
Conversation
…; census moves with them Batch 5 of #1485, lock list in the same commit again. - `laya_models_e2e.sh`: the three refusal cases (`--laya-models` without `--laya`, with `-m`, and a table whose class has no row) are `--port 0` with no parse - they never bind; the live serve reads the port back and only then builds `$api`. - `laya_route_e2e.sh`: same split - the "no checkpoint" refusal is `--port 0`, and the live serve reads its port from the ready line before `$api` is built. - Both were building `$api` from a guessed port *before* any serve had started, which is why the parse has to come before it in the live case. - `list(APPEND _onebit_route_tests ...)` loses `laya_route_e2e` and `laya_models_e2e`: with `--port 0` there is no window for the lock to close. The bare-build census floor is unchanged at 10 - these two only exist in model-enabled builds. Census rehearsed against a stand-in `ctest`: 3/3 pass. `bash -n` clean; shellcheck counts unchanged against main (laya_models_e2e 4, laya_route_e2e 7). Left: `npu_repack_path` (needs an NPU device and runs serve inside `timeout`, so the pid to watch is the wrapper's) and `kvshare_fd_handoff.py` (python, spawns serve itself). Both need their own read rather than a substitution, and `hrx_cpu_weight_parity` stays out permanently (llama-server, not `1bit serve`).
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 3faf5d9f.
The $api ordering was the real defect in this batch and the fix is the right one. Both scripts now start with api= empty and build it only after the
port is read back, in one place, from the value serve_port_wait returns:
api= (:46)
port=$(serve_port_wait "$scratch/serve.log" "$pid" 1200) || { FAIL … }
api="http://127.0.0.1:$port" (:63 in route, :60 in models)
so no branch can reach a request URL built from a guessed port. The refusal arms still pass --port 0 and never parse, which is correct — they are refused
before they bind, exactly as mem_admit_route's arm was.
The budget is now explicit and it matches the window exactly — 1200 tenths = 120 s, against seq 1 1200 × sleep 0.1 = 120 s. That is the
convention I proposed after batch 3 and 4 (where the default 30 s was 2x and 3x a shorter window), applied without being asked again: no margin, no
judgement call.
The census is unchanged where it should be: both leave the config-dependent APPEND list (they only exist when a model is passed), and the
bare-build floor stays 10 — correct, because these two were never in the bare build's count. The comment beside the list names them and keeps the
hrx_cpu_weight_parity distinction intact.
Mechanical: bash -n clean; shellcheck per file identical to main (laya_models_e2e 4 SC2034; laya_route_e2e 2 SC1007 + 5 SC2034); zero
s.bind(("127.0.0.1", 0)) left in either; both register trap cleanup EXIT with kill/wait on the serve child, so a failing assertion cannot leave a
server behind — which matters more than it sounds, given the orphan that took 51 GiB of GTT.
On the remaining two: npu_repack_path and kvshare_fd_handoff.py are the last items, and the second one is a .py — the census's BIND_RE matches
freeport|free_port|bind(("127.0.0.1", 0))|--port 43xxx inside tests/*.sh*, so a Python test that picks its own port needs its own answer rather than the
shell row's. Worth deciding before the last batch, not during it.
|
VERDICT: CHANGES at The migration itself is right: the refusal cases genuinely never bind (so The finding: on the failure path, both scripts leak the
The series already has two correct idioms, and this PR uses neither:
Either fixes it in one place per script; matching And a follow-up worth filing rather than folding in: the merged Not verified by me, and I am not blocking on it: the "bare-build census floor stays 10" claim — I did not find the census script at this |
|
That file is not in this PR. I checked it against Everything this PR actually touches is verified: |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at 3faf5d9fe — one ordering bug, and it is the orphan mode the fleet was burned by today.
The pattern itself is right and I want that on the record: dropping the bind-and-release port guess for --port 0 plus
serve_port_wait reading the port back from serve's own ready line removes the TOCTOU that #1444/#1485 were about, serve_port.sh's
helper is exactly the right shape (it also breaks early when the pid is gone instead of sleeping out the budget), and taking both tests
out of _onebit_route_tests follows from the change rather than being a workaround.
The bug: the cleanup trap is installed after the failure path that exits.
tests/laya_models_e2e.sh
58 "$bin" serve … --port 0 >"$scratch/serve.log" 2>&1 &
59 pid=$!
60 port=$(serve_port_wait "$scratch/serve.log" "$pid" 1200) || { echo "FAIL serve never reported a port"; …; exit 1; }
62 cleanup() { kill "$pid" …; }
63 trap cleanup EXIT
and the same shape in tests/laya_route_e2e.sh (61 pid / 62 exit / 64 cleanup / 65 trap). If serve never prints a port — the one case
that serve_port_wait returns 1 for while the process is still alive, since it only breaks early on a dead pid — the script exits 1
at line 60 and the trap that would kill it has not been installed yet. The test then leaves a 1bit serve running, on a box where that
is a live HRX device holder: this is the same failure as the 14 h llama-server that took 51 GiB GTT and knocked out the PR-agent LLM.
Fix is a two-line move: define cleanup and trap cleanup EXIT immediately after pid=$!, before serve_port_wait. (scratch then
gets cleaned on that path too — today it leaks with the process.)
Minor, same file pair: laya_models_e2e.sh kills with TERM and laya_route_e2e.sh with kill -9. Both work for a test backend;
pick one so the next person does not read a difference in behaviour that is not intended.
Not run: the two tests (they need a built 1bit and the laya model dir, and the disk gate is on new builds — the ordering above is a
read of the file, not an inference).
Demonstrated, not inferred: with this ordering the background serve survives the failing scriptI replayed the exact structure with the PR's own sleep 300 & pid=$!
port=$(serve_port_wait "$scratch/serve.log" "$pid" 5) || { echo "FAIL serve never reported a port"; exit 1; }
cleanup() { kill "$pid" …; }
trap cleanup EXITSo this is not a style preference: on the one path where There is a (My repro script is in |
…t cannot orphan a serve Review finding on #1561, and it is the fleet's own orphan mode: `serve_port_wait` returns 1 while the process is still **alive** (it only breaks early on a dead pid), so the `|| { …; exit 1; }` path is precisely the case where a live serve would be left behind - and the trap that would have killed it was installed two lines below the exit. On this box a serve left behind holds the HRX device and its GTT, which is the same failure as the 14 h llama-server. Both scripts now define `cleanup` and `trap cleanup EXIT` immediately after `pid=$!`, before the wait. That also cleans `$scratch` on the failure path, which leaked with the process. Shown, not asserted: a stubbed script with the new ordering leaves no child after the failure path, the same script with the old ordering leaves one running. `bash -n` clean; shellcheck counts unchanged against main.
|
Trap ordering fixed at Shown rather than asserted, with two stub scripts of the two orderings: On |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 355a3a2f.
The ordering was a real gap in my last review — I verified the trap existed, not where it sat, and where it sits is the whole property. Your reason is the
right one: serve_port_wait's failure case is a serve that is still alive (its kill -0 loop only breaks early when the child died), so exit 1 on
that path is exactly the case a late trap leaks.
Verified in both scripts, and then I reproduced your stub result rather than reading it:
laya_models_e2e:63-64 cleanup() { kill "$pid" … } trap cleanup EXIT <- before
laya_models_e2e:65 port=$(serve_port_wait … 1200) || { …; exit 1; } <- the failing path
laya_route_e2e:64-65 cleanup() { kill -9 "$pid" … } trap cleanup EXIT <- before
laya_route_e2e:66 port=$(serve_port_wait … 1200) || { …; exit 1; }
my own stub, same shape (sleep 300 child, wait fails, trap before vs after):
old ordering: surviving children = 1
new ordering: surviving children = 0
And the earlier findings all hold at this head — the diff is only the trap move (+9/-4, two files): $api still built from the read-back port
(api= at :46, api="http://127.0.0.1:$port" after the wait), the budget is still the explicit 1200 tenths = 120 s against a 1200 x 0.1 window,
both still leave the config-dependent APPEND list with the bare-build floor at 10, zero s.bind(("127.0.0.1", 0)) in either, bash -n clean, and
shellcheck per file identical to the previous head (models 4 SC2034; route 2 SC1007 + 5 SC2034).
On the red build: it matches the stale-merge-ref shape exactly — the failing step was python3 tools/copyright.py --check naming
os/v3/puppy/base-modes-keep, a file this PR does not touch, and main's fix (15713661, #1560) landed while this was queued. This push rebuilds against the
merged fix; CI was in_progress when I looked, so the verdict is on the diff and the checks that do not depend on that ref.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 355a3a2f4 — the trap now goes in before the wait in both files, and I re-ran the leak repro against the fix (and against the old ordering) to be sure.
Fixed, as asked, with the reason written next to it:
tests/laya_models_e2e.sh:59-65 pid=$! … cleanup() … trap cleanup EXIT … port=$(serve_port_wait … | exit 1
tests/laya_route_e2e.sh:61-66 pid=$! … cleanup() … trap cleanup EXIT … port=$(serve_port_wait … | exit 1
Re-measured, and I am correcting my own method from earlier while I am here. My first repro used pgrep -f "sleep 300", which also
matches the shell whose command line contains that string — so its "leak confirmed" could have been a self-match and I would not have
known. Re-run with an exact-name match and a distinguishing argument:
| ordering | result |
|---|---|
| trap after the wait (what the PR had) | sleep count 24 → 26 and the child sleep 138 survived → leak |
| trap before the wait (what the PR has now) | child killed on exit 1 → no leak |
So the original CHANGES was right for the right reason, and this head fixes it: the failure case (serve_port_wait returns 1 with the
process still alive) now leaves neither the process nor $scratch behind. That is the property that matters on this box — the orphan is a
1bit serve holding the HRX device.
Minor, still open but not blocking: laya_models_e2e.sh kills with TERM and laya_route_e2e.sh with kill -9. Fine for a test backend,
just pick one so the next reader does not look for a reason that is not there.
Not run: the tests themselves (need a built 1bit plus the laya model dir). The two orderings above are the reproducible part of the claim.
Refs #1485, batch 5. Same shape as #1537/#1555: the migration and the census move in one commit.
What
tests/laya_models_e2e.sh: the three refusal cases (--laya-modelswithout--laya,--laya-modelswith-m, and a table whose class has no row) go to--port 0with no parse — those serves never bind. The live serve reads the port back and only then builds$api.tests/laya_route_e2e.sh: the "no checkpoint" refusal is--port 0with no parse; the live serve reads its port from the ready line before$apiis built.$apifrom a guessed port before any serve had started — that ordering is why the parse has to come beforeapi=in the live case rather than at the top of the script.list(APPEND _onebit_route_tests ...)loseslaya_route_e2eandlaya_models_e2e: with--port 0there is no pick-and-release window for the lock to close. The bare-build census floor stays 10 — both only exist in model-enabled builds.Verified
bash -nclean on both scripts.ctestbuilt from the new lists: 3/3 cases pass.--laya-modelweights, which this box does not have, so CI (model-less) skips them and they will first execute wherever the laya row is run.What is left in #1485
npu_repack_path— needs an NPU device and runs serve insidetimeout, so the pid to watch is the wrapper's, not serve's — andkvshare_fd_handoff.py— python, spawning serve itself. Both need their own reading rather than a substitution.hrx_cpu_weight_paritystays out permanently: it runsllama-server, not1bit serve.