fix(telemetry): stop blocking the caller and leaking the flush thread (#105) - #133
Merged
Merged
Conversation
…n close (#105) The opt-in telemetry collector is an advertised public option -- both clients take `enable_telemetry` and `configure_telemetry` is exported -- so it is fixed rather than deleted. - track_request no longer calls _flush on the caller. It buffers (bounded at 1000 events, oldest dropped) and wakes the background thread. Delivery, which is a blocking httpx.post(timeout=5), now only ever runs on that thread, so AsyncOilPriceAPI no longer posts from the event loop. - close() sets enabled=False, signals the flush loop to drain and exit, and joins it with a bounded timeout. It is idempotent and never raises. - configure_telemetry closes the previous global instance instead of leaking its thread. - Both clients route tracking through an identical _track_telemetry guard, so a telemetry failure can no longer change or fail a request result. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ao5paex73xXvuM424Libo
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…105) respx is not in the [dev] extra, so the new module failed to import on every matrix Python and collection errored. Mock at httpx.Client.request / httpx.AsyncClient.request instead, which is what the rest of tests/unit does. Same assertions, same red against pre-fix code (10 failed, 2 passed). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ao5paex73xXvuM424Libo
Merged
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.
Decision first: the telemetry transport is USED, so it is fixed, not deleted
Issue #105 offers two branches — delete the collector if it has no consumer, or
fix its lifecycle if it does. It has a consumer: the collector is an advertised
public option on both clients, so deleting it is a breaking public API change,
which the issue explicitly forbids ("Do not silently remove an advertised option").
Grep on
origin/main(7982b0b), source only:OilPriceAPI(..., enable_telemetry=True)andAsyncOilPriceAPI(..., enable_telemetry=True)are documented constructor parameters, and
configure_telemetry/get_telemetryare module-level API. Both stay. This PR changes no signature and removes no option.
Per-defect verdicts, re-verified against
origin/maintodaytrack_requestruns a blockinghttpx.post(timeout=5)on the caller every 10 events;AsyncOilPriceAPIdoes it on the event looptelemetry.py:167-168callsself._flush();telemetry.py:192posts synchronously under a comment claiming "non-blocking"close()only flushes; never setsenabled=False, never stops the daemon threadtelemetry.py:213-216configure_telemetryreplaces the global instance without stopping the prior threadtelemetry.py:242client.py:295andasync_client.py:257calltrack_requestunguarded on the success path, immediately beforereturn response.json()The change (smallest correct; no new telemetry platform)
oilpriceapi/telemetry.pytrack_requestbuffers and sets anEvent; it never delivers. The buffer isbounded at
MAX_BUFFERED_EVENTS = 1000with the oldest dropped, so anunreachable collector cannot grow memory without limit.
_flushruns. It waits onthe event with
flush_intervalas the timeout, so a full buffer wakes itimmediately and
close()wakes it at once.close()sets_stopping, clearsenabled, wakes the thread for a finaldrain, and joins with a bounded
close_timeout(default 2.0s). It isidempotent, safe on a never-started collector, and never raises.
configure_telemetrycloses the previous global instance before returning.oilpriceapi/client.pyandoilpriceapi/async_client.py_track_telemetry(**fields),a byte-identical guard in both files that swallows any telemetry exception
into
logger.debug.close()in both also guardsself._telemetry.close().Red
New tests run against unmodified
origin/mainsources(
git checkout origin/main -- oilpriceapi/telemetry.py oilpriceapi/client.py oilpriceapi/async_client.py,then restored):
The two that passed pre-fix are the disabled-mode guards (no thread, no network),
which already held and are kept as regression pins.
Green
The 15.30s -> 0.47s drop is the defect itself: the pre-fix runtime is five
rounds of 3s caller-thread blocking on a deliberately slow sink.
Full suite, same venv, before and after:
876 = 864 + the 12 new tests. The 3 failures are unchanged and environmental:
tests/integration/test_demo_contract.pymakes live calls and is getting HTTP 429.ruff check oilpriceapi/-> All checks passed.mypy oilpriceapi/ --ignore-missing-imports-> Success: no issues found in 47 source files. CI is green on all five matrix Pythons (3.8-3.12) plus the live-API job.Sync and async stayed identical
test_sync_and_async_clients_share_identical_telemetry_handlingpins it:inspect.getsource(OilPriceAPI._track_telemetry) == inspect.getsource(AsyncOilPriceAPI._track_telemetry),neither
requestcallstrack_requestdirectly any more, both call_track_telemetry, and bothclose()bodies callself._telemetry.close().That test fails if either file is changed without the other.
Test hygiene
An autouse fixture replaces
oilpriceapi.telemetry.httpx.postwith a functionthat raises if called, and each test then installs a local in-process sink. API
calls are mocked at
httpx.Client.request/httpx.AsyncClient.request, theconvention the rest of
tests/unitalready uses -- the first push usedrespx,which is not in the
[dev]extra and errored collection on every matrix Python;the second commit removes that dependency with no change to the assertions. No
test in this file can reach the network. Every test stops its collector in a
finally, so no thread outlives a test.Scope notes
1.14.0is already on PyPI and is unchanged here.1.14.0is published, and the only## [Unreleased]heading sits mid-file below
1.12.7while a release-readiness test pins it toexactly one occurrence. Flagging that heading placement for the maintainer
rather than fixing it in a telemetry PR.
Closes #105
🤖 Generated with Claude Code
https://claude.ai/code/session_015ao5paex73xXvuM424Libo