We run a nightly aggregation job for a France-only product. The schedule was a plain 0 2 * * * in a container that had TZ=Europe/Paris set in the Dockerfile. It worked for months, then the March DST switch moved the job to 03:00 local, which overlapped a downstream export window and produced duplicated daily rows for four days before anyone noticed. The dataset is small — about 900k rows/day, ~40MB of Postgres output — so the bug was cheap to detect but expensive to explain to the finance team.
What I did: I stopped trusting the container’s TZ for scheduling and moved the decision into the job itself. The cron entry became 0 1 * * * UTC, and the script computes the target local day explicitly with Python’s zoneinfo:
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
PARIS = ZoneInfo("Europe/Paris")
now_utc = datetime.now(tz=ZoneInfo("UTC"))
local = now_utc.astimezone(PARIS)
# aggregate for the previous local calendar day
target = (local - timedelta(days=1)).date()
print(target.isoformat())
Then the SQL takes the date as a parameter instead of using CURRENT_DATE. I also added a guard that writes the computed target and the UTC timestamp to a small audit table, so a rerun with the same target is a no-op:
INSERT INTO agg_runs (target_date, started_at)
VALUES (%s, now())
ON CONFLICT (target_date) DO NOTHING;
Why this shape: Europe/Paris is a rule, not an offset. Computing the local day from a UTC instant keeps the boundary correct across the two DST transitions, and making the date an explicit parameter means a manual rerun can’t silently pick a different day. The cost is one extra query and a bit of code that has to know the timezone name; the benefit is that the cron line no longer carries hidden state.
Where it breaks: if the job fails and gets retried after the next local midnight, target shifts and you need the audit table to tell you which day you actually meant. I got bitten by exactly that in October — the retry ran at 00:20 Paris time and computed a new target instead of reusing the failed one. Also, zoneinfo needs tzdata present; a slim image without it will raise at import time, which is loud but only on deploy. And if someone later adds a second region, hardcoding Europe/Paris becomes the next trap.
For anyone scheduling France-bound jobs: do you compute the target day inside the job, or keep it in the scheduler and accept the DST edge? I’m curious whether the audit-table approach is overkill for smaller teams.
0 条评论
还没有评论,写下第一条。