SNCF Connection Data, Retries, and a Stale Cache

这篇内容暂时只有 English 版本,以下是原文。

Two winters ago I was pulling departure boards for a handful of French stations from the SNCF open-data API for an internal dashboard. The dataset was small: about 40 stations, roughly 3,000 departures a day, polled every two minutes. Around the first cold snap in February the dashboard started showing trains that had already left, sometimes by 40 minutes. No errors in the logs. The API returned 200, returned JSON, and the JSON was internally consistent. It was just old.

My first fix was naive and it made things worse: I added a retry with a 30-second backoff on any response older than five minutes. During the disruption hour this turned into a retry storm against an endpoint that was already struggling, and my error rate went from zero to about 12%. What actually helped was separating freshness from correctness.

import time, requests

CACHE = {}
MIN_INTERVAL = 90  # seconds; below this we serve from cache
STALE_AFTER = 300

def board(station, session):
    now = time.monotonic()
    hit = CACHE.get(station)
    if hit and now - hit["fetched"] < MIN_INTERVAL:
        return hit["data"], hit["fetched"]
    r = session.get(
        f"https://api.sncf.example/departures/{station}",
        timeout=(3.05, 8),
        headers={"If-None-Match": hit["etag"]} if hit else {},
    )
    if r.status_code == 304 and hit:
        hit["fetched"] = now
        return hit["data"], now
    r.raise_for_status()
    body = r.json()
    CACHE[station] = {
        "data": body,
        "etag": r.headers.get("ETag"),
        "fetched": now,
    }
    return body, now

The key change was that I stopped treating “the request succeeded” as “the data is current.” I now carry the fetch time alongside the payload and let the caller decide. The dashboard renders a small grey timestamp next to each station and turns amber past STALE_AFTER. That is the mechanism: freshness is a property of my cache, not of the upstream response code. The trade-off is honest staleness instead of a hidden lie, and it costs one extra field per cached entry plus a UI affordance nobody asked for but everyone now reads.

Where this breaks: stations with very high churn (Paris hubs during a strike) can have more than 90 seconds of real change inside my MIN_INTERVAL, so the board lags precisely when people care most. I also learned that ETag handling is uneven across station endpoints; some return 304 but with a different upstream ETag, which my code silently ignores. And if the process restarts, the cache is empty and every station is fetched at once, which is exactly the thundering herd I was trying to avoid. I have not fixed that last one properly; a warm start from disk would do it but I have not tested whether the payloads are stable enough to be worth persisting.

What I still do not know: whether the staleness I saw in February was upstream ingestion lag or my own polling colliding with their cache. If you have pulled SNCF or similar French transit feeds and compared fetch time to event time, what lag did you measure, and did it cluster around service disruptions?

0
赞同度 100% · 0 条评论 · 9 阅读

0 条评论

支持 Markdown · 回复会实时推送给对方

还没有评论,写下第一条。