Survive losing the connection to the results database - #300
Merged
Conversation
A run opens its connection, claims its libraries, and then tests for hours before writing anything: the results of every model go into one transaction at the very end, after the reports have been generated. The connection spends that whole time idle, which is exactly the connection a firewall, a NAT or a restarted server drops. When that happened the first INSERT of the run threw OperationalError, test.py died, and a night of testing was lost - and nothing looked wrong until then, because the heartbeat that holds the claims opens a connection of its own every minute. The connection now asks for TCP keepalives, so an idle one is kept alive and a dead one is noticed. If it is lost anyway, the statement that discovers it reconnects, replays the statements the transaction had so far and carries on. Replaying is safe because everything a run writes is an insert guarded by a unique key: it can only produce the rows that were lost, never a duplicate. Queries are not remembered, so the tens of thousands a run makes do not fill the buffer. Checked against the real thing: with a run's transaction half written, killing its backend with pg_terminate_backend loses nothing - the run reconnects, replays its 250 statements, and all 500 rows are in the database, with the connection usable afterwards. This makes losing the connection survivable; it does not make a run's results arrive any earlier. Writing them per library, as each finishes, would also stop a crashed run from losing the libraries it had already tested, and is the better fix - it needs the results to be gathered per library rather than at the end, so it is worth doing on its own. --- Generated by Claude Code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A run opens its connection to the results database, claims its libraries, and then tests for hours before writing anything: every model's results go into a single transaction at the very end, after the reports have been generated and uploaded.
That leaves the connection idle for the whole run — and an idle connection is exactly what a firewall, a NAT or a restarted server drops. When that happened, the first
INSERTat the end threwtest.pydied, and a night of testing was lost. Nothing looked wrong until that moment either: the heartbeat that holds the job claims opens a connection of its own every minute, so the claims kept being refreshed while the connection that mattered was already dead.What this does
keepalives=1 keepalives_idle=60 …), so an idle connection is kept alive and a genuinely dead one is noticed early rather than at the end.Replaying is safe because of what #295 already put in place: everything a run writes is an insert guarded by a unique key —
(date, libname, model)for a branch table — and the inserts useON CONFLICT DO NOTHING. Replaying can only produce the rows that were lost, never a duplicate. Queries are not remembered, so the tens of thousands a run makes do not fill the buffer.Nothing in
test.pychanges; the recovery lives inresultsdb.pyand the sqlite3 backend is untouched.How it was checked
Against a real PostgreSQL, with a run's transaction half written, killing its backend the way a firewall would:
Also, that the paths this must not disturb still work: a full
configs/sanityCheck.jsonrun on sqlite3 with the CI assertion passing, and a run against PostgreSQL with two FMI simulators fillingv1.27-fmiandv1.27-fmi-fmpyand releasing its claim.What this is not
It makes losing the connection survivable; it does not make the results arrive any earlier. A run that is killed still loses everything it has tested, because it has written nothing yet.
Writing the results per library, as each one finishes — in the same transaction that marks that library's claim done — would fix that properly: a lost connection or a crash would then cost one library instead of a night, every library in the database would be whole rather than half-tested, and a rerun would resume where the last one stopped. It needs the results to be gathered per library rather than accumulated to the end, so it is worth doing as its own change.
Generated by Claude Code.