The Bugs That Only Happen Sometimes
The hardest failures are the ones that heal the moment you look at them, because they were never really about the code. They were about time.
Every engineer knows how this goes. A bug report comes in. You try it on your machine and it works fine. You run it again and it's still fine. Then you add one line of logging so you can watch what happens, and after that it works every single time, forever, like it heard you coming up the stairs. Most people put this down to bad luck or a flaky report or some gremlin in the wiring. It's none of that. A bug that disappears the second you look at it is actually telling you something quite exact. The failure was living in the gap between two events, and watching it - the log line, the breakpoint, the run that was a touch slower because you were paying attention - moved one of those events along just enough to close the gap up. So these bugs aren't random at all. They're bugs about time.
The word people reach for is flaky, and that word is really just giving up while pretending to be a diagnosis. A flaky bug is a bit of determinism you haven't found yet. It looks like chance because the thing that settles it, which is the exact order two events happen to land in, is invisible, and it shuffles around from one run to the next. Back in 1985 a computer scientist called Jim Gray drew a line between two kinds of bug and the names stuck. A Bohrbug is solid, like the old picture of the atom - give it the same inputs and it shows up every time, which at least means you can chase it down and kill it. A Heisenbug is the other sort. It moves when you measure it, because the measuring disturbs the very timing that caused it in the first place. "Sometimes" was never a frequency. It's a race you keep almost, but not quite, winning.
A race is about the easiest thing there is to describe and one of the hardest to actually see. Two things happen at the same time, both go for the same thing, and which one gets there first decides the outcome, except nobody decided it. Think of two waiters in a busy restaurant. Both look over at the last free table, both see it's empty, and both walk off to the kitchen to put in an order for it, because neither of them marked it taken before they turned around. The table is the shared thing. That walk to the kitchen is the window - the stretch of time where the truth has already changed and nobody's written it down yet. In software the table is just a number sitting in a database. Two requests both read a balance and both see a hundred pounds, both take thirty off, and the account ends up at seventy instead of forty, because each one assumed nothing had moved between reading the number and writing it back. The bug isn't in either line of code. It's in the assumption that the world sat still while you were thinking about it. Every race has a window like this, and the smaller the window gets the rarer the bug is, and the crueller, because you can almost never catch it.
Now make that window a few milliseconds wide and put real people on the far end of it. On the fourteenth of August 2003 a race condition in the control-room software at one American utility let two processes reach for the same piece of data at the same instant, through a coding flaw that hardly ever lined up. It didn't crash anything. What it did was quietly stall the alarm system in an Ohio control room, so the screens carried on looking calm while the grid outside was falling apart. The operators trusted the calm screens and waved off the phone calls that were trying to tell them otherwise. The software didn't cause the blackout that came next - that had several causes, one of them power lines sagging down into trees that hadn't been trimmed - but it did blind the people who might otherwise have caught the thing early, and by the end of the night somewhere around fifty million people across eight states and Ontario had lost power. A window a few thousandths of a second wide, and the lights went out from Ohio up into Ontario.
This isn't a new lesson and it's been paid for in worse than electricity. In the mid-1980s a radiation therapy machine called the Therac-25 gave several patients massive overdoses, and at least three of them died. One of the main causes was a race condition. If an operator typed in the treatment settings and then went back and corrected them within about eight seconds, two parts of the software fell out of step and the machine fired its full beam without the shield that was supposed to be in place. The older models had a physical piece of hardware that just stopped this from happening. On the Therac-25 that check had been moved into software, where the race could get at it. The engineers who went through the disaster afterwards were clear that no single bad line and no careless operator was the cause. It came out of a whole way of building things that trusted timing it had no business trusting.
You can't make concurrency rare. Two things are always going to happen at once, and that's just what a running system is. What you can do is stop depending on the order they happen in. There are three moves for that. The first is making an operation safe to repeat. If doing something twice comes out exactly the same as doing it once, then a retry or a double-tap or a network hiccup that quietly resends the request can't hurt you any more. That's why a careful payment system stamps each request with a unique key and remembers it, so that when a second copy turns up carrying the same key it gets handed the first result instead of charging the card all over again. The second move is making the dangerous update atomic. You take "read the number, then write the new one" and collapse it into one indivisible step that nothing can slip inside, so there's no window left to lose. Either the whole thing happens or none of it does, and nobody ever catches it half done. The third move is to pull the decision apart from the act. Decide once, write the decision down, and then carry it out from the record, so that a check you made in one moment can't go and fire off a fresh action in a later one, after the world has already moved on.
That third one is the subtle move, because it's a whole family of bugs all wearing the same face. Every check is a photograph of a moment that has already gone past by the time you act on it. "Is this seat free? Yes. Book it." In between the question and the booking, someone on another continent booked it. You can't fix this by checking harder, or by checking a second time, because the second photograph goes stale exactly as fast as the first one did. The only cure is to make the booking itself be the thing that checks, in one indivisible motion, so the decision and the act turn into the same event with no gap in the middle for the world to change in. Two people claiming the same username, two withdrawals against the same money, two parcels shipped against one order - it's all the same shape. A promise made on the strength of a fact that stopped being true while you were turning around.
So when a bug only happens sometimes, listen to what it's actually telling you. It isn't bad luck. It's a spot where you assumed one thing had finished before another started, and reality wouldn't promise you that. The engineers who sleep well aren't the ones watching their systems the hardest. They're the ones who built systems that don't care what order things turn up in. A request you can safely repeat doesn't need watching. Neither does an update that nobody can catch half-done, or a decision you made once and wrote down. None of that wants a person standing over it with a stopwatch. You can't make concurrency rare. Make it boring instead, and then it stops mattering.
In the manifesto, this is tenets (IX), (X) and (XI).
Sources
- [Coffman et al. 1971] E. G. Coffman, M. J. Elphick & A. Shoshani, "System Deadlocks". ACM Computing Surveys 3(2), 1971. https://dl.acm.org/doi/10.1145/356586.356588. The four deadlock conditions; consistent lock order breaks circular wait; tenet IX.
- [Gray 1985] Jim Gray, "Why Do Computers Stop and What Can Be Done About It?". Tandem Technical Report 85.7, 1985. https://www.cs.cmu.edu/~garlan/17811/Readings/gray85-easy.pdf. Bohrbugs reproduce on every run; Heisenbugs hide under timing and retries; tenets IX, X.
- [Leach 2017] Brandur Leach, "Designing robust and predictable APIs with idempotency". Stripe blog, 2017. https://stripe.com/blog/idempotency. The idempotency key, returning the original result on retry; tenet X.
- [Northeast Blackout 2003] US-Canada Power System Outage Task Force, "Final Report on the August 14, 2003 Blackout in the United States and Canada: Causes and Recommendations". 2004. https://www.energy.gov/sites/prod/files/oeprod/DocumentsandMedia/BlackoutFinal-Web.pdf. A race condition in GE's XA/21 alarm system stalled the control room for over an hour; tenet IX.
- [Peirce 1870] Benjamin Peirce, "Linear Associative Algebra" (coined 'idempotent'). 1870. https://en.wikipedia.org/wiki/Idempotence. The mathematical origin of 'idempotent'; tenets X, IX, VI, XI, XII, XIX, XXI, XXII, VIII.
- [Therac-25 1985] Nancy Leveson & Clark Turner, "An Investigation of the Therac-25 Accidents". IEEE Computer 26(7), 1993. https://doi.org/10.1109/MC.1993.274940. A timing-dependent race in radiation-therapy software overdosed patients; tenet IX.
One of a series of field notes on building software for the way minds actually work: tired, distractible, ordinary, and now partly machine. They all lead back to the manifesto behind them, The Shape of the System.