Fix uds `get_random` reaper-regex break (+2 nits)

Copilot's 2nd-pass review (PR #468) caught a regression I landed in
the uds fix: `UDSAddress.get_random()` put the per-call token AFTER
`@{pid}` (`no_runtime_*@{pid}.{token}.sock`), which breaks the
`tractor._testing._reap` matcher
`^(?P<name>.+)@(?P<pid>\d+)\.sock$` — so no-runtime orphan socks
stopped matching and never got reaped/attributed. Move the token
INTO the name (`{prefix}.{token}@{pid}.sock`) so the canonical
`@{pid}.sock` suffix stays intact for both that regex and the
`spawn._reap` reconstruction; also bump the token to 8 hex chars.

Also two robustness nits from the same review,
- `tests.conftest._measure_sustained_headroom()`: guard `frac <= 0`
  before `1./frac` — a 0/parked-core freq read would
  `ZeroDivisionError`, get swallowed by the broad `except` into a
  1.0 (no-throttle), defeating the probe on the exact broken box it
  should flag; read 0 as max throttle.
- `scripts/cpu-perf-check`: mark the burn procs `daemon=True` and
  wrap sampling in `try/finally` so a Ctrl-C / error reaps them
  instead of leaving stray CPU hogs.

Regressed-by: 09c50f49 (uds no-runtime token placed after `@pid`)
Found-via: Copilot review #4595803812 (`_testing._reap` regex)
Review: PR #468 (Copilot)
https://github.com/goodboy/tractor/pull/468#pullrequestreview-4595803812

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
test_cpu_throttling
Gud Boi 2026-06-29 18:51:26 -04:00
parent 63498a80c2
commit 51326f4b7a
3 changed files with 34 additions and 19 deletions

View File

@ -112,21 +112,28 @@ def main(
print(f'\n=== sustained {ncpu}-core load ({secs:.0f}s) — the real test ===') print(f'\n=== sustained {ncpu}-core load ({secs:.0f}s) — the real test ===')
stop: float = time.perf_counter() + secs stop: float = time.perf_counter() + secs
procs = [ procs = [
mp.Process(target=_burn, args=(stop,)) mp.Process(target=_burn, args=(stop,), daemon=True)
for _ in range(ncpu) for _ in range(ncpu)
] ]
for p in procs: for p in procs:
p.start() p.start()
# skip the initial ~0.6s ramp, then sample steady-state # skip the initial ~0.6s ramp, then sample steady-state.
# `try/finally` so a Ctrl-C / sampling error still reaps the
# burners instead of leaving stray CPU hogs (daemon=True is the
# backstop should we exit abnormally before the join).
samples: list[int] = [] samples: list[int] = []
time.sleep(0.6) try:
while time.perf_counter() < stop - 0.2: time.sleep(0.6)
if (fr := _cur_freqs_mhz()): while time.perf_counter() < stop - 0.2:
samples.append(sum(fr) // len(fr)) if (fr := _cur_freqs_mhz()):
time.sleep(0.3) samples.append(sum(fr) // len(fr))
for p in procs: time.sleep(0.3)
p.join() finally:
for p in procs:
p.terminate()
for p in procs:
p.join()
if not (samples and pkg_max): if not (samples and pkg_max):
print(' could not sample cur freq — assume OK') print(' could not sample cur freq — assume OK')

View File

@ -236,6 +236,12 @@ def _measure_sustained_headroom(
# under-shoots and still trips the marginal cases). # under-shoots and still trips the marginal cases).
if frac >= throttle_gate: if frac >= throttle_gate:
return 1. return 1.
# a 0/parked-core freq read would `ZeroDivisionError` the
# inverse below — silently swallowed by the outer `except`
# into a 1.0 (no-throttle), defeating the probe on exactly
# the broken box it should flag; read 0 as max throttle.
if frac <= 0:
return max_headroom
return min(max_headroom, 1. / frac) return min(max_headroom, 1. / frac)
except Exception: except Exception:

View File

@ -205,16 +205,18 @@ class UDSAddress(
else: else:
prefix: str = 'no_runtime_actor' prefix: str = 'no_runtime_actor'
# XXX, no live actor -> no `Aid` to key off, so append a # XXX, no live actor -> no `Aid` to key off, so mix a
# per-CALL token: w/o a runtime the sockname is otherwise # per-CALL token into the NAME part for uniqueness:
# a pure fn of `(prefix, pid)`, so two `get_random()` # w/o a runtime the sockname is otherwise a pure fn of
# calls in one proc alias to the SAME sockpath and the # `(prefix, pid)`, so two `get_random()` calls in one
# 2nd `.bind()` trips `EADDRINUSE`. The token makes each # proc alias to the SAME sockpath and the 2nd `.bind()`
# call a distinct file. Safe vs `._reap` cleanup which # trips `EADDRINUSE`. Token goes BEFORE `@{pid}` so the
# only reconstructs the runtime `{name}@{pid}` convention # canonical `...@{pid}.sock` suffix stays intact for the
# (above), never these `no_runtime_*` socks. # reapers keyed off it: `._testing._reap`'s
token: str = uuid4().hex[:6] # `(?P<name>.+)@(?P<pid>\d+)\.sock` regex, and the
sockname: str = f'{prefix}@{pid}.{token}' # `spawn._reap` `{name}@{pid}.sock` reconstruction.
token: str = uuid4().hex[:8]
sockname: str = f'{prefix}.{token}@{pid}'
sockpath: Path = Path(f'{sockname}.sock') sockpath: Path = Path(f'{sockname}.sock')
return UDSAddress( return UDSAddress(