Wave B-2: correctness — flush backoff, Flask dispose, exclude_hosts#37
Conversation
…12) Distinct from exclude_patterns (substring). Wave B-2 step 1 of 3 — the init() exclude logic and *-rejection ride in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… parity (#12) - New: exact-host exclusion via exclude_hosts (short-circuits substring matching). Both fields apply additively. - New: init() raises ValueError if any exclude_patterns entry contains '*' — substring is not glob; error points users at exclude_hosts. - Fixed: cloud mode now derives base_url's HOST (not the full URL substring) for auto-exclusion. When base_url is a loopback, 127.0.0.1 and localhost are both excluded so the substring quirk can't half-match. - Fixed: local mode excludes both loopback hosts directly and uses a :PORT substring pattern for port-specific belt-and-suspenders. Adds TestExcludeSemantics (6 tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…es (#10) Persistent flush errors (malformed metric, transport unreachable) used to fire every flush_interval forever, generating CPU + log spam. After 5 consecutive failures, _timer_loop now defers the next tick by min(1s * 2**(N-5), 5min) and announces the backoff via on_error — exactly once per cap doubling, not once per tick. Counter resets on the next successful flush. Reuses the existing _deferral_ms cell so the 429 path and the flush-backoff path compose via max() (longer wait wins). flush_and_send() now returns bool so the timer can distinguish a real successful send from a no-op empty-window flush — only a true send resets the failure streak. Adds TestFlushLoopBackoff (4 tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the same RecostExtension instance binds to multiple apps (e.g. in pytest fixtures or config-reload setups), init_app() previously left self._handle pointing at the now-disposed handle from the prior call. The fix adds a defensive dispose at the Flask layer so self._handle is never stale. Idempotent — safe when self._handle is already None. Adds TestInitAppDisposePrevious (2 tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add exclude_hosts to configuration table and clarify the distinction: exclude_patterns is substring match (no glob), exclude_hosts is exact host match for unambiguous exclusion without false-positives. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe PR extends the Recost telemetry client with exact-host event exclusion, exponential backoff for repeated flush failures with operator visibility, improved flush return semantics, and Flask re-initialization safety. It addresses issue ChangesEvent Exclusion, Backoff, and Flask Reinit
Sequence Diagram(s)sequenceDiagram
participant App as Flask App
participant Ext as RecostExtension
participant Handle as RecostHandle
participant Timer as Timer Loop
participant Agg as Aggregator
participant Trans as Transport
App->>Ext: init_app(first_app)
Note over Ext: no prior handle
Ext->>Handle: __init__()
Handle->>Timer: start()
App->>Ext: init_app(second_app)
Note over Ext: prior handle exists
Ext->>Handle: dispose()
Note over Timer: stops, cleanup
Ext->>Handle: __init__()
Handle->>Timer: start()
Timer->>Agg: flush()
Agg->>Trans: send(summary)
alt Success
Trans-->>Timer: OK
Note over Timer: reset consecutive_failures
else Failure
Trans-->>Timer: exception
Note over Timer: consecutive_failures++
alt consecutive_failures ≥ 5
Timer->>Timer: exponential backoff
Note over Timer: defer(backoff_ms)
Note over Timer: emit RecostError
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
Summary
Closes #10
Closes #12
Closes #20
Three correctness bugs, no external dependencies.
#10 — Flush-loop exponential backoff
_timer_loopnow tracks consecutive failures. After 5, it defers the next tick bymin(1s * 2**(N-5), 5min)via the existing_deferral_mscell (composes with 429-deferral viamax). The backoff is announced viaon_errorexactly once per cap doubling, not once per tick. Counter resets on the next successful flush.Side change:
flush_and_send()now returnsboolso the timer can distinguish a real successful send from a no-op empty-window flush — only a true send resets the failure streak (otherwise sparse traffic would prevent backoff from ever engaging).#20 — Flask
init_app()dispose-previousRecostExtension.init_app()now disposesself._handlebefore reassigning. The same extension instance can bind to multiple apps without leaking a stale handle reference. Inherited by the deprecatedReCostalias.#12 —
exclude_hosts+*-rejection + loopback parityRecostConfig.exclude_hosts: List[str]field — exact-host match, distinct fromexclude_patterns(substring).init()raisesValueErrorfor anyexclude_patternsentry containing*. Error message points atexclude_hosts.base_url's host for auto-exclusion (not the full URL substring) — can't false-positive on URLs containing the base host as a query value.base_url=http://localhost:3000) now auto-excludes bothlocalhostand127.0.0.1hosts.Tests added
12 new tests across
test_init.pyandtest_flask.py:TestFlushLoopBackoff(4) — threshold, announcement, reset on success, bounded announcement count.TestExcludeSemantics(6) —*-rejection, exact host match, cloud base_url host-only exclusion, loopback parity (cloud + local), local port substring.TestInitAppDisposePrevious(2) — dispose-previous on rebind, idempotent on first call.Local: 197 passed, mypy clean, ruff clean.
Test plan
mypy recost/clean.ruff check recost/ tests/clean.exclude_hostsvsexclude_patterns.🤖 Generated with Claude Code