The 5:00 pm message said our data-collection job had failed twice overnight (exit code 2, about 93 seconds each run) and had not recovered since. The job’s last run, at 3:51 pm, had finished cleanly.

The job collects our infrastructure data every six hours, and an agent checked it for me. The scheduled task showed Ready, with a last result of 0. The data was about 75 minutes old, which is normal for a six-hour job. So the alert was wrong about “not recovered.” It was also wrong about “overnight.” The failures had happened during the day: from 6:41 am to 12:41 pm the connection to our web server kept timing out, and at 3:48 pm one run failed outright. A retry three minutes later worked.

The fix that missed

In the 3:48 run, the SSH connections timed out and the push then hit a DNS lookup failure, so it gave up after 93 seconds. The agent’s hypothesis was that the PC briefly had no network. It said so plainly as a hypothesis, since nobody had captured the network state at 3:48. It also found a bug in the alert itself and offered two fixes: stop treating a total loss of network as a failure worth an alert, and make the alert check the latest run before saying “not recovered.”

We went ahead with both. The push step got its own exit code 6 for network errors, and the scheduling script logged that as “push skipped” instead of a failure. The targeted tests passed (138). The full fast suite ran 15,276 tests and one failed, a known problem from earlier commits that day. We committed and pushed.

Then the 5:00 pm message got traced. The ledger showed it came from the queue where deferred alerts wait to be flushed together. Two failures were sitting in that queue: one at 534 seconds and one at 93 seconds. The flush grouped them as a single series, used the latest headline, and added “(x2)”. The 3:51 pm success never got written into the queue. The flush already knew how to drop a failure that had a matching “recovered” entry, but nothing ever wrote one.

The agent’s own verdict on our fix was that it “fixed the wrong path.” The network change would not have stopped that message anyway, because the server connections had also failed in that run. The fix did work. The agent ran the real script from a scratch copy with stand-in steps, and it exited 0 for an unreachable endpoint and 2 for a hard error. It just wasn’t the thing that sent the message. I had spent a full test run and a push on it before checking the path the message actually took.

The fix that matched

When any scheduled job goes from failing back to green, it now writes a “recovered” entry into the alert queue under the same name as the failure. The flush then drops the whole series. It applies to every scheduled job, not only this one, and it writes only when a failure is actually waiting in the queue, so a healthy job adds nothing.

Four new tests cover it:

  • Fail, fail, recover leaves nothing to report.
  • Fail with no recovery still reports “(x2)”.
  • No write happens when nothing is queued.
  • A recovered job produces no finding.

The full suite came back with 15,293 passed and 2 failed, and neither failure came from this change. One was the known problem. The other was a job registry test that passes when run alone, in a file where another session had uncommitted edits.

What has and hasn’t been seen

The next morning the agent reported that nothing had paged me overnight. The 6:41 pm run had timed out, but it ran before the retry fix and was held as a first failure, so it never alerted. The 12:41 am run was clean at 141 seconds and was logged as recovered.

A second change, a single retry after a 30-second pause when the server connection stalls, never had to fire. Neither it nor the “recovered” queue entry has been seen on a real stall. Both have only been tested in scratch runs, not on a real run.

The 5:00 pm message was an old failure reported after the job had already recovered at 3:51 pm, and it stayed in the queue because nothing recorded the recovery. The first real fail-then-recover cycle will show whether the “recovered” entry empties the queue.