We run a small ingestion service on a French VPS (Debian 12, systemd 252, PostgreSQL 15). It pulls a partner feed every 15 minutes and writes roughly 40k rows/day; one nightly job rebuilds a rollup that downstream dashboards read at 08:00 CET. In late October the rollup ran twice in one night, and the second run overwrote a partial snapshot. Nobody noticed until a dashboard showed half a day of zeros.
What I found: the nightly job was scheduled with a plain cron expression in the application container, while the VPS clock was set to Europe/Paris. The container had no TZ, so it defaulted to UTC. On the DST transition the host and the container disagreed about which hour “02:30” meant, and the rollup fired once on host-local time and once on container time. The fix was not a smarter cron expression; it was removing the ambiguity.
I moved the schedule into systemd timers with an explicit timezone and a monotonic guard. The unit looks like this:
# /etc/systemd/system/rollup.timer
[Unit]
Description=Nightly rollup
[Timer]
OnCalendar=*-*-* 02:30:00 Europe/Paris
Persistent=true
AccuracySec=1min
[Install]
WantedBy=timers.target
And the service refuses to run twice for the same local date:
#!/bin/sh
set -eu
stamp=/var/lib/rollup/last-run
today=$(date +%F)
[ -f "$stamp" ] && [ "$(cat "$stamp")" = "$today" ] && exit 0
/opt/rollup/bin/rebuild --since "$today"
printf '%s\n' "$today" > "$stamp"
The mechanism matters more than the snippet. OnCalendar with an explicit zone makes systemd resolve the local wall-clock time itself, so the DST hour is handled once. Persistent=true catches missed runs after downtime, but it also means a timer can fire late, so the date stamp is the real idempotency guard. The cost is that the stamp file is local state: if we rebuild the VPS from a snapshot older than the last run, the guard disappears and the job runs again. I have hit that once.
Where this breaks: the stamp assumes one job instance per host. If we ever scale to two workers, the file lock is useless and we need a database row with INSERT ... ON CONFLICT DO NOTHING. It also assumes the rollup is safe to skip, which is fine for a dashboard but wrong for billing. And date +%F is still host-local; if the host TZ drifts, the stamp drifts with it.
For anyone running scheduled work against French business hours: do you pin the timezone in the unit, in the app, or both, and how do you keep the idempotency guard from becoming the thing that silently skips a run?
0 条评论
还没有评论,写下第一条。