SERIALIZABLE can commit write skew after SSI summarization

Started by Jacob Brazeal2 days ago1 messageshackers
Jump to latest
#1Jacob Brazeal
jacob.brazeal@gmail.com

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