Skip to content

test: restore the global metrics collector instead of leaking it (#3780) - #3816

Open
nicoloboschi wants to merge 1 commit into
mainfrom
fix/3780-metrics-collector-leak
Open

test: restore the global metrics collector instead of leaking it (#3780)#3816
nicoloboschi wants to merge 1 commit into
mainfrom
fix/3780-metrics-collector-leak

Conversation

@nicoloboschi

Copy link
Copy Markdown
Collaborator

Fixes #3780.

What was wrong

create_metrics_collector() replaces a module global in metrics.py and nothing ever put the previous collector back. NoOpMetricsCollector ignores its arguments; the real MetricsCollector inspects them (if cached_input_tokens > 0). Provider tests hand it a bare MagicMock usage object, so once anything installed the real collector, every test that ran after it in the same xdist worker was one MagicMock away from:

TypeError: '>' not supported between instances of 'MagicMock' and 'int'

Which files got hit was decided by xdist's distribution, so the failure count moved between runs of identical code — which is what made the suite useless as a signal.

The API lifespan is not the only poisoner. worker/main.py:72 installs a collector as a side effect of constructing create_worker_app, and tests/test_health_probes.py builds that app with no lifespan at all.

The fix

  • metrics.pyreset_metrics_collector(collector=None), the missing counterpart to create_metrics_collector(): puts back a previously installed collector, or the no-op default.
  • api/http.py — the lifespan snapshots get_metrics_collector() before startup and restores it after shutdown_tracing(). Worth doing on its own merits: an app that starts and stops otherwise leaves its own collector, holding a reference to a now-closed DB pool, installed for the rest of the process.
  • tests/conftest.py — an autouse fixture doing the same around every test, following the existing _cleanup_leaked_span_recorders fail-safe. This is the part that actually fixes the suite, since it also covers the construction-time paths that never run a shutdown.

Verification

A/B with a real in-suite poisoner, no synthetic plugin — test_health_probes.py builds the worker app, test_reasoning_effort_explicit_config.py is one of the files the issue lists:

$ uv run pytest tests/test_health_probes.py tests/test_reasoning_effort_explicit_config.py -p no:randomly -n 0
  autouse fixture off: 13 failed, 12 passed
  autouse fixture on:  25 passed

tests/test_metrics_collector_lifecycle.py covers reset_metrics_collector both ways, plus a DB-free test that drives app.router.lifespan_context over a stub engine and asserts the collector is restored — and asserts the app really did install one first, so it cannot pass vacuously.

Scope

  • No production fix, deliberately. _usage_from_openai_response already coerces the field with getattr(usage.prompt_tokens_details, "cached_tokens", 0) or 0, so a real provider response yields 0. Only a MagicMock reaches that comparison.
  • create_worker_app still installs the collector at construction time. Harmless in production — that process runs until exit — and the autouse fixture covers the test consequence. Giving the worker app a lifespan is a restructure beyond this bug.

`create_metrics_collector()` replaces a module global in `metrics.py` and
nothing ever put the previous collector back. Anything that starts the API
app — or builds the worker metrics app, which installs a collector as a
side effect of construction — therefore poisoned every test that ran after
it in the same xdist worker.

`NoOpMetricsCollector` ignores its arguments; the real `MetricsCollector`
inspects them (`if cached_input_tokens > 0`). Provider tests hand it a bare
`MagicMock` usage object, so those tests failed with "'>' not supported
between instances of 'MagicMock' and 'int'" — in whichever files the worker
happened to be given, which is why the failure count moved between runs of
identical code.

- `reset_metrics_collector()` restores a previously installed collector
  (or the no-op default).
- The API lifespan snapshots the collector at startup and restores it at
  shutdown, so an app that starts and stops no longer leaves its own
  collector — holding a closed DB pool — installed for the process.
- An autouse fixture in `tests/conftest.py` does the same around every
  test, covering the paths that never run a shutdown, following the
  existing `_cleanup_leaked_span_recorders` fail-safe.

Verified: `tests/test_health_probes.py tests/test_reasoning_effort_explicit_config.py`
in one process goes from 13 failed / 12 passed to 25 passed.

Production is unaffected — `_usage_from_openai_response` already coerces the
field with `getattr(..., 0) or 0`, so only mock-fed tests could hit it.

Claude-Session: https://claude.ai/code/session_01VpUxYeN2PGLovRhXLAiRoF
@strix-security

strix-security Bot commented Aug 26, 2026

Copy link
Copy Markdown

Strix Security Review

No security issues found.

Updated for 1cdb243.


Reviewed by Strix
Re-run review · Configure security review settings

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Leaked global metrics collector makes provider tests fail depending on xdist worker assignment

1 participant