Skip to content

Commit 65468cc

Browse files
committed
Fix predicate-locking of HOT updated rows.
In serializable mode, heap_hot_search_buffer() incorrectly acquired a predicate lock on the root tuple, not the returned tuple that satisfied the visibility checks. As explained in README-SSI, the predicate lock does not need to be copied or extended to other tuple versions, but for that to work, the correct, visible, tuple version must be locked in the first place. The original SSI commit had this bug in it, but it was fixed back in 2013, in commit 81fbbfe. But unfortunately, it was reintroduced a few months later in commit b89e151. Wising up from that, add a regression test to cover this, so that it doesn't get reintroduced again. Also, move the code that sets 't_self', so that it happens at the same time that the other HeapTuple fields are set, to make it more clear that all the code in the loop operate on the "current" tuple in the chain, not the root tuple. Bug spotted by Andres Freund, analysis and original fix by Thomas Munro, test case and some additional changes to the fix by Heikki Linnakangas. Backpatch to all supported versions (9.4). Discussion: https://www.postgresql.org/message-id/20190731210630.nqhszuktygwftjty%40alap3.anarazel.de
1 parent 1ba4d0f commit 65468cc

File tree

4 files changed

+70
-17
lines changed

4 files changed

+70
-17
lines changed

src/backend/access/heap/heapam.c

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2000,6 +2000,7 @@ heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer,
20002000
{
20012001
Page dp = (Page) BufferGetPage(buffer);
20022002
TransactionId prev_xmax = InvalidTransactionId;
2003+
BlockNumber blkno;
20032004
OffsetNumber offnum;
20042005
bool at_chain_start;
20052006
bool valid;
@@ -2009,14 +2010,13 @@ heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer,
20092010
if (all_dead)
20102011
*all_dead = first_call;
20112012

2012-
Assert(TransactionIdIsValid(RecentGlobalXmin));
2013-
2014-
Assert(ItemPointerGetBlockNumber(tid) == BufferGetBlockNumber(buffer));
2013+
blkno = ItemPointerGetBlockNumber(tid);
20152014
offnum = ItemPointerGetOffsetNumber(tid);
20162015
at_chain_start = first_call;
20172016
skip = !first_call;
20182017

2019-
heapTuple->t_self = *tid;
2018+
Assert(TransactionIdIsValid(RecentGlobalXmin));
2019+
Assert(BufferGetBlockNumber(buffer) == blkno);
20202020

20212021
/* Scan through possible multiple members of HOT-chain */
20222022
for (;;)
@@ -2044,10 +2044,16 @@ heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer,
20442044
break;
20452045
}
20462046

2047+
/*
2048+
* Update heapTuple to point to the element of the HOT chain we're
2049+
* currently investigating. Having t_self set correctly is important
2050+
* because the SSI checks and the *Satisfies routine for historical
2051+
* MVCC snapshots need the correct tid to decide about the visibility.
2052+
*/
20472053
heapTuple->t_data = (HeapTupleHeader) PageGetItem(dp, lp);
20482054
heapTuple->t_len = ItemIdGetLength(lp);
20492055
heapTuple->t_tableOid = RelationGetRelid(relation);
2050-
ItemPointerSetOffsetNumber(&heapTuple->t_self, offnum);
2056+
ItemPointerSet(&heapTuple->t_self, blkno, offnum);
20512057

