Repository navigation
test(ffi): wait for the joined workers' task entries before counting them - #350
Conversation
…them `faer-ffi/tests/fork.rs` asserted that `/proc/self/task` is back to its pre-`create` size immediately after `libfaer_v0_23_team_destroy`, and that read races the workers' exit: `destroy` joins them (threadteam's dedicated-pool drop joins each std handle), but a joined task's `/proc` entry outlives the join, because `std`'s `join` returns when the worker's closure has finished, not when the task is gone. The single read therefore sometimes samples the teardown and reports "destroy left the handle's workers running: 3 != 2", which is what #348 saw on CI (run 42629271729). Reproduced on the pinned 1.96.0 at 3 of 2000 runs of the test binary, with the extra entry still in `/proc` and gone again within 2 ms; over 3000 create/use/destroy cycles the baseline never grew (2 tasks throughout) and the count always came back, so nothing leaks and `destroy` does join. A plain `std::thread::spawn` + `join` with no faer-ffi in the process shows the same over-count, so the observation, not the pool, is what was wrong. Wait for the count to come back, bounded at a second, instead of reading it once. A pool left parked in threadteam's cache — the #333 regression this test guards — never leaves, so the wait times out and the assertion still fails: with the handle's pool forced back to a shared one the test reports "6 threads, 2 before the handle".
GPT-6.1-sol adversarial reviewCRITICAL: none Critical (must not merge)none Needs changesnone Unverified claims
Checked and fine
Reviewed head: |
Fixes the flake behind #348. Refs #348 (do not close it on merge).
Symptom
PR #346 (docs only) failed
ci/Run cargo test -p faer-ffi -j4(run 42629271729, head
eb7c2c46):The same head's
bodypassed andmain(57db5c04,4ccd69b3) was green: not a docs change.Cause
faer-ffi/tests/fork.rs:102counts/proc/self/task— the process's tasks — and the test read itonce, immediately after
libfaer_v0_23_team_destroy:destroydoes join the workers (Team::pool_forgives the pool athread_name, so threadteam 0.5.1treats it as dedicated and
Pool::dropjoins each std handle instead of parking it in the process-widecache —
threadteam-0.5.1/src/pool.rs:295,:399), but a joined task's/procentry outlives thejoin:
std'sjoinreturns when the worker's closure has finished, not when the task is gone, so thesingle read sometimes samples the exit path. That is candidate (a) of the issue, not (b): nothing
leaks.
Measured on the pinned 1.96.0 (Linux x86_64):
The extra task is the handle's own worker and it is gone within 2 ms; over 3000 cycles the baseline
never grew (2 tasks throughout) and the count always came back. So
destroyjoins, and the extrathread was never a leak — the wait is the observation, not the fix.
Change
faer-ffi/tests/fork.rsonly (+30 −4):threads_settle(want, bound): spin-read/proc/self/taskuntil it is back towant, boundedat a second, so the read no longer samples a worker's exit path;
destroyassertion becomesassert!(threads_settle(before, 1s).is_some(), ...), still named"destroy left the handle's workers running" and still checking the same thing.
The #333 regression this test guards is not weakened: with the pool forced back to a shared one
(
thread_nameremoved, the pre-#333 shape) the workers stay parked in threadteam's cache and neverleave, so the wait times out and the assertion still fails:
The rest of the test (the live handle at the fork and
create, use, destroy, fork, create, each childunder
alarm(20)) is untouched.Verification
Recorded tasks:
cargo test -p faer-ffi -j4is the CI step that failed; it is green locally(
cargo test -p faer-ffiabove, plus the 200- and 5000-run loops on the same binary the harnessloads).
Files
faer-ffi/tests/fork.rs—threads_settleand thedestroyassertion. No test was renamed, so noinventory file moves (this repository has no
scripts/ci/*.tsv).Reproduction artifacts (inlined)
The measurement host is
evo_vpn(Linux x86_64, rustc 1.96.0 pinned); its/tmpis not reachable fromthe review side, so the probe source and the retained logs are reproduced here verbatim.
The spawn/join control (no faer-ffi in the process) —
/tmp/faer348-probe/probe.rsSo the transient task is
std::thread::joinitself, independent offaer-ffi:joinreturns when theworker's closure returns, not when the kernel has reaped the task.
The guard still guards:
Team::pool_for's.thread_name(...)removed, on heade90c5185Mutation:
faer-ffi/src/lib.rs,Team::pool_for— drop the.thread_name(...)builder call (the pre-#333shape, pool parked in threadteam's cache). Command and full log
(
/tmp/faer348-repro/guard-shared-pool.log):This fails at the modified assertion (
fork.rs:141, "6 threads") after the one-second bound, i.e. thewait does not paper over a parked pool — it times out, as the original single read would have.
The in-process diagnostic —
/tmp/faer348-repro/diag2.logdiagruns 3000 create/use/destroy cycles in one process, records whether the count is back afterdestroy, and re-reads/proc/self/taskwhile listing the extra task:never_settled=0: every one of the 20 overshoots came back; the baseline (base_first/base_last) nevergrew over 3000 cycles, so nothing leaks. Both artifacts were produced at this head with the pinned
toolchain (
rustc 1.96.0); the mutation run used a separate checkout at/tmp/faer-348onevo_vpn.