A library test run writes nothing to the database until it has finished. Every model's result is kept in memory and inserted in a single transaction at the very end, after the reports have been generated and uploaded:
# test.py, after everything else
db.commit()
A master run takes days. Anything that ends it before that last commit - a reboot, a killed job, an omc crash that takes the process with it, an out-of-disk - throws away every model tested so far. #300 makes a lost database connection survivable, but it does not help with any of the others, because there is still nothing written to survive with.
Write each model as it finishes
test.py already runs testmodel.py per model and reads back files/<name>.stat.json. The result row can be written when that result is collected, so a run that dies has cost one model rather than the week. Roughly 19500 small transactions instead of one large one, which is nothing against ~19 seconds of work per model.
Resuming
A resumed run must adopt the date of the run it continues, not start a new one. Then skipping what is already stored needs no new schema at all:
SELECT model FROM "master-fmi" WHERE date = ? AND libname = ?
and the unique key (date, libname, model) from #295 makes a re-inserted model a no-op, so a resume can be sloppy without ever duplicating a row.
What invalidates partial data
Partial results may only be reused when the inputs that produced them are unchanged, and the granularity differs:
- the compiler changed - discard the whole partial run:
omcversion is per run, so every row in it came from a different omc;
- a library was bumped - discard only that library's rows:
libversion and confighash are per (run, library), so a new Buildings says nothing about the Modelica models already tested in the same run.
The test for this is the one the run already makes before testing a library:
SELECT ... FROM libversion NATURAL JOIN omcversion
WHERE libversion=? AND libname=? AND branch=? AND omcversion=? AND confighash=?
confighash covering the configuration and the reference files means a changed reference file invalidates too, which is right.
Discarding should delete the rows. An abandoned run is invisible to the reports, so its rows would otherwise accumulate silently, and nothing existing cleans them up - clean-empty-omcversion-dates.py does the opposite, dropping run rows that have no results.
Knowing whether a run finished
This is the part that needs a new record, and it is also what
#301 asks for:
CREATE TABLE run (
branch text NOT NULL,
date bigint NOT NULL,
host text, -- which machine, see #301
omcversion text,
started_at timestamptz NOT NULL,
finished_at timestamptz, -- NULL: still running, or died
PRIMARY KEY (branch, date)
);
The run inserts its row at the start and sets finished_at at the end. A resume looks for the newest run of this branch, with the same compiler, whose finished_at is NULL.
The dangerous part: the reports
Every place that picks "the latest run" reads it straight out of a branch table:
# report.py
cursor.execute("SELECT date FROM %s WHERE libname=? ORDER BY date DESC LIMIT 1" ...)
With partial data that silently selects a half-written run, and a library showing 200 of its 3000 models looks like a catastrophic regression - with the emails that go with it. Every such query has to require a finished run: report.py, all-reports.py, all-plots.py, single-model.py.
This is most of the work in this change, and getting it wrong is worse than not doing the change at all, so it wants testing against real data before it goes anywhere near the nightly jobs.
Smaller things to decide
- The job claim stays per library, since it is about two machines not doing the same work. But a resumed run has to be able to re-claim the libraries it had partly done, or the machine that died holds them until its heartbeat goes stale, 30 minutes by default.
libversion is written per library at the end of a run today. It has to move to when that library's first model is stored, otherwise a resume cannot tell which library version the partial rows belong to.
- A resumed run spans more than one day. The rows of one
date may have been produced over two days; started_at and finished_at make that visible rather than implied.
Context: the database is the shared PostgreSQL one from #295; #300 is the narrower fix for losing the connection; #301 wants the machine recorded per run, which the run table above would carry.
Generated by Claude Code.
A library test run writes nothing to the database until it has finished. Every model's result is kept in memory and inserted in a single transaction at the very end, after the reports have been generated and uploaded:
A
masterrun takes days. Anything that ends it before that last commit - a reboot, a killed job, an omc crash that takes the process with it, an out-of-disk - throws away every model tested so far. #300 makes a lost database connection survivable, but it does not help with any of the others, because there is still nothing written to survive with.Write each model as it finishes
test.pyalready runstestmodel.pyper model and reads backfiles/<name>.stat.json. The result row can be written when that result is collected, so a run that dies has cost one model rather than the week. Roughly 19500 small transactions instead of one large one, which is nothing against ~19 seconds of work per model.Resuming
A resumed run must adopt the date of the run it continues, not start a new one. Then skipping what is already stored needs no new schema at all:
and the unique key
(date, libname, model)from #295 makes a re-inserted model a no-op, so a resume can be sloppy without ever duplicating a row.What invalidates partial data
Partial results may only be reused when the inputs that produced them are unchanged, and the granularity differs:
omcversionis per run, so every row in it came from a different omc;libversionandconfighashare per (run, library), so a new Buildings says nothing about the Modelica models already tested in the same run.The test for this is the one the run already makes before testing a library:
confighashcovering the configuration and the reference files means a changed reference file invalidates too, which is right.Discarding should delete the rows. An abandoned run is invisible to the reports, so its rows would otherwise accumulate silently, and nothing existing cleans them up -
clean-empty-omcversion-dates.pydoes the opposite, dropping run rows that have no results.Knowing whether a run finished
This is the part that needs a new record, and it is also what
#301 asks for:
The run inserts its row at the start and sets
finished_atat the end. A resume looks for the newest run of this branch, with the same compiler, whosefinished_atis NULL.The dangerous part: the reports
Every place that picks "the latest run" reads it straight out of a branch table:
With partial data that silently selects a half-written run, and a library showing 200 of its 3000 models looks like a catastrophic regression - with the emails that go with it. Every such query has to require a finished run:
report.py,all-reports.py,all-plots.py,single-model.py.This is most of the work in this change, and getting it wrong is worse than not doing the change at all, so it wants testing against real data before it goes anywhere near the nightly jobs.
Smaller things to decide
libversionis written per library at the end of a run today. It has to move to when that library's first model is stored, otherwise a resume cannot tell which library version the partial rows belong to.datemay have been produced over two days;started_atandfinished_atmake that visible rather than implied.Context: the database is the shared PostgreSQL one from #295; #300 is the narrower fix for losing the connection; #301 wants the machine recorded per run, which the
runtable above would carry.Generated by Claude Code.