Description
PostgreSQL aborts the whole transaction on any server error: every following command in it fails, and COMMIT performs a rollback instead of committing, reporting success.
The server knows about this and keeps a per-session mark for it: while a transaction is marked as broken, cleaning up temporary data is skipped and the commit fails loudly instead of silently discarding the work. But the mark is only set for the queries the session builds itself. Statements executed outside that route — DDL issued by service operations and by the structure synchronization at startup, batch inserts, and raw SQL from INTERNAL DB / runSQL — leave the transaction unmarked when they fail.
The result for an application developer: an operation fails somewhere inside, the failure is logged, execution continues, everything afterwards fails too (current transaction is aborted), and the apply ends without an error even though nothing was written.
apply () {
NEWSESSION {
// any raw statement that the server rejects - a wrong column, a missing table
INTERNAL DB <{ ... }>; // fails, logged, execution goes on
someProperty() <- 1; // fails with "current transaction is aborted"
APPLY; // reports success - and commits nothing
}
}
The same happens with the service operations that drop columns or synchronize the structure: the per-item error is logged, the loop keeps going, and the final commit reports success without having committed anything.
Reason
A failure whose result is thrown away is bad enough; a failure that is reported as success loses data with no trace. The mark exists exactly to prevent that, and the statements most likely to fail against a live database — schema changes and raw application SQL — are the ones that never set it.
Two things make the fix non-trivial, and both need to be part of it:
- Not every failure carrying an SQL state comes from the server. The driver produces
08006 and 08003 on its own when the socket breaks or the connection is already closed, and a statement that succeeded can still fail while being closed. Marking those would be wrong in the opposite direction: the transaction is alive, and the mark would skip cleaning up a temporary table and put it back to be reused with its rows still in it. The state of the transaction has to be taken from the connection, not guessed from the error.
- A failure inside a transaction is not always final. The structure synchronization deliberately takes a savepoint before dropping a data column and rolls back to it when the drop fails, because that failure is expected. After such a rollback the transaction is healthy again and the mark has to be lifted, the way it already is for the internal retry path — otherwise the server would stop starting up on a migration.
Fix
A statement that the server rejects inside a transaction marks that transaction as broken, whichever way the statement was executed. Rolling back to a savepoint lifts the mark. The commit of a broken transaction keeps failing loudly instead of silently rolling back.
Description
PostgreSQL aborts the whole transaction on any server error: every following command in it fails, and
COMMITperforms a rollback instead of committing, reporting success.The server knows about this and keeps a per-session mark for it: while a transaction is marked as broken, cleaning up temporary data is skipped and the commit fails loudly instead of silently discarding the work. But the mark is only set for the queries the session builds itself. Statements executed outside that route — DDL issued by service operations and by the structure synchronization at startup, batch inserts, and raw SQL from
INTERNAL DB/runSQL— leave the transaction unmarked when they fail.The result for an application developer: an operation fails somewhere inside, the failure is logged, execution continues, everything afterwards fails too (
current transaction is aborted), and the apply ends without an error even though nothing was written.The same happens with the service operations that drop columns or synchronize the structure: the per-item error is logged, the loop keeps going, and the final commit reports success without having committed anything.
Reason
A failure whose result is thrown away is bad enough; a failure that is reported as success loses data with no trace. The mark exists exactly to prevent that, and the statements most likely to fail against a live database — schema changes and raw application SQL — are the ones that never set it.
Two things make the fix non-trivial, and both need to be part of it:
08006and08003on its own when the socket breaks or the connection is already closed, and a statement that succeeded can still fail while being closed. Marking those would be wrong in the opposite direction: the transaction is alive, and the mark would skip cleaning up a temporary table and put it back to be reused with its rows still in it. The state of the transaction has to be taken from the connection, not guessed from the error.Fix
A statement that the server rejects inside a transaction marks that transaction as broken, whichever way the statement was executed. Rolling back to a savepoint lifts the mark. The commit of a broken transaction keeps failing loudly instead of silently rolling back.