SERIALIZABLE can commit write skew after SSI summarization
Hi,
I found a reproducible case where SERIALIZABLE commits a non-serializable
write-skew schedule after SSI summarizes a committed transaction's SIREAD
locks into OldCommittedSxact.
I reproduced this on PostgreSQL 17.10, 18.4, and current master, although
it appears to date back to 9.1.
Setup:
CREATE TABLE t(id int PRIMARY KEY, v int);
INSERT INTO t VALUES (1, 0), (2, 0);
Let's say our application rule is that a transaction may set its row to 1
only if no row is already 1:
UPDATE t
SET v = 1
WHERE id = <n>
AND (SELECT count(*) FROM t WHERE v = 1) = 0;
In either serial order, the second transaction sees one row already set and
does not update, so the final count is 1.
Session 1:
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT count(*) FROM t WHERE v = 1;
Leave this transaction open.
Session 2:
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT count(*) FROM t WHERE v = 1;
UPDATE t
SET v = 1
WHERE id = 1
AND (SELECT count(*) FROM t WHERE v = 1) = 0;
COMMIT;
At this point session 2 is committed, but its SIREAD locks are still
retained because session 1 remains open.
Now run enough short serializable transactions to exhaust the
SERIALIZABLEXACT array and force summarization:
python3 - <<'PY' > /tmp/churn.sql
for i in range(1500):
print("BEGIN ISOLATION LEVEL SERIALIZABLE; SELECT 1; COMMIT;")
PY
psql -d test -f /tmp/churn.sql
The exact threshold depends on MaxBackends. On my stock max_connections =
100 cluster it was between 1400 and 1420 transactions.
MaxBackends = max_connections + autovacuum_worker_slots
+ max_worker_processes + max_wal_senders + 2
(NUM_SPECIAL_WORKER_PROCS)
max_serializable_xacts = (MaxBackends + max_prepared_transactions) * 10
The transfer of session 2's SIREAD locks to the summarization dummy is
visible in pg_locks:
SELECT pid, virtualtransaction, locktype, relation::regclass
FROM pg_locks
WHERE mode = 'SIReadLock'
AND relation = 't'::regclass;
Before summarization, the relevant locks have session 2's pid. Afterwards
they appear with:
pid | virtualtransaction
-----+-------------------
NULL | -1/0
Back in session 1:
UPDATE t
SET v = 1
WHERE id = 2
AND (SELECT count(*) FROM t WHERE v = 1) = 0;
COMMIT;
SELECT count(*) FROM t WHERE v = 1;
Observed after summarization:
count
-------
2
Both transactions commit with no serialization failure.
Controls:
- with no churn, session 1's UPDATE raises 40001;
- with sub-threshold churn, it also raises 40001;
- both serial orders produce a final count of 1;
- once session 2's SIREAD locks move to virtualtransaction = '-1/0',
session 1 commits and the final count is 2.
The control error is:
ERROR: could not serialize access due to read/write dependencies among
transactions
DETAIL: Reason code: Canceled on identification as a pivot, during write.
HINT: The transaction might succeed if retried.
Note - this appears to be a live variant of the DTEST_SUMMARIZE_SERIAL case
referenced here [0]/messages/by-id/787936C5-4155-4CF9-939D-39DC0EC1C892@yandex-team.ru. Starting a new thread since that was not that primary
point of the thread, I think.
[0]: /messages/by-id/787936C5-4155-4CF9-939D-39DC0EC1C892@yandex-team.ru
/messages/by-id/787936C5-4155-4CF9-939D-39DC0EC1C892@yandex-team.ru
Regards,
Jacob