Skip to main content
FomiVIP
Tech Sharing

The Nightly Batch Ran Twice: A 2 AM Reconciliation Lesson

A reconciliation script ran at 2 AM every night — until the client's finance team got two summaries for the same day. The script wasn't wrong: the same line had been pasted twice into crontab, 7 rows apart. We added flock, a run_id per execution, a run-log table, and moved crontab into Git.

October 13, 2017Cron JobsPostmortemReconciliation

The 2 AM reconciliation script ran for months without incident. On the morning of October 13, the client's finance team sent a screenshot: two summaries for the same day, one entry apart in total.

1. First Suspect: a Bug in the Script

Half an hour into reading the script, the idempotency logic looked fine — a unique key on date plus store ID. Then the execution log showed it: one complete run at 2:00 and another at 2:01. Both finished.

2. The Same Line Pasted Twice in crontab

SSH in, run crontab -l — 30-odd lines on one screen, and lines 8 and 15 were identical. Weeks earlier, a temporary backfill job had been pasted in and never removed; the two runs were exactly 1 minute apart. By the time the second run started, the first batch was already in the database. The unique key blocked duplicate detail rows, but the summary table inserted one row 'per run' — so it gained an extra row.

3. Why the Unique Key Didn't Stop the Summary

The detail table had UNIQUE(date, store_id); the summary table had nothing. It stored the time, row count, and total of each run, designed on the assumption that 'it runs once a day' — an assumption enforced by no constraint at all. We added UNIQUE(run_date).

4. Add a File Lock

One line of flock at the top of the script: if /var/lock/daily_recon.lock can't be acquired, exit immediately and log it. Even if someone pastes a duplicate again, the second process can't get in. The lock file lives in /var/lock and clears on reboot, so it can't deadlock the system.

5. Every Run Starts by Taking a run_id

Now the script's first act is inserting a row into a run_log table and taking the auto-increment id; every subsequent write carries that run_id. When something goes wrong, one query by id shows which steps ran, which tables were written, when it started and when it ended. If we'd had this that morning, there'd have been nothing to guess.

6. crontab Goes into Git

Before, the server's crontab was edited by anyone who ssh'd in and ran crontab -e. Now the repo holds a crontab file; changes go through the deploy script, and the copy on the server is read-only. One diff shows exactly which line appeared.

Those Two Sheets

The client's finance team deleted the duplicate row; nothing else was affected. We logged October 13 in an internal notebook under a four-word title: don't trust defaults.

Consulting · Free initial chat

Need advice for your scenario?

Tell us your business context and current systems, and we'll give an actionable approach.

Contact us

Mon–Fri 9:00–18:00 · We respond on business days within one business day.

WeChat (Business Only)
WeChat (Business Only)二维码

Scan and add our WeChat for consulting

Or fill out the requirements form