architecture/design/wait-on-conflict-functional-spec.md
This document aims to define fail-on-conflict concurrency control behavior in YSQL, wait-on-conflict behavior in PostgreSQL, and outline the requirements for changing YSQL's default concurrency-control semantics.
To understand wait-on-conflict concurrency control in PostgreSQL, it is important to take note of some points about row-level locking and its interaction with DMLs.
FOR UPDATE - takes exclusive lock on the whole row (blocks the shared locks as well)FOR NO KEY UPDATE - takes exclusive lock on row but allows a shared lock on primary key i.e., FOR KEY SHAREFOR SHARE - shared lock on full row i.e., blocks all exclusive locksFOR KEY SHARE - shared lock on key i.e., allows FOR NO KEY UPDATEFOR UPDATE: doesn’t allow any concurrent modification to the locked tuple.FOR NO KEY UPDATE: doesn’t allow any concurrent modification to the locked tuple.FOR SHARE: doesn’t allow any concurrent modification to the locked tuple.FOR KEY SHARE: allow only modification to non-primary key colsLock-modification conflict only happens when the period from lock acquire to lock release intersects/overlaps with the read to commit time of the transaction performing the modification.
Lock-modification conflicts result in serialization errors. Note that the lock might be issued before or after the modification (see example 2i and 2ii).
Note that in READ COMMITTED isolation, when a lock-modification conflict occurs, PostgreSQL does some post-processing (called “READ COMMITTED update checking steps”) instead of throwing a 40001.
Lock-lock conflict: this conflict only refers to one lock blocking another i.e., just waiting behaviour. It doesn’t refer to a serialization error. In other words, two conflicting locks can only result in blocking and not in a serialization error (or “write” conflict as we roughly call it). Also note that these locks can be taken explicitly using a SELECT FOR or implicitly using a DML. So there can be 4 cases - implicit-implicit, implicit-explicit, explicit-implicit, explicit-explicit lock-lock conflicts.
(see example 1 for explicit lock-explicit lock conflict).
DMLs implicitly take the following row-level locks:
DELETE -> FOR UPDATEUPDATE -> FOR UPDATE if updating a column which has a unique index on it (one that can be used in a foreign key)UPDATEs other than those in point 2 -> FOR NO KEY UPDATE.In other words, a DML operation consists of locking and modification of a tuple.
Given that writing = locking + modification, the definition of lock-modification conflict is more generic to think of for serialization errors (the usual write-write “conflict” we talk about is just a special case of this. A write-write conflict = implicit lock - modification conflict).
YSQL does not differentiate between a lock-lock conflict and a lock-modification conflict. In either case, such conflicts will be detected during conflict resolution. Conflict resolution in YSQL would not lead to any blocking, but would surely abort either itself or all conflicting transactions based on priorities of the transactions (which are randomly chosen). We call this behavior fail-on-conflict concurrency control.
PostgreSQL uses wait-on-conflict concurrency control, where a transaction will wait upon encountering a lock-lock conflict in order to acquire the locks it needs and proceed.
Wait-on-conflict (In PostgreSQL): When a transaction T1 attempts to take a lock (either implicitly via a write or explicitly using SELECT FOR), it might be blocked by transactions that hold a conflicting lock (note again, either implicitly via a write or explicitly using SELECT FOR). The transaction will then wait for all lock-lock conflicting transactions to end before obtaining the lock. Once the other transactions end, either the lock will be taken or a lock-modification conflict will be detected and a serialization error will occur.
Some points to note are -
NOTE: Refer this article for a good overview of row-level locking.
Once wait-on-conflict concurrency control is supported, YSQL will have the same behaviour as Postgres with regards to transaction waiting behaviour in case of writing/locking, with two exceptions:
enable_wait_queues: a cluster-level gflag to turn on wait-on-conflict behavior. This will require a cluster restart. Note that all transactions in the cluster will either use wait-on-conflict OR fail-on-conflict behavior, and mixed behavior is tolerable during restart/migration but otherwise generally not supported.
create table test (k int primary key, v int);
insert into test values (1, 1);
For each case, we will only explore the interaction of FOR SHARE along with an update
to a non-primary key column. Recall that FOR SHARE doesn’t allow any concurrent modification to the locked tuple.
This feature is upgrade and downgrade safe. When turning the gflag on/off, or during rolling restarts across versions with the flag “on” in the higher version, if some nodes have wait-on-conflict behavior enabled and some don’t, users will experience mixed (but still correct) behavior. A mix of both fail-on-conflict and wait-on-conflict traffic will result in the following additional YSQL specific semantics -