fix(ci): prevent native detector from hanging test shards (#2996)

## Description

CI shard 4 was not merely slow: after thousands of fast tests it parked
indefinitely inside `headroom._core.detect_content_type` at 0% CPU. The
router watchdogged only the first native call and then permanently
trusted direct calls via `_detect_native_verified`. Earlier suite
activity can change ORT/native state after that first success, making a
later call deadlock until GitHub cancels the job.

This keeps every native call bounded by the existing watchdog, activates
the process-wide pure-Python circuit breaker after a timeout, restores
the test-job ceiling to 30 minutes, and removes a separate wall-clock
scheduler assertion that generated false shard-1 failures despite the
structural regression guards passing.

No issue is auto-closed by this infrastructure repair.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Removed the unsafe process-lifetime `_detect_native_verified` fast
path.
- Kept every native detection call behind the existing bounded watchdog.
- Preserved the process-wide fallback circuit breaker so only the first
wedged call consumes the watchdog budget.
- Added a success-then-hang regression test.
- Isolated native circuit-breaker state in fallback exception tests.
- Restored the CI test timeout from the temporary 90-minute diagnostic
ceiling to 30 minutes.
- Replaced the Codex scheduler's noise-sensitive p99/p50 assertion with
its meaningful absolute regression ceiling while retaining source-level
guards against the removed semaphore and nested executor.
- Corrected import order and formatting defects inherited from current
main so the synthetic merge commit passes repository-wide lint.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
Exact local shard-4 command with coverage:
2723 passed, 172 skipped, 8661 deselected in 108.40s

Focused detector/router suite:
62 passed

Codex scheduler suite:
3 passed, 1 skipped

ruff check .
All checks passed!

ruff format --check .
1411 files already formatted

mypy headroom/transforms/content_router.py
Success: no issues found in 1 source file
```

Exact-head GitHub CI on `28f284c7a156d5be2fac2d21ba904a19d3389e6d` is
entirely green. Test jobs 1–4, test-extras, test-agno, build, wheel,
lint, CodeQL, dependency audit, secret scan, smoke, governance, and
conflict checks all passed. Remaining skips are path-filtered jobs not
applicable to this diff.

## Real Behavior Proof

- Environment: macOS arm64/Python 3.13 locally; GitHub-hosted
Ubuntu/Python 3.12 using the production CI workflow and prebuilt wheel.
- Exact command / steps: reproduced `pytest tests scripts/tests --splits
4 --group 4 ...` hanging in native detection; sampled the parked
process; reran with `pytest-timeout` to locate `_rust_detect`; applied
the correction; reran the exact shard locally and all four CI shards
remotely.
- Observed result: local shard 4 completed in 1:48. GitHub shard 4's
pytest step completed in 5:45 and its full job in 8:06 under the
restored 30-minute ceiling. All four shards passed on the same head.
- Not tested: deliberately wedging a real production ORT runtime outside
the deterministic mocked regression; the watchdog behavior is covered
with a native-call fake that succeeds once and then never returns.

## Runtime Rollout Safety

- Rollout-managed feature(s): native content detection watchdog and
fallback only.
- Minimum rollout channel: normal patch release; no staged feature flag
required.
- Stable/default behavior changed: every native detection call remains
watchdog-bounded instead of only the first successful call.
- Kill switch / disable path: `HEADROOM_DETECT_BACKEND=python` bypasses
native detection; `HEADROOM_DETECT_TIMEOUT_SECS` controls the watchdog
budget.
- Unsafe override required: none.
- Qualification impact: full Python CI matrix must remain green; exact
shard-4 completion is the primary qualification evidence.
- Rollback path: human revert of this PR if bounded calls cause an
unexpected regression; setting the Python backend provides an immediate
operational fallback without code rollback.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation — inline
lifecycle documentation and PR operational notes; no user-facing docs
change is needed
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

Not applicable; no UI change.

## Additional Notes

Human review only. No merge or auto-merge action has been configured.
The branch includes current main and preserves the MCP SDK compatibility
cap `mcp>=1.28.1,<2.0.0`.
This commit is contained in:
JD Davis
2026-08-13 20:47:55 -05:00
committed by GitHub
parent 3145242645
commit a708c0571e
6 changed files with 42 additions and 45 deletions
+2 -6
View File
@@ -227,9 +227,7 @@ class HeadroomOtelMetrics:
)
self._proxy_attempted_input_tokens = self._meter.create_counter(
"headroom.proxy.tokens.attempted_input",
description=(
"Input tokens Headroom attempted to optimize before compression."
),
description=("Input tokens Headroom attempted to optimize before compression."),
unit="1",
)
self._proxy_output_saved_tokens = self._meter.create_counter(
@@ -239,9 +237,7 @@ class HeadroomOtelMetrics:
)
self._proxy_savings_usd = self._meter.create_counter(
"headroom.proxy.savings.usd",
description=(
"Estimated savings in USD by distinct Headroom or provider-cache layer."
),
description=("Estimated savings in USD by distinct Headroom or provider-cache layer."),
unit="USD",
)
self._proxy_saved_tokens = self._meter.create_counter(
+1 -1
View File
@@ -14,12 +14,12 @@ import math
import os
import tempfile
import threading
from collections.abc import Mapping
from csv import DictWriter
from datetime import datetime, timedelta, timezone
from functools import lru_cache
from io import StringIO
from pathlib import Path
from collections.abc import Mapping
from typing import Any
from headroom import paths as _paths
+7 -15
View File
@@ -98,7 +98,6 @@ split_into_sections = _mixed_content.split_into_sections
_detect_backend_warned = False
_detect_panic_warned = False
_detect_native_unhealthy = False # circuit breaker: native detect hung once (#575)
_detect_native_verified = False # native detect has returned once -> skip the watchdog
# Shared calibrated fallback estimator (tiktoken cl100k_base ~90% accuracy,
@@ -917,7 +916,6 @@ def _detect_content(content: str) -> DetectionResult:
`_strategy_from_detection` keys off that field alone.
"""
global _detect_backend_warned, _detect_panic_warned, _detect_native_unhealthy
global _detect_native_verified
# Detect on the unwrapped payload so a tool-output envelope's tags don't get
# the whole result misclassified as HTML/XML (#route-converter corruption).
@@ -957,19 +955,13 @@ def _detect_content(content: str) -> DetectionResult:
from headroom._core import detect_content_type as _rust_detect
try:
# The native detector can deadlock on FIRST use (#575 — seen on Windows
# and macOS/arm64). Bound it with a watchdog so a hang degrades to the
# pure-Python detector; the previous win32-only guard left other
# platforms unprotected, so a hung Linux sidecar silently stopped
# compressing (every request failed open to passthrough). Watchdog until
# the native detector has returned once, then use the direct fast path —
# the hang is first-use only, so steady state pays no per-call thread
# overhead. win32 keeps watchdogging every call (unchanged).
if sys.platform == "win32" or not _detect_native_verified:
rust_result = _rust_detect_watchdogged(_rust_detect, content, _detect_timeout_secs())
else:
rust_result = _rust_detect(content)
_detect_native_verified = True # returned without hanging -> trusted hot path
# Native detector state can become wedged after an earlier successful
# call (for example when another test or component initializes ORT).
# A one-time "verified" fast path therefore turns a later native stall
# into an unbounded process hang. Keep every call bounded; on timeout
# the process-wide circuit breaker below makes subsequent calls use the
# pure-Python detector without spawning more watchdog threads.
rust_result = _rust_detect_watchdogged(_rust_detect, content, _detect_timeout_secs())
# Rust's `content_type` is the lowercase string tag (e.g.
# "json_array"); translate to the Python `ContentType` enum so
# downstream mapping keys match.
+6 -22
View File
@@ -300,30 +300,14 @@ def test_concurrent_compression_has_no_semaphore_tail() -> None:
)
assert not errors, f"Got {len(errors)} errors; first: {errors[0].error}"
ratio = p99 / max(p50, 1)
SEMAPHORE_P99_CEILING_MS = 1_000.0
assert p99 < SEMAPHORE_P99_CEILING_MS, (
f"p99 is {p99:.0f}ms; expected < {SEMAPHORE_P99_CEILING_MS:.0f}ms on "
"uniform-size workload. The pre-fix semaphore baseline was ~2433ms."
)
# The p99/p50 ratio only signals contention when the tail is also
# *absolutely* large. On a fast/quiet runner p50 rounds toward 0ms, so the
# ratio collapses to "p99 in ms" and a few milliseconds of ordinary
# scheduler jitter reads as a spurious multiple (e.g. p50=0ms, p99=5ms →
# ~5×) that has nothing to do with the semaphore. The deleted semaphore
# produced a tail of *tens* of milliseconds (and ~27×); a healthy run keeps
# p99 in the single-digit-ms range regardless of ratio. So only treat a high
# ratio as a regression once p50 is measurable and p99 clears a noise floor.
# Hosted CI can occasionally park one worker for a few dozen milliseconds
# even when the compression path is healthy; the semaphore regression this
# test guards against had a seconds-scale p99 and is still bounded by the
# hard p99 guard above.
SEMAPHORE_TAIL_FLOOR_MS = 75.0
assert p50 < 1.0 or ratio < 4.0 or p99 < SEMAPHORE_TAIL_FLOOR_MS, (
f"p99/p50 ratio is {ratio:.1f}× (p50={p50:.0f}ms, p99={p99:.0f}ms). "
f"Expected < 4× on uniform-size workload once p50 is measurable and p99 clears "
f"the {SEMAPHORE_TAIL_FLOOR_MS:.0f}ms noise floor — a high ratio with a large "
f"absolute tail means the semaphore-induced contention tail may be back. "
f"Pre-fix baseline ratio on this same workload shape was ~27× regardless "
f"of CPU speed."
)
# Do not add a p99/p50 wall-clock ratio here. A hosted runner can park one
# worker independently of this code path, making an otherwise healthy
# 2ms/76ms distribution look like a 35x contention tail. The property is
# covered structurally above (the semaphore and nested executor must stay
# absent), while this absolute ceiling still rejects the measured 2433ms
# pre-fix behavior without pretending scheduler jitter is product state.
@@ -20,8 +20,9 @@ from headroom.transforms import content_router as cr
@pytest.fixture(autouse=True)
def _compatible_mock_native_runtime(monkeypatch: pytest.MonkeyPatch) -> None:
"""These tests replace the native detector, so keep the ORT preflight open."""
"""Keep mocked native calls reachable regardless of prior test state."""
monkeypatch.setattr(ort_runtime, "rust_ort_runtime_compatible", lambda: True)
monkeypatch.setattr(cr, "_detect_native_unhealthy", False)
def test_falls_back_on_rust_exception(monkeypatch):
+24
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
import threading
from types import SimpleNamespace
import pytest
@@ -160,6 +161,29 @@ def test_content_signature_and_detection_helpers(monkeypatch: pytest.MonkeyPatch
assert result.metadata == {}
def test_native_detection_remains_bounded_after_success(monkeypatch: pytest.MonkeyPatch) -> None:
"""A successful native call must not disable the watchdog for later calls."""
import headroom._core as _core
monkeypatch.setenv("HEADROOM_DETECT_BACKEND", "rust")
monkeypatch.setattr(content_router_module, "_detect_timeout_secs", lambda: 0.01)
calls = 0
def _succeeds_then_hangs(_content: str) -> SimpleNamespace:
nonlocal calls
calls += 1
if calls == 1:
return SimpleNamespace(content_type="plain_text")
threading.Event().wait()
raise AssertionError("unreachable")
monkeypatch.setattr(_core, "detect_content_type", _succeeds_then_hangs)
assert _detect_content("first").content_type is ContentType.PLAIN_TEXT
assert _detect_content("second").content_type is ContentType.PLAIN_TEXT
assert content_router_module._detect_native_unhealthy is True
def test_mixed_content_section_splitting_and_json_extraction() -> None:
content = "\n".join(
[