Skip to content

fix(cachedclient): serve fresh period-based data without calling the API - #15

Merged
codemug merged 2 commits into
mainfrom
fix/period-cache-serves-from-storage
Aug 10, 2026
Merged

fix(cachedclient): serve fresh period-based data without calling the API#15
codemug merged 2 commits into
mainfrom
fix/period-cache-serves-from-storage

Conversation

@codemug

@codemug codemug commented Aug 10, 2026

Copy link
Copy Markdown
Owner

The bug

_read_period_based fetched unconditionally. Its own comment said so:

"""Cache logic for Pattern B (period-based) endpoints."""
original_limit = bound_args.get(meta.limit_param) if meta.limit_param else None

# Always fetch from API with the caller's original params
result = await real_method(*original_args, **original_kwargs)

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_days documents 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_gaps already 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 —

income-statement          5274 / 5316
balance-sheet-statement   5266 / 5316
cash-flow-statement       5251 / 5316
key-metrics               5281 / 5316

— 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_updated against staleness_days. Everything needed already existed — StoredRangeMetadata.last_updated is populated on write and surfaced by get_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_days is passed to the proxy as a scalar rather than the config object, because client.py imports proxy.py and 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_data asserted the old behaviour, so it now sets staleness_days=-1 to exercise the merge path explicitly — the merge logic itself is unchanged.

Tests

  • test_period_based_serves_fresh_from_storage — two calls, exactly one API await
  • test_period_based_refetches_when_storage_is_stale — sidecar aged, API called again
  • test_period_based_fetches_when_nothing_stored — empty cache always reaches the API
tests/test_cachedclient/   104 passed
tests/                     935 passed
ruff check + ruff format --check   clean

🤖 Generated with Claude Code

codemug and others added 2 commits August 6, 2026 01:29
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.
@codemug
codemug merged commit d39bef2 into main Aug 10, 2026
1 check passed
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.

1 participant