fix(cachedclient): serve fresh period-based data without calling the API - #15
Merged
Conversation
A dedicated docs/CONTAINER.md covers running the published GHCR image (quick start, Docker Compose, configuration, cache persistence, tool selection, MCP client wiring, provenance). The README gains a short Container Image section that links to it — GHCR renders the repo README on the package page, so the link surfaces there, keeping the SDK-focused README uncluttered. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Pattern B path fetched unconditionally — its own comment said "Always fetch from API" — and used storage only to merge and persist the response. That made it a write-through archive rather than a cache: it never avoided a single request, and `staleness_days` was dead config, referenced nowhere but its own definition. The cost is not theoretical. A 5,316-name universe issues ~21k statement requests (4 endpoints per name) on every rebuild regardless of what is on disk, and the provider's rate limiter stretches that into a 4-5 hour crawl. Measured on a live cache that was 99.2% populated, a rebuild still re-fetched every ticker and never completed. Gate the fetch on the sidecar's `last_updated` against `staleness_days`, which is what that setting was always meant to do. Period-based data is annual and quarterly statements — it changes a handful of times a year, so re-fetching it on every build buys nothing. Freshness errs toward refetching: negative staleness_days, absent metadata, or an unreadable sidecar all fall through to the API. Serving stale data silently is worse than paying for a request.
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.
The bug
_read_period_basedfetched unconditionally. Its own comment said so:Storage was used only to merge and persist the response afterwards. That makes the Pattern B cache a write-through archive, not a cache — it never avoided a single network call.
CachedClientConfig.staleness_daysdocuments itself as controlling exactly this ("How many days old stored data can be before a Pattern B refetch is triggered") but was referenced nowhere except its own definition.Pattern A (date-range) is unaffected —
compute_gapsalready does the right thing there.Why it matters
Pattern B covers the statement endpoints:
income-statement,balance-sheet-statement,cash-flow-statement,key-metrics. A 5,316-name universe therefore issues ~21k requests on every rebuild regardless of what is already on disk, and the provider's rate limiter stretches that into a 4–5 hour crawl.Observed on a live deployment: the cache was measured at 99.2% populated for the configured universe —
— and a rebuild still re-fetched every ticker from scratch, writing 2,842 cache files in 10 minutes while re-downloading data it already had. Four consecutive builds failed to complete.
This is also a needless cost against the API quota: annual and quarterly statements change a handful of times a year.
The fix
Gate the fetch on the stored sidecar's
last_updatedagainststaleness_days. Everything needed already existed —StoredRangeMetadata.last_updatedis populated on write and surfaced byget_stored_range(); only the check was missing.Freshness deliberately errs toward refetching: a negative
staleness_days, absent metadata, or an unreadable sidecar all fall through to the API. Serving stale data silently is worse than paying for one request.staleness_daysis passed to the proxy as a scalar rather than the config object, becauseclient.pyimportsproxy.pyand importing the config back would be circular.Behaviour change
With the default
staleness_days=1, a second call for the same key on the same day is now served from storage instead of re-fetching.test_period_based_merges_dataasserted the old behaviour, so it now setsstaleness_days=-1to exercise the merge path explicitly — the merge logic itself is unchanged.Tests
test_period_based_serves_fresh_from_storage— two calls, exactly one API awaittest_period_based_refetches_when_storage_is_stale— sidecar aged, API called againtest_period_based_fetches_when_nothing_stored— empty cache always reaches the API🤖 Generated with Claude Code