Skip to content

Commit ef0512b

Browse files
fix(sync): invalidate API in-memory cache after sync-postgres
sync-postgres updates PostgreSQL but the Cloud Run API caches spec responses in-process with a 24h TTL, so fresh data (e.g. today's bucket-name migration in f37ebbd) remained invisible to clients. The cache comment said "data only changes on deploy" — sync is the counter-example that was never wired up. - POST /debug/cache/invalidate: flushes the TTLCache; auth via X-Cache-Token header matched against CACHE_INVALIDATE_TOKEN; 503 if the token is unset so the endpoint is inert by default. - sync-postgres.yml: after a successful sync, POSTs to the endpoint with the GH Actions secret, skipping silently if unset. Pushing this change also redeploys the backend (api/ is part of the Cloud Build trigger), which clears the in-memory cache immediately and unblocks interactive plots that were 400'ing against the stale pyplots-images URLs still held in the old cache. To fully activate recurring invalidation, set CACHE_INVALIDATE_TOKEN as both a Cloud Run secret env var and a GitHub Actions repo secret. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f37ebbd commit ef0512b

3 files changed

Lines changed: 55 additions & 1 deletion

File tree

.github/workflows/sync-postgres.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,24 @@ jobs:
7171
GCS_BUCKET: ${{ vars.GCS_BUCKET || 'anyplot-images' }}
7272
ENVIRONMENT: production
7373

74+
- name: Invalidate API cache
75+
env:
76+
API_URL: ${{ vars.API_BASE_URL || 'https://api.anyplot.ai' }}
77+
CACHE_INVALIDATE_TOKEN: ${{ secrets.CACHE_INVALIDATE_TOKEN }}
78+
run: |
79+
if [ -z "${CACHE_INVALIDATE_TOKEN}" ]; then
80+
echo "CACHE_INVALIDATE_TOKEN not set — skipping cache invalidation (cache will fall back to TTL expiry)"
81+
exit 0
82+
fi
83+
status=$(curl -sS -o /tmp/invalidate.json -w '%{http_code}' \
84+
-X POST "${API_URL}/debug/cache/invalidate" \
85+
-H "X-Cache-Token: ${CACHE_INVALIDATE_TOKEN}")
86+
echo "HTTP ${status}"
87+
cat /tmp/invalidate.json || true
88+
if [ "${status}" != "200" ]; then
89+
echo "::warning::Cache invalidation returned HTTP ${status} (sync succeeded; cache will fall back to TTL expiry)"
90+
fi
91+
7492
- name: Summary
7593
run: |
7694
echo "### Sync Complete" >> $GITHUB_STEP_SUMMARY

api/routers/debug.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
import time
66
from datetime import datetime, timezone
77

8-
from fastapi import APIRouter, Depends, Request
8+
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
99
from pydantic import BaseModel
1010
from sqlalchemy.ext.asyncio import AsyncSession
1111

12+
from api.cache import clear_cache, get_cache_stats
1213
from api.dependencies import require_db
14+
from core.config import settings
1315
from core.constants import SUPPORTED_LIBRARIES
1416
from core.database import SpecRepository
1517

@@ -283,3 +285,31 @@ async def get_debug_status(request: Request, db: AsyncSession = Depends(require_
283285
system=system_health,
284286
specs=specs_status,
285287
)
288+
289+
290+
class CacheInvalidateResponse(BaseModel):
291+
cleared: int
292+
maxsize: int
293+
ttl: int
294+
295+
296+
@router.post("/cache/invalidate", response_model=CacheInvalidateResponse)
297+
async def invalidate_cache(x_cache_token: str | None = Header(default=None)) -> CacheInvalidateResponse:
298+
"""Flush the in-memory response cache.
299+
300+
Called by sync-postgres at the end of a successful sync so clients see
301+
fresh data without waiting for TTL expiry. Requires the shared token
302+
`CACHE_INVALIDATE_TOKEN` in the `X-Cache-Token` header; returns 503 if
303+
no token is configured on the server.
304+
"""
305+
expected = settings.cache_invalidate_token
306+
if not expected:
307+
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Cache invalidation not configured")
308+
if x_cache_token != expected:
309+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid cache token")
310+
311+
stats_before = get_cache_stats()
312+
clear_cache()
313+
return CacheInvalidateResponse(
314+
cleared=stats_before["size"], maxsize=stats_before["maxsize"], ttl=stats_before["ttl"]
315+
)

core/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ class Settings(BaseSettings):
143143
cache_maxsize: int = 1000
144144
"""Maximum number of cache entries"""
145145

146+
cache_invalidate_token: Optional[str] = None
147+
"""Shared secret required by the POST /debug/cache/invalidate endpoint.
148+
When unset, the endpoint is disabled (503). Set via Secret Manager in Cloud Run
149+
and as a GitHub Actions secret so sync-postgres can invalidate the cache after
150+
writing new data to PostgreSQL."""
151+
146152
# =============================================================================
147153
# CORS
148154
# =============================================================================

0 commit comments

Comments
 (0)