Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 5c609a7

Browse files
committed
Fix locking a tuple updated by an aborted (sub)transaction
When heap_lock_tuple decides to follow the update chain, it tried to also lock any version of the tuple that was created by an update that was subsequently rolled back. This is pointless, since for all intents and purposes that tuple exists no more; and moreover it causes misbehavior, as reported independently by Marko Tiikkaja and Marti Raudsepp: some SELECT FOR UPDATE/SHARE queries may fail to return the tuples, and assertion-enabled builds crash. Fix by having heap_lock_updated_tuple test the xmin and return success immediately if the tuple was created by an aborted transaction. The condition where tuples become invisible occurs when an updated tuple chain is followed by heap_lock_updated_tuple, which reports the problem as HeapTupleSelfUpdated to its caller heap_lock_tuple, which in turn propagates that code outwards possibly leading the calling code (ExecLockRows) to believe that the tuple exists no longer. Backpatch to 9.3. Only on 9.5 and newer this leads to a visible failure, because of commit 27846f0; before that, heap_lock_tuple skips the whole dance when the tuple is already locked by the same transaction, because of the ancient HeapTupleSatisfiesUpdate behavior. Still, the buggy condition may also exist in more convoluted scenarios involving concurrent transactions, so it seems safer to fix the bug in the old branches too. Discussion: https://www.postgresql.org/message-id/CABRT9RC81YUf1=jsmWopcKJEro=VoeG2ou6sPwyOUTx_qteRsg@mail.gmail.com https://www.postgresql.org/message-id/48d3eade-98d3-8b9a-477e-1a8dc32a724d@joh.to
1 parent 984d0a1 commit 5c609a7

File tree

3 files changed

+56
-0
lines changed

3 files changed

+56
-0
lines changed

src/backend/access/heap/heapam.c

+11
Original file line numberDiff line numberDiff line change
@@ -5722,6 +5722,17 @@ heap_lock_updated_tuple_rec(Relation rel, ItemPointer tid, TransactionId xid,
57225722
goto out_locked;
57235723
}
57245724

5725+
/*
5726+
* Also check Xmin: if this tuple was created by an aborted
5727+
* (sub)transaction, then we already locked the last live one in the
5728+
* chain, thus we're done, so return success.
5729+
*/
5730+
if (TransactionIdDidAbort(HeapTupleHeaderGetXmin(mytup.t_data)))
5731+
{
5732+
UnlockReleaseBuffer(buf);
5733+
return HeapTupleMayBeUpdated;
5734+
}
5735+
57255736
old_infomask = mytup.t_data->t_infomask;
57265737
old_infomask2 = mytup.t_data->t_infomask2;
57275738
xmax = HeapTupleHeaderGetRawXmax(mytup.t_data);

src/test/regress/expected/combocid.out

+27
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,30 @@ SELECT ctid,cmin,* FROM combocidtest;
140140
(0,6) | 0 | 444
141141
(3 rows)
142142

143+
-- test for bug reported in
144+
-- CABRT9RC81YUf1=jsmWopcKJEro=VoeG2ou6sPwyOUTx_qteRsg@mail.gmail.com
145+
CREATE TABLE IF NOT EXISTS testcase(
146+
id int PRIMARY KEY,
147+
balance numeric
148+
);
149+
INSERT INTO testcase VALUES (1, 0);
150+
BEGIN;
151+
SELECT * FROM testcase WHERE testcase.id = 1 FOR UPDATE;
152+
id | balance
153+
----+---------
154+
1 | 0
155+
(1 row)
156+
157+
UPDATE testcase SET balance = balance + 400 WHERE id=1;
158+
SAVEPOINT subxact;
159+
UPDATE testcase SET balance = balance - 100 WHERE id=1;
160+
ROLLBACK TO SAVEPOINT subxact;
161+
-- should return one tuple
162+
SELECT * FROM testcase WHERE id = 1 FOR UPDATE;
163+
id | balance
164+
----+---------
165+
1 | 400
166+
(1 row)
167+
168+
ROLLBACK;
169+
DROP TABLE testcase;

src/test/regress/sql/combocid.sql

+18
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,21 @@ SELECT ctid,cmin,* FROM combocidtest;
9191
COMMIT;
9292

9393
SELECT ctid,cmin,* FROM combocidtest;
94+
95+
-- test for bug reported in
96+
-- CABRT9RC81YUf1=jsmWopcKJEro=VoeG2ou6sPwyOUTx_qteRsg@mail.gmail.com
97+
CREATE TABLE IF NOT EXISTS testcase(
98+
id int PRIMARY KEY,
99+
balance numeric
100+
);
101+
INSERT INTO testcase VALUES (1, 0);
102+
BEGIN;
103+
SELECT * FROM testcase WHERE testcase.id = 1 FOR UPDATE;
104+
UPDATE testcase SET balance = balance + 400 WHERE id=1;
105+
SAVEPOINT subxact;
106+
UPDATE testcase SET balance = balance - 100 WHERE id=1;
107+
ROLLBACK TO SAVEPOINT subxact;
108+
-- should return one tuple
109+
SELECT * FROM testcase WHERE id = 1 FOR UPDATE;
110+
ROLLBACK;
111+
DROP TABLE testcase;

0 commit comments

Comments
 (0)