20522058
/*
20532059
* Shouldn't see a HEAP_ONLY tuple at chain start.
@@ -2073,21 +2079,10 @@ heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer,
20732079
*/
20742080
if (!skip)
20752081
{
2076-
/*
2077-
* For the benefit of logical decoding, have t_self point at the
2078-
* element of the HOT chain we're currently investigating instead
2079-
* of the root tuple of the HOT chain. This is important because
2080-
* the *Satisfies routine for historical mvcc snapshots needs the
2081-
* correct tid to decide about the visibility in some cases.
2082-
*/
2083-
ItemPointerSet(&(heapTuple->t_self), BufferGetBlockNumber(buffer), offnum);
2084-
20852082
/* If it's visible per the snapshot, we must return it */
20862083
valid = HeapTupleSatisfiesVisibility(heapTuple, snapshot, buffer);
20872084
CheckForSerializableConflictOut(valid, relation, heapTuple,
20882085
buffer, snapshot);
2089-
/* reset to original, non-redirected, tid */
2090-
heapTuple->t_self = *tid;
20912086

20922087
if (valid)
20932088
{
@@ -2116,7 +2111,7 @@ heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer,
21162111
if (HeapTupleIsHotUpdated(heapTuple))
21172112
{
21182113
Assert(ItemPointerGetBlockNumber(&heapTuple->t_data->t_ctid) ==
2119-
ItemPointerGetBlockNumber(tid));
2114+
blkno);
21202115
offnum = ItemPointerGetOffsetNumber(&heapTuple->t_data->t_ctid);
21212116
at_chain_start = false;
21222117
prev_xmax = HeapTupleHeaderGetUpdateXid(heapTuple->t_data);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Parsed test spec with 2 sessions
2+
3+
starting permutation: b1 b2 r1 r2 w1 w2 c1 c2
4+
step b1: BEGIN ISOLATION LEVEL SERIALIZABLE;
5+
step b2: BEGIN ISOLATION LEVEL SERIALIZABLE;
6+
step r1: SELECT * FROM test WHERE i IN (5, 7)
7+
i t
8+
9+
5 apple
10+
7 pear_hot_updated
11+
step r2: SELECT * FROM test WHERE i IN (5, 7)
12+
i t
13+
14+
5 apple
15+
7 pear_hot_updated
16+
step w1: UPDATE test SET t = 'pear_xact1' WHERE i = 7
17+
step w2: UPDATE test SET t = 'apple_xact2' WHERE i = 5
18+
step c1: COMMIT;
19+
step c2: COMMIT;
20+
ERROR: could not serialize access due to read/write dependencies among transactions

src/test/isolation/isolation_schedule

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ test: partial-index
1717
test: two-ids
1818
test: multiple-row-versions
1919
test: index-only-scan
20+
test: predicate-lock-hot-tuple
2021
test: deadlock-simple
2122
test: deadlock-hard
2223
test: deadlock-soft
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Test predicate locks on HOT updated tuples.
2+
#
3+
# This test has two serializable transactions. Both select two rows
4+
# from the table, and then update one of them.
5+
# If these were serialized (run one at a time), the transaction that
6+
# runs later would see one of the rows to be updated.
7+
#
8+
# Any overlap between the transactions must cause a serialization failure.
9+
# We used to have a bug in predicate locking HOT updated tuples, which
10+
# caused the conflict to be missed, if the row was HOT updated.
11+
12+
setup
13+
{
14+
CREATE TABLE test (i int PRIMARY KEY, t text);
15+
INSERT INTO test VALUES (5, 'apple'), (7, 'pear'), (11, 'banana');
16+
-- HOT-update 'pear' row.
17+
UPDATE test SET t = 'pear_hot_updated' WHERE i = 7;
18+
}
19+
20+
teardown
21+
{
22+
DROP TABLE test;
23+
}
24+
25+
session "s1"
26+
step "b1" { BEGIN ISOLATION LEVEL SERIALIZABLE; }
27+
step "r1" { SELECT * FROM test WHERE i IN (5, 7) }
28+
step "w1" { UPDATE test SET t = 'pear_xact1' WHERE i = 7 }
29+
step "c1" { COMMIT; }
30+
31+
session "s2"
32+
step "b2" { BEGIN ISOLATION LEVEL SERIALIZABLE; }
33+
step "r2" { SELECT * FROM test WHERE i IN (5, 7) }
34+
step "w2" { UPDATE test SET t = 'apple_xact2' WHERE i = 5 }
35+
step "c2" { COMMIT; }
36+
37+
permutation "b1" "b2" "r1" "r2" "w1" "w2" "c1" "c2"

0 commit comments

Comments
 (0)