10

Our nightly cleanup job used to assume it ran exactly once and finished. Then a run overlapped with the next one, they both deleted the same temp files, and one crashed mid-way leaving a half-cleaned state. On the retry it did the wrong thing because it assumed the previous run had completed.

I now prompt specifically for idempotency and single-instance locking: a flock so two copies never overlap, and every operation written so running it 5 times equals running it once. Turns out "idempotent" is a word models nod at but don't implement unless you enumerate what it means.

How do you all handle the case where the lock holder is killed and leaves a stale lock? flock's fd-based locking mostly solves it for me but I'd love other approaches.

THE PROMPT
Write a maintenance script (`{LANG}`: bash or Python, your call, say why) meant to run from cron/systemd on a Linux host. It should {MAINTENANCE_TASK}.

It MUST be safe to run repeatedly and concurrently:
- Single-instance lock: use `flock` on a fixed fd/lockfile (bash) or `fcntl.flock` on an opened file (Python). If another instance holds the lock, exit 0 with a log line, do not queue or block forever. Stale locks must self-resolve (fd locks release on process death).
- Idempotency: every action is a no-op if already done. Deleting: ignore 'already gone'. Creating: 'create if not exists'. Moving: check destination first. Running N times must equal running once.
- Dry-run: `--dry-run` prints intended actions and touches nothing.
- Bounded work: cap how much it processes per run ({MAX_ITEMS}) so a backlog can't make one run take hours.
- Logging: structured single-line logs with timestamp + level to stderr, suitable for journald. Exit 0 success, non-zero only on real failure so cron alerts mean something.
- Time safety: never delete based on mtime younger than {MIN_AGE}; guard against clock skew.

End with a TEST PLAN: the exact commands to prove (a) two concurrent invocations don't collide, (b) running twice leaves the same end state, (c) --dry-run changes nothing.

3 Answers

5

For stale locks I moved everything to systemd and let it own the mutual exclusion instead of the script. A oneshot service plus a timer gives you RefuseManualStart=no and, crucially, systemd won't start a new run while the old unit is still active, so you get single-instance for free and journald logging without any flock code. The script stays dumb and idempotent, the init system handles overlap.

THE PROMPT
Alternative: instead of flock, deliver as a systemd oneshot .service + .timer. systemd guarantees a new run won't start while the unit is active. Provide both unit files and the idempotent script, no in-script locking needed.
6

One addition to the test plan: actually kill -9 the lock holder mid-run and confirm the next invocation acquires the lock. That's the scenario people assume works and never verify, and it's exactly where lockfile-based (as opposed to fd-based) schemes leave you stuck.

2

The mtime + clock-skew guard is the sneaky-important one. I've seen a 'delete files older than 7 days' job wipe fresh files after an NTP correction jumped the clock forward. Always compare against a monotonic-ish reference or add a sanity floor like your prompt does.

Your Answer