Timezone-aware cron in a French VPS: the DST hour that ran twice

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

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
赞同度 100% · 0 条评论 · 6 阅读

0 条评论

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

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