The lock that doesn't wait

Twice an hour, on a large enough site, two cron jobs reach for the same folder at almost the same moment: the scheduled publish, building and deploying whatever post came due, and the sidebar refresh, rewriting a few of the same output files with fresh numbers. Most ticks miss each other by minutes. On an archive big enough that a build and a full deploy outlast the interval between them, some don't.

A deploy walking a tree that another run is still rewriting can serve a page half-written. A run pruning orphans can delete, as an orphan, a page the other run published a second earlier. Neither failure announces itself — the site just ends up quietly wrong.

The lock, and what it refuses to do

One file, .blog-sh.lock in the project root, stands between those two runs — and it makes a choice that looks wrong until you see the alternative. A run that finds the lock already held does not wait for it.

A cron tick that loses that race says so and leaves, exit 0. Cron doesn't need it to wait: the next tick is minutes away regardless, and the alternative — queuing behind the lock — has a worse failure mode than losing a turn. A pile of blocked runs all wake at once the moment the lock frees, and all try to do the same work together.

A run started by hand is held to a different promise. It reports the collision and exits non-zero, because the quiet alternative — succeeding at nothing while looking like it succeeded — would mean ./blog.sh told you a deploy happened when it didn't.

One operation, not three

Publishing on schedule is publish, rebuild and deploy back to back, and the lock is held for all of it at once rather than reacquired between steps — a rebuild starting the instant a publish ends is exactly the overlap the lock exists to prevent, just with itself as both sides. The build and the deploy it shells out to inherit that same lock instead of asking for their own, which is the difference between one run holding a door open for the parts of itself that come after, and that run deadlocking against its own child.

Where the choice doesn't even come up

Reordering the queue takes the same lock too, and it's the one place the pattern above breaks — worth naming rather than skating past. It never touches public.nosync; it writes the same post files a scheduled publish would read from, and it holds the lock for the instant of the move alone, the checks and the writes together and nothing longer, never while a prompt sits open waiting for you to pick a slot. There's no cron-versus-human choice to make here, because there's no interval to lose in the first place: an operation over before the next tick could possibly land doesn't need to decide between waiting and bailing. A queue screen left open overnight has never once kept a cron tick out — not because it was built to be polite about it, but because it was never open long enough to be in the way.

Where a filesystem can't do advisory locks at all — some network mounts — the lock quietly degrades to no lock, which is exactly where every installation stood before this existed.

Comments