Skip to content

Commit 681bee0

Browse files
committed
optbuilder: take FOR SHARE locking during RC FK child checks
Under Read Committed isolation, we need to take FOR SHARE locking during FK child checks to ensure that the rows read by the FK check have not already been written to at a later timestamp. (This is the same reason that we needed to add FOR UPDATE locking to RC FK cascades in #150291.) Even with this locking, we still need Serializable transactions to have the FK parent locking enabled, if both SSI and RC transactions are going to concurrently modify rows involved in FK relationships. This is because FOR SHARE (and FOR UPDATE) locking does not prevent phantoms. Informs: #151663 Release note (bug fix): A bug that would allow FK violations as a result of some combinations of concurrent Read Committed and Serializable transactions has been fixed. Note that if both Serializable and weaker-isolation transactions will concurrently modify rows involved in FK relationships, the Serializable transactions must have the following session variables set to prevent any possible FK violations: - `SET enable_implicit_fk_locking_for_serializable = on;` - `SET enable_shared_locking_for_serializable = on;` - `SET enable_durable_locking_for_serializable = on;`
1 parent a0acebd commit 681bee0

File tree

3 files changed

+207
-8
lines changed

3 files changed

+207
-8
lines changed

pkg/ccl/logictestccl/testdata/logic_test/fk_read_committed

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,191 @@ SELECT * FROM child_150282;
202202
4 4
203203

204204
subtest end
205+
206+
subtest fk_cascade_race_151663
207+
208+
statement ok
209+
CREATE TABLE parent_151663 (
210+
p INT PRIMARY KEY,
211+
q INT
212+
);
213+
214+
statement ok
215+
CREATE TABLE child_151663 (
216+
c INT PRIMARY KEY,
217+
p INT REFERENCES parent_151663 (p),
218+
INDEX (p),
219+
FAMILY (c, p)
220+
);
221+
222+
statement ok
223+
CREATE USER testuser2;
224+
CREATE USER testuser3
225+
226+
statement ok
227+
GRANT ALL ON TABLE parent_151663 TO testuser;
228+
GRANT ALL ON TABLE parent_151663 TO testuser2;
229+
GRANT ALL ON TABLE parent_151663 TO testuser3;
230+
GRANT ALL ON TABLE child_151663 TO testuser3
231+
232+
statement ok
233+
INSERT INTO parent_151663 VALUES (1, 0), (2, 0), (3, 0)
234+
235+
# The timeline of this test is:
236+
#
237+
# testuser testuser2 testuser3 root
238+
# -------- --------- --------- ----
239+
# begin
240+
# update p=2
241+
# begin
242+
# lock p=3
243+
# begin RC
244+
# start DELETE
245+
# wait on p=3
246+
# begin SSI
247+
# start INSERT
248+
# wait on p=2
249+
# rollback
250+
# lock p=1, p=2
251+
# rollback
252+
# wait on p=1
253+
# insert c=10, c=20
254+
# finish INSERT
255+
# commit
256+
# delete p=1
257+
# FK check child
258+
# finish DELETE
259+
# commit
260+
#
261+
# The transactions run by testuser3 and root are the important part. testuser
262+
# and testuser2 only exist to control the timing of the transactions run by
263+
# testuser3 and root.
264+
#
265+
# The transactions run by testuser3 and root conflict with each other. To
266+
# prevent a FK violation they need to either run serially, or one transaction
267+
# needs to fail.
268+
#
269+
# This test demonstrates that we need locking during both the parent FK check
270+
# and the child FK check to prevent a FK violation.
271+
#
272+
# The parent FK check is performed by the Serializable INSERT run by
273+
# root. Without locking during this check, the two transactions are not
274+
# guaranteed to detect the conflict with each other. For example, without this
275+
# locking, the Read Committed DELETE could finish after the INSERT's FK check
276+
# but before its insert. The INSERT could then write a phantom child row and
277+
# have a successful read refresh, and both transactions could commit. Locking
278+
# during the parent FK check avoids this by forcing the transactions to
279+
# coordinate on the parent row.
280+
#
281+
# The child FK check is performed by the Read Committed DELETE run by
282+
# testuser3. Without locking during this check, the Read Committed DELETE could
283+
# perform a stale read for its child FK check, missing a newer child row. For
284+
# example, it could miss the row written by the Serializable INSERT and
285+
# successfully commit. (Serializable isolation does not have this same risk of
286+
# stale reads and thus does not need locking during child FK checks.)
287+
#
288+
# For more discussion of this and other scenarios please see #151663.
289+
290+
user testuser
291+
292+
statement ok
293+
BEGIN
294+
295+
# Use an update on p=2 to block the future insert by root.
296+
statement ok
297+
UPDATE parent_151663 SET q = q + 1 WHERE p = 2
298+
299+
user testuser2
300+
301+
statement ok
302+
BEGIN
303+
304+
# Use a lock on p=3 to block the future delete by testuser3.
305+
statement ok
306+
SELECT * FROM parent_151663 WHERE p = 3 FOR UPDATE
307+
308+
user testuser3
309+
310+
statement ok
311+
BEGIN ISOLATION LEVEL READ COMMITTED
312+
313+
statement ok
314+
SELECT 1
315+
316+
statement async fkdelete error pgcode 23503 delete on table "parent_151663" violates foreign key constraint "child_151663_p_fkey" on table "child_151663"\nDETAIL: Key \(p\)=\(1\) is still referenced from table "child_151663"\.
317+
DELETE FROM parent_151663 WHERE p = (SELECT 1 FROM parent_151663 WHERE p = 3 FOR UPDATE)
318+
319+
user root
320+
321+
# Give the delete a moment to wait on the p=3 lock by testuser2.
322+
statement ok
323+
SELECT pg_sleep(1)
324+
325+
# The serializable insert needs this locking to properly sychronize with the
326+
# read committed delete.
327+
statement ok
328+
SET enable_implicit_fk_locking_for_serializable = on;
329+
SET enable_shared_locking_for_serializable = on;
330+
SET enable_durable_locking_for_serializable = on
331+
332+
statement ok
333+
BEGIN ISOLATION LEVEL SERIALIZABLE
334+
335+
statement async fkinsert
336+
INSERT INTO child_151663 VALUES (10, 1), (20, 2)
337+
338+
user testuser
339+
340+
# Give the insert a moment to wait on the p=2 update by testuser.
341+
statement ok
342+
SELECT pg_sleep(1)
343+
344+
statement ok
345+
ROLLBACK
346+
347+
user testuser2
348+
349+
# Give the insert a moment to lock p=1.
350+
statement ok
351+
SELECT pg_sleep(1)
352+
353+
statement ok
354+
ROLLBACK
355+
356+
user root
357+
358+
awaitstatement fkinsert
359+
360+
statement ok
361+
COMMIT
362+
363+
user testuser3
364+
365+
awaitstatement fkdelete
366+
367+
statement ok
368+
COMMIT
369+
370+
user root
371+
372+
# Check that there are no orphan children.
373+
374+
query II
375+
SELECT * FROM parent_151663 ORDER BY p
376+
----
377+
1 0
378+
2 0
379+
3 0
380+
381+
query II
382+
SELECT * FROM child_151663 ORDER BY c
383+
----
384+
10 1
385+
20 2
386+
387+
statement ok
388+
RESET enable_implicit_fk_locking_for_serializable;
389+
RESET enable_shared_locking_for_serializable;
390+
RESET enable_durable_locking_for_serializable
391+
392+
subtest end

pkg/sql/opt/exec/execbuilder/testdata/fk_read_committed

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,9 @@ vectorized: true
192192
estimated row count: 1 (missing stats)
193193
label: buffer 1
194194

195-
# Foreign key checks of the child do not require locking.
195+
# Foreign key checks of the child do not require locking under serializable
196+
# isolation, but do require locking under read committed isolation to avoid
197+
# stale reads.
196198
query T
197199
EXPLAIN (OPT) UPDATE jars SET j = j + 4
198200
----
@@ -211,7 +213,8 @@ update jars
211213
│ └── with-scan &1
212214
├── distinct-on
213215
│ └── scan cookies
214-
│ └── flags: avoid-full-scan
216+
│ ├── flags: avoid-full-scan
217+
│ └── locking: for-share,durability-guaranteed
215218
└── filters
216219
└── j = cookies.j
217220

@@ -358,6 +361,8 @@ vectorized: true
358361
estimated row count: 1,000 (missing stats)
359362
table: cookies@cookies_pkey
360363
spans: FULL SCAN
364+
locking strength: for share
365+
locking durability: guaranteed
361366

362367
query T
363368
EXPLAIN (OPT) DELETE FROM jars WHERE j = 1
@@ -371,7 +376,8 @@ delete jars
371376
└── semi-join (hash)
372377
├── with-scan &1
373378
├── scan cookies
374-
│ └── flags: avoid-full-scan
379+
│ ├── flags: avoid-full-scan
380+
│ └── locking: for-share,durability-guaranteed
375381
└── filters
376382
└── j = cookies.j
377383

@@ -440,6 +446,8 @@ vectorized: true
440446
│ estimated row count: 1,000 (missing stats)
441447
│ table: cookies@cookies_pkey
442448
│ spans: FULL SCAN
449+
│ locking strength: for share
450+
│ locking durability: guaranteed
443451
444452
└── • scan buffer
445453
columns: (j)

pkg/sql/opt/optbuilder/mutation_builder_fk.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -640,11 +640,14 @@ func (h *fkCheckHelper) buildOtherTableScan(parent bool) (outScope *scope, tabMe
640640
// For insertion-side checks, if enable_implicit_fk_locking_for_serializable
641641
// is true or we're using a weaker isolation level, we lock the parent row(s)
642642
// to prevent concurrent mutations of the parent from other transactions from
643-
// violating the FK constraint. Deletion-side checks don't need to lock
644-
// because they can rely on the deletion intent conflicting with locks from
645-
// any concurrent inserts or updates of the child.
646-
if parent && (h.mb.b.evalCtx.TxnIsoLevel != isolation.Serializable ||
647-
h.mb.b.evalCtx.SessionData().ImplicitFKLockingForSerializable) {
643+
// violating the FK constraint. Deletion-side checks don't need to lock the
644+
// child row(s) under Serializable isolation because deletions can rely on the
645+
// deletion intent on the parent row conflicting with locks from any
646+
// concurrent inserts or updates of the child. Deletion-side checks do need to
647+
// lock the child row(s) under Read Committed isolation to ensure there are no
648+
// newer writers.
649+
if h.mb.b.evalCtx.TxnIsoLevel != isolation.Serializable ||
650+
(parent && h.mb.b.evalCtx.SessionData().ImplicitFKLockingForSerializable) {
648651
locking = lockingSpec{
649652
&lockingItem{
650653
item: &tree.LockingItem{

0 commit comments

Comments
 (0)