test: restore the global metrics collector instead of leaking it (#3780) - #3816
Open
nicoloboschi wants to merge 1 commit into
Open
test: restore the global metrics collector instead of leaking it (#3780)#3816nicoloboschi wants to merge 1 commit into
nicoloboschi wants to merge 1 commit into
Conversation
`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 ReviewNo security issues found. Updated for Reviewed by Strix |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3780.
What was wrong
create_metrics_collector()replaces a module global inmetrics.pyand nothing ever put the previous collector back.NoOpMetricsCollectorignores its arguments; the realMetricsCollectorinspects them (if cached_input_tokens > 0). Provider tests hand it a bareMagicMockusage object, so once anything installed the real collector, every test that ran after it in the same xdist worker was oneMagicMockaway from: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:72installs a collector as a side effect of constructingcreate_worker_app, andtests/test_health_probes.pybuilds that app with no lifespan at all.The fix
metrics.py—reset_metrics_collector(collector=None), the missing counterpart tocreate_metrics_collector(): puts back a previously installed collector, or the no-op default.api/http.py— the lifespan snapshotsget_metrics_collector()before startup and restores it aftershutdown_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_recordersfail-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.pybuilds the worker app,test_reasoning_effort_explicit_config.pyis one of the files the issue lists:tests/test_metrics_collector_lifecycle.pycoversreset_metrics_collectorboth ways, plus a DB-free test that drivesapp.router.lifespan_contextover a stub engine and asserts the collector is restored — and asserts the app really did install one first, so it cannot pass vacuously.Scope
_usage_from_openai_responsealready coerces the field withgetattr(usage.prompt_tokens_details, "cached_tokens", 0) or 0, so a real provider response yields0. Only aMagicMockreaches that comparison.create_worker_appstill 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.