Skip to content

[#933] Read a catalog table another session created while this one was creating it - #1005

Open
vharseko wants to merge 1 commit into
OpenIdentityPlatform:masterfrom
vharseko:issues/933-catalog-adopted-table
Open

[#933] Read a catalog table another session created while this one was creating it#1005
vharseko wants to merge 1 commit into
OpenIdentityPlatform:masterfrom
vharseko:issues/933-catalog-adopted-table

Conversation

@vharseko

@vharseko vharseko commented Sep 9, 2026

Copy link
Copy Markdown
Member

Fixes #933

The problem

openCatalog() makes the catalog of a backend usable once per open of the storage: it reads what the catalog records where the table is there, and creates the table where it is not. The lock it runs under orders the transactions of one storage and nothing else, which is why createCatalogTable() tolerates a table that turned up while it was being made — an offline tool beside a running server is a pair no lock of one process can order.

On that tolerated path the method returned without reading a row, and openCatalog() went on to raise catalogTableOpened over an empty enrolledTrees. The table is there and full of rows; this storage believes it records nothing.

enrolInCatalog() skips a tree the memo already names and writes it otherwise, so every tree of that open is written again — about 25 upserts and 25 commits on the catalog connection for a stock suffix, each a row that was already there, and the same for every later write of that open which names a tree. Not a correctness bug: the upsert is idempotent, the rows are the same rows, and the commits are on the catalog's own connection, so nothing of the caller's transaction is committed and partlyCommitted is not raised. It defeats the optimisation that exists to make an open of a complete catalog write nothing at all.

The change

createCatalogTable() answers whether it created the table or adopted one that was already there, and openCatalog() reads the rows on the second answer — the reading branch is exactly what such a table is for. Where the create really did create it, there is still nothing to read.

The read runs on the session the failed create reset, which is a connection rolled back or, where even the rollback failed, one given up for the read to establish again: either is a session a select may be asked of. A select that fails there is thrown, exactly as it is thrown on the branch that reads a table which was there from the start — one failure, one answer.

The test

testAnOpenThatAdoptsTheCatalogTableOfAnotherSessionEnrolsNothingAgain, in the shared jdbc suite, so every engine runs it. AdoptingStorage answers once that the catalog table is not there while it is — the state of a session whose lookup ran a moment before another session's create table committed — and counts the connections its catalog is read and written on.

What tells a storage that read the table from one that only wrote its way through the trees of that write is the tree asked for afterwards: one the catalog already names and the adopting write never opened. The catalog of a transaction is opened at the first row that transaction has to write, so a write whose trees the catalog already names opens no connection at all.

Without the fix it fails with expected [1] but found [2] — the second write had to open a connection to write a row that is already there.

Verification

Run Result
the new case, before the fix fails: expected [1] but found [2]
the new case, after it passes
PgSqlTestCase 80/80, 0 skipped
MySqlTestCase 79/79, 0 skipped
CachedConnectionTestCase, CatalogConnectionTestCase, JDBCStatementBoundTestCase, JDBCStorageRetryTest, StampConnectionTestCase 228/228

The mssql and oracle suites were not run locally; the change carries no dialect of its own.

…ed while this one was creating it

createCatalogTable() answers whether it created the table or found it already
there, and openCatalog() reads the rows on the second answer instead of raising
catalogTableOpened over an empty enrolledTrees. A storage that adopted a table
without reading it enrols every tree of that open again - one upsert and one
commit each on the catalog connection, against rows that are already there - and
goes on doing it for every later write of that open which names a tree.

Fixes OpenIdentityPlatform#933
@vharseko
vharseko requested a review from maximthomas September 9, 2026 17:12
@vharseko vharseko added jdbc concurrency Thread-safety / race-condition bugs tests Test suites: fixing, enabling, un-disabling performance Performance / concurrency / lock-contention work enhancement labels Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

concurrency Thread-safety / race-condition bugs enhancement jdbc performance Performance / concurrency / lock-contention work tests Test suites: fixing, enabling, un-disabling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JDBC backend: a catalog table another session created leaves enrolledTrees empty, so that open enrols every tree again

1 participant