[#1012] Pin the commit of the post-import statistics refresh, and say why it is there - #1016
Conversation
…ics refresh, and say why it is there updateTableStatistics() has committed every statistics statement where it runs since 3800973, and nothing pinned it: the connection of an import goes back to the pool right after the refresh and CachedConnection.close() rolls back, so on postgres - the one engine that does not commit the statement itself - a refresh left inside the transaction of the borrow would be discarded by its own return. Measured: sql server keeps its UPDATE STATISTICS across a rollback with @@trancount still 1, mysql commits ANALYZE TABLE implicitly and dbms_stats commits of its own, while postgres 18.6 loses the 3 pg_statistic rows of the table and keeps only the reltuples that ANALYZE writes in place - after which the cursor-batch query of OpenIdentityPlatform#859 is estimated at 13 rows instead of 36, where 35 match. JDBCStatementBoundTestCase pins the order with no database at all - one execute() and one commit() per tree - and the postgres read-back of assertTableStatisticsFresh() now reads the per-column rows of pg_statistic instead of pg_class.reltuples, which vac_update_relstats() overwrites in place ("We violate transaction semantics here") and which therefore survives the rollback. Both were watched failing against con.commit() taken out: 45 tests of JDBCStatementBoundTestCase green, exactly the new one red under the mutation; 79 of PgSqlTestCase green, exactly testImportRefreshesTableStatistics red.
|
Not a defect as described — the refresh commits where it runs. Line numbers below are The four-step order in #1012 is the order of // JDBCStorage.java:2146
bounded(con, STATISTICS_TIMEOUT_PROPERTY, timeoutSeconds, cancelArmed, () -> { ... });
con.commit(); // :2163 - unconditional, one per tree
} catch (Exception e) {
try { con.rollback(); } ... // :2167 - the failure path only
allRefreshed=false;
logger.warn(...); // :2170 - and the operator is toldSo step 4 has nothing to discard: by the time Worth adding that the order is the opposite of a hazard: the data of the import is committed first ( The open question, answered — measured on each enginepostgres (
sql server (
So mysql — Postgres alone would have lost anything had the commit been missing — and precisely the part the feature exists for. What is broken: the commit is pinned by nothingWalking the same
What the green suite would be hiding, same 40 rows in both tables, the cursor-batch query of #859: 35 rows actually match. What this PR does with thatNothing of the behaviour — the three items above the fold are all that was left to do, and the diff is the first two plus a comment:
Both tests were watched failing against the commit taken out — the runs are in the description. If the preference is to close #1012 as not-a-defect and leave the hardening out, say so and I will drop this; the analysis above stands either way. |
Fixes #1012.
The issue as filed is not a defect
The four-step order in #1012 is the order of
ImporterImpl.close(), and the statistics are not one step of it:updateTableStatistics()issues one statement per tree and commits each of them on the spot (JDBCStorage.java:2163), rolling back only on the failure path and logging what failed. By the timerelease()reachescon.close()— which does roll back,CachedConnection.java:2211— every statistics statement of the import is already durable. The commit has been on that line since the feature's first commit, 3800973 (#866); #940 moves the call todescribing.conunder that connection's monitor and does not touch the method, so the same holds there.The full analysis, including the per-engine measurements that answer the question #1012 asks first, is in a comment below.
What changed
Nothing of the behaviour. What the issue did turn up is that the invariant is pinned nowhere, on any engine, and that the read-back of the suite could not have caught it on the one engine where it bites:
JDBCStatementBoundTestCase.testEveryStatisticsStatementIsCommittedWhereItRuns— two trees against a mock connection of the postgres dialect,InOrder:execute(),commit(),execute(),commit(), withtimes(2)on the commit andnever()on the rollback. Behind each statement rather than once at the end of the loop: a single commit there would leave every tree but the last of arebuild-indeximport inside the transaction of the borrow. Needs no database.TestCase.assertTableStatisticsFresh()reads the per-column rows ofpg_statisticon postgres instead ofpg_class.reltuples.reltuplesis overwritten in place —vac_update_relstats(): "We violate transaction semantics here" — so it survives the rollback of the connection's return and can pin nothing; thepg_statisticrows are ordinary catalog rows and do not survive it. Strictly stronger than what it replaces: the count is 0 both for a table that was never analyzed and for one whose refresh was rolled back, and the row still comes frompg_class, so a table that is not there is still reported as "not found" rather than as stale statistics.JDBCStorage.java:2163saying why the commit is there, the wayreadStoredComment()already says it for the stamp path. Reading the order ofclose()without it is what produced JDBC: post-import table statistics may be rolled back by the connection's return #1012.Verification
Mutation =
con.commit()taken out ofJDBCStorage.java:2163.JDBCStatementBoundTestCaseVerificationInOrderFailure: Wanted but not invoked: postgresConnection.commit()— and the other 44 stay greenPgSqlTestCasetestImportRefreshesTableStatisticsfails —statistics of opendj_8e0159e8... look stale: 0— where thereltuplesread it replaces passesMySqlTestCase,MsSqlTestCaseandOracleTestCaseare untouched by the change — the read-back it edits is the postgres branch of that helper — and were not run here: this machine has 8 GB and three other worktrees were building on it. The sql server behaviour the analysis reports was measured against the suite's own image with a hand probe instead.