diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c index 55a8ca7ac4..03581bea66 100644 --- a/src/backend/access/transam/multixact.c +++ b/src/backend/access/transam/multixact.c @@ -1274,6 +1274,39 @@ retry: return truelength; } +/* + * MultiXactHasRunningRemoteMembers + * Does the given multixact have still-live members from + * transactions other than our own? + */ +bool +MultiXactHasRunningRemoteMembers(MultiXactId multi) +{ + MultiXactMember *members; + int nmembers; + int i; + + nmembers = GetMultiXactIdMembers(multi, &members, true); + if (nmembers <= 0) + return false; + + for (i = 0; i < nmembers; i++) + { + /* not interested in our own members */ + if (TransactionIdIsCurrentTransactionId(members[i].xid)) + continue; + + if (TransactionIdIsInProgress(members[i].xid)) + { + pfree(members); + return true; + } + } + + pfree(members); + return false; +} + /* * mxactMemberComparator * qsort comparison function for MultiXactMember diff --git a/src/backend/utils/time/tqual.c b/src/backend/utils/time/tqual.c index e5d0b0a666..1ff1da2f07 100644 --- a/src/backend/utils/time/tqual.c +++ b/src/backend/utils/time/tqual.c @@ -493,8 +493,36 @@ HeapTupleSatisfiesUpdate(HeapTuple htup, CommandId curcid, if (tuple->t_infomask & HEAP_XMAX_INVALID) /* xid invalid */ return HeapTupleMayBeUpdated; - if (HEAP_XMAX_IS_LOCKED_ONLY(tuple->t_infomask)) /* not deleter */ - return HeapTupleMayBeUpdated; + if (HEAP_XMAX_IS_LOCKED_ONLY(tuple->t_infomask)) + { + TransactionId xmax; + + xmax = HeapTupleHeaderGetRawXmax(tuple); + + /* + * Careful here: even though this tuple was created by our own + * transaction, it might be locked by other transactions, if + * the original version was key-share locked when we updated + * it. + */ + + if (tuple->t_infomask & HEAP_XMAX_IS_MULTI) + { + if (MultiXactHasRunningRemoteMembers(xmax)) + return HeapTupleBeingUpdated; + else + return HeapTupleMayBeUpdated; + } + + /* if locker is gone, all's well */ + if (!TransactionIdIsInProgress(xmax)) + return HeapTupleMayBeUpdated; + + if (!TransactionIdIsCurrentTransactionId(xmax)) + return HeapTupleBeingUpdated; + else + return HeapTupleMayBeUpdated; + } if (tuple->t_infomask & HEAP_XMAX_IS_MULTI) { @@ -507,7 +535,11 @@ HeapTupleSatisfiesUpdate(HeapTuple htup, CommandId curcid, /* updating subtransaction must have aborted */ if (!TransactionIdIsCurrentTransactionId(xmax)) + { + if (MultiXactHasRunningRemoteMembers(HeapTupleHeaderGetRawXmax(tuple))) + return HeapTupleBeingUpdated; return HeapTupleMayBeUpdated; + } else { if (HeapTupleHeaderGetCmax(tuple) >= curcid) diff --git a/src/include/access/multixact.h b/src/include/access/multixact.h index 0e3b273b9e..5f82907dac 100644 --- a/src/include/access/multixact.h +++ b/src/include/access/multixact.h @@ -89,6 +89,7 @@ extern bool MultiXactIdIsRunning(MultiXactId multi); extern void MultiXactIdSetOldestMember(void); extern int GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **xids, bool allow_old); +extern bool MultiXactHasRunningRemoteMembers(MultiXactId multi); extern bool MultiXactIdPrecedes(MultiXactId multi1, MultiXactId multi2); extern bool MultiXactIdPrecedesOrEquals(MultiXactId multi1, MultiXactId multi2); diff --git a/src/test/isolation/expected/propagate-lock-delete.out b/src/test/isolation/expected/propagate-lock-delete.out new file mode 100644 index 0000000000..b668b895f1 --- /dev/null +++ b/src/test/isolation/expected/propagate-lock-delete.out @@ -0,0 +1,105 @@ +Parsed test spec with 3 sessions + +starting permutation: s1b s1l s2b s2l s3b s3u s3d s1c s2c s3c +step s1b: BEGIN; +step s1l: INSERT INTO child VALUES (1); +step s2b: BEGIN; +step s2l: INSERT INTO child VALUES (1); +step s3b: BEGIN; +step s3u: UPDATE parent SET c=lower(c); +step s3d: DELETE FROM parent; +step s1c: COMMIT; +step s2c: COMMIT; +step s3d: <... completed> +error in steps s2c s3d: ERROR: update or delete on table "parent" violates foreign key constraint "child_i_fkey" on table "child" +step s3c: COMMIT; + +starting permutation: s1b s1l s2b s2l s3b s3u s3svu s3d s1c s2c s3c +step s1b: BEGIN; +step s1l: INSERT INTO child VALUES (1); +step s2b: BEGIN; +step s2l: INSERT INTO child VALUES (1); +step s3b: BEGIN; +step s3u: UPDATE parent SET c=lower(c); +step s3svu: SAVEPOINT f; UPDATE parent SET c = 'bbb'; ROLLBACK TO f; +step s3d: DELETE FROM parent; +step s1c: COMMIT; +step s2c: COMMIT; +step s3d: <... completed> +error in steps s2c s3d: ERROR: update or delete on table "parent" violates foreign key constraint "child_i_fkey" on table "child" +step s3c: COMMIT; + +starting permutation: s1b s1l s2b s2l s3b s3u2 s3d s1c s2c s3c +step s1b: BEGIN; +step s1l: INSERT INTO child VALUES (1); +step s2b: BEGIN; +step s2l: INSERT INTO child VALUES (1); +step s3b: BEGIN; +step s3u2: UPDATE parent SET i = i; +step s3d: DELETE FROM parent; +step s1c: COMMIT; +step s2c: COMMIT; +step s3d: <... completed> +error in steps s2c s3d: ERROR: update or delete on table "parent" violates foreign key constraint "child_i_fkey" on table "child" +step s3c: COMMIT; + +starting permutation: s1b s1l s2b s2l s3b s3u2 s3svu s3d s1c s2c s3c +step s1b: BEGIN; +step s1l: INSERT INTO child VALUES (1); +step s2b: BEGIN; +step s2l: INSERT INTO child VALUES (1); +step s3b: BEGIN; +step s3u2: UPDATE parent SET i = i; +step s3svu: SAVEPOINT f; UPDATE parent SET c = 'bbb'; ROLLBACK TO f; +step s3d: DELETE FROM parent; +step s1c: COMMIT; +step s2c: COMMIT; +step s3d: <... completed> +error in steps s2c s3d: ERROR: update or delete on table "parent" violates foreign key constraint "child_i_fkey" on table "child" +step s3c: COMMIT; + +starting permutation: s1b s1l s3b s3u s3d s1c s3c +step s1b: BEGIN; +step s1l: INSERT INTO child VALUES (1); +step s3b: BEGIN; +step s3u: UPDATE parent SET c=lower(c); +step s3d: DELETE FROM parent; +step s1c: COMMIT; +step s3d: <... completed> +error in steps s1c s3d: ERROR: update or delete on table "parent" violates foreign key constraint "child_i_fkey" on table "child" +step s3c: COMMIT; + +starting permutation: s1b s1l s3b s3u s3svu s3d s1c s3c +step s1b: BEGIN; +step s1l: INSERT INTO child VALUES (1); +step s3b: BEGIN; +step s3u: UPDATE parent SET c=lower(c); +step s3svu: SAVEPOINT f; UPDATE parent SET c = 'bbb'; ROLLBACK TO f; +step s3d: DELETE FROM parent; +step s1c: COMMIT; +step s3d: <... completed> +error in steps s1c s3d: ERROR: update or delete on table "parent" violates foreign key constraint "child_i_fkey" on table "child" +step s3c: COMMIT; + +starting permutation: s1b s1l s3b s3u2 s3d s1c s3c +step s1b: BEGIN; +step s1l: INSERT INTO child VALUES (1); +step s3b: BEGIN; +step s3u2: UPDATE parent SET i = i; +step s3d: DELETE FROM parent; +step s1c: COMMIT; +step s3d: <... completed> +error in steps s1c s3d: ERROR: update or delete on table "parent" violates foreign key constraint "child_i_fkey" on table "child" +step s3c: COMMIT; + +starting permutation: s1b s1l s3b s3u2 s3svu s3d s1c s3c +step s1b: BEGIN; +step s1l: INSERT INTO child VALUES (1); +step s3b: BEGIN; +step s3u2: UPDATE parent SET i = i; +step s3svu: SAVEPOINT f; UPDATE parent SET c = 'bbb'; ROLLBACK TO f; +step s3d: DELETE FROM parent; +step s1c: COMMIT; +step s3d: <... completed> +error in steps s1c s3d: ERROR: update or delete on table "parent" violates foreign key constraint "child_i_fkey" on table "child" +step s3c: COMMIT; diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index dd4b404183..1e73b4aebd 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -21,5 +21,6 @@ test: delete-abort-savept-2 test: aborted-keyrevoke test: multixact-no-deadlock test: multixact-no-forget +test: propagate-lock-delete test: drop-index-concurrently-1 test: timeouts diff --git a/src/test/isolation/specs/propagate-lock-delete.spec b/src/test/isolation/specs/propagate-lock-delete.spec new file mode 100644 index 0000000000..857c36b3db --- /dev/null +++ b/src/test/isolation/specs/propagate-lock-delete.spec @@ -0,0 +1,42 @@ +# When an update propagates a preexisting lock on the updated tuple, make sure +# we don't ignore the lock in subsequent operations of the new version. (The +# version with the aborted savepoint uses a slightly different code path). +setup +{ + create table parent (i int, c char(3)); + create unique index parent_idx on parent (i); + insert into parent values (1, 'AAA'); + create table child (i int references parent(i)); +} + +teardown +{ + drop table child, parent; +} + +session "s1" +step "s1b" { BEGIN; } +step "s1l" { INSERT INTO child VALUES (1); } +step "s1c" { COMMIT; } + +session "s2" +step "s2b" { BEGIN; } +step "s2l" { INSERT INTO child VALUES (1); } +step "s2c" { COMMIT; } + +session "s3" +step "s3b" { BEGIN; } +step "s3u" { UPDATE parent SET c=lower(c); } # no key update +step "s3u2" { UPDATE parent SET i = i; } # key update +step "s3svu" { SAVEPOINT f; UPDATE parent SET c = 'bbb'; ROLLBACK TO f; } +step "s3d" { DELETE FROM parent; } +step "s3c" { COMMIT; } + +permutation "s1b" "s1l" "s2b" "s2l" "s3b" "s3u" "s3d" "s1c" "s2c" "s3c" +permutation "s1b" "s1l" "s2b" "s2l" "s3b" "s3u" "s3svu" "s3d" "s1c" "s2c" "s3c" +permutation "s1b" "s1l" "s2b" "s2l" "s3b" "s3u2" "s3d" "s1c" "s2c" "s3c" +permutation "s1b" "s1l" "s2b" "s2l" "s3b" "s3u2" "s3svu" "s3d" "s1c" "s2c" "s3c" +permutation "s1b" "s1l" "s3b" "s3u" "s3d" "s1c" "s3c" +permutation "s1b" "s1l" "s3b" "s3u" "s3svu" "s3d" "s1c" "s3c" +permutation "s1b" "s1l" "s3b" "s3u2" "s3d" "s1c" "s3c" +permutation "s1b" "s1l" "s3b" "s3u2" "s3svu" "s3d" "s1c" "s3c"