fix: no longer retries the transaction on 40001 errors

This commit is contained in:
Laurence Isla
2026-04-27 14:30:10 -05:00
parent bf758698b3
commit 41b86fffa5
8 changed files with 46 additions and 6 deletions
+1
View File
@@ -19,6 +19,7 @@ All notable changes to this project will be documented in this file. From versio
- Shutdown should wait for in flight requests by @mkleczek in #4702 - Shutdown should wait for in flight requests by @mkleczek in #4702
- Fix login with uppercase and mixed case role names by @taimoorzaeem in #4678 - Fix login with uppercase and mixed case role names by @taimoorzaeem in #4678
- Remove automatic transaction retries on `40001 (serialization_failure)` errors to prevent replication lag by @laurence in #3673
### Changed ### Changed
+2 -1
View File
@@ -105,7 +105,8 @@ let
log "Starting replica on $replica_host" log "Starting replica on $replica_host"
pg_ctl -D "$replica_dir" -l "$replica_dblog" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $replica_host -c log_statement=\"all\" " \ # We set a low max_standby_streaming_delay to make the replication conflict fail faster in tests (otherwise it waits for the default 30s)
pg_ctl -D "$replica_dir" -l "$replica_dblog" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $replica_host -c log_statement=\"all\" -c max_standby_streaming_delay=\"3s\" " \
>> "$setuplog" >> "$setuplog"
>&2 echo "${commandName}: Replica enabled. You can connect to it with: psql 'postgres:///$PGDATABASE?host=$replica_host' -U postgres" >&2 echo "${commandName}: Replica enabled. You can connect to it with: psql 'postgres:///$PGDATABASE?host=$replica_host' -U postgres"
+1 -1
View File
@@ -343,7 +343,7 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
qSchemaCache = do qSchemaCache = do
conf@AppConfig{..} <- getConfig appState conf@AppConfig{..} <- getConfig appState
(resultTime, result) <- (resultTime, result) <-
timeItT $ usePool appState (SQL.transaction SQL.ReadCommitted SQL.Read $ querySchemaCache conf) timeItT $ usePool appState (SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
case result of case result of
Left e -> do Left e -> do
markSchemaCachePending appState markSchemaCachePending appState
+1 -1
View File
@@ -62,7 +62,7 @@ dumpSchema :: AppState -> IO LBS.ByteString
dumpSchema appState = do dumpSchema appState = do
conf@AppConfig{..} <- AppState.getConfig appState conf@AppConfig{..} <- AppState.getConfig appState
result <- result <-
AppState.usePool appState (SQL.transaction SQL.ReadCommitted SQL.Read $ querySchemaCache conf) AppState.usePool appState (SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
case result of case result of
Left e -> do Left e -> do
let observer = AppState.getObserver appState let observer = AppState.getObserver appState
+2 -2
View File
@@ -94,7 +94,7 @@ pgVersionStatement = SQL.Statement sql HE.noParams versionRow
-- A setting on the database only will have no effect: ALTER DATABASE postgres SET <prefix>jwt_aud = 'xx' -- A setting on the database only will have no effect: ALTER DATABASE postgres SET <prefix>jwt_aud = 'xx'
queryDbSettings :: Maybe Text -> Session [(Text, Text)] queryDbSettings :: Maybe Text -> Session [(Text, Text)]
queryDbSettings preConfFunc = queryDbSettings preConfFunc =
SQL.transaction SQL.ReadCommitted SQL.Read $ SQL.statement dbSettingsNames $ SQL.Statement sql (arrayParam HE.text) decodeSettings True SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ SQL.statement dbSettingsNames $ SQL.Statement sql (arrayParam HE.text) decodeSettings True
where where
sql = encodeUtf8 [trimming| sql = encodeUtf8 [trimming|
WITH WITH
@@ -134,7 +134,7 @@ queryDbSettings preConfFunc =
queryRoleSettings :: PgVersion -> Session (RoleSettings, RoleIsolationLvl) queryRoleSettings :: PgVersion -> Session (RoleSettings, RoleIsolationLvl)
queryRoleSettings pgVer = queryRoleSettings pgVer =
SQL.transaction SQL.ReadCommitted SQL.Read $ SQL.statement mempty $ SQL.Statement sql HE.noParams (processRows <$> rows) True SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ SQL.statement mempty $ SQL.Statement sql HE.noParams (processRows <$> rows) True
where where
sql = encodeUtf8 [trimming| sql = encodeUtf8 [trimming|
with with
+1 -1
View File
@@ -96,7 +96,7 @@ data ResultSet
mainTx :: MainQuery -> AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> MainTx mainTx :: MainQuery -> AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> MainTx
mainTx _ _ _ _ (NoDb x) _ = NoDbTx $ NoDbResult x mainTx _ _ _ _ (NoDb x) _ = NoDbTx $ NoDbResult x
mainTx genQ@MainQuery{..} conf@AppConfig{..} AuthResult{..} apiReq (Db plan) sCache = mainTx genQ@MainQuery{..} conf@AppConfig{..} AuthResult{..} apiReq (Db plan) sCache =
DbTx isoLvl txMode dbHandler SQL.transaction DbTx isoLvl txMode dbHandler SQL.transactionNoRetry
where where
isoLvl = planIsoLvl conf authRole plan isoLvl = planIsoLvl conf authRole plan
txMode = planTxMode plan txMode = planTxMode plan
+4
View File
@@ -10,6 +10,10 @@ $$ language sql;
create table replica.items as select x as id from generate_series(1, 10) x; create table replica.items as select x as id from generate_series(1, 10) x;
create table replica.conflict as select x as id from generate_series(1, 1000000) x;
create view replica.conflict_view as select * from replica.conflict where (pg_sleep(0.01) is not null);
DROP ROLE IF EXISTS postgrest_test_anonymous; DROP ROLE IF EXISTS postgrest_test_anonymous;
CREATE ROLE postgrest_test_anonymous; CREATE ROLE postgrest_test_anonymous;
+34
View File
@@ -1,6 +1,10 @@
"IO tests for PostgREST started on replicas" "IO tests for PostgREST started on replicas"
import os
import time
from postgrest import run from postgrest import run
from util import Thread
def test_sanity_replica(replicaenv): def test_sanity_replica(replicaenv):
@@ -22,3 +26,33 @@ def test_sanity_replica(replicaenv):
response = postgrest.session.get("/items?select=count") response = postgrest.session.get("/items?select=count")
assert response.text == '[{"count":10}]' assert response.text == '[{"count":10}]'
def test_conflict_replica(replicaenv):
"Test that PostgREST does not retry the transaction on conflict with recovery (PG error code 40001)"
with run(env=replicaenv["replica"]) as postgrest:
def conflict():
response = postgrest.session.get("/conflict_view")
# Checks that the transaction stops and returns the 40001 error instead of retrying
assert response.json()["code"] == "40001"
assert response.status_code == 500
t = Thread(target=conflict)
t.start()
# make sure the request has started
time.sleep(0.1)
prienv = replicaenv["primary"]
connopts = f'-d {prienv["PGDATABASE"]} -U postgres -h {prienv["PGHOST"]}'
# Delete the table data while the request with the lock is running to trigger the recovery conflict
os.system(
f'psql {connopts} --set ON_ERROR_STOP=1 -a -c "DELETE FROM replica.conflict;"'
)
# Vacuum the table to accelerate the process
os.system(f"vacuumdb {connopts} -t replica.conflict")
t.join()