Skip to content

Commit 604754a

Browse files
committed
Add deadlock provocation tests for GIN posting tree page deletion
Both vacuum and insertions acquire buffer locks leaf-to-parent and left-to-right, so they can never form a lock cycle. Exercise both directions of the would-be cycle: suspend vacuum just before a page deletion, holding the leaf pair and the parent, and drive an insertion into those locks; then suspend an inserter finishing an incomplete split, holding the leaf, and drive vacuum's sweep into it. If the locking protocol regressed and allowed a cycle, these permutations would hang and time out. The isolationtester cannot detect buffer lock waits, so the blocked steps carry (*) markers, and the unwinding of the second scenario has to happen within a single step.
1 parent 41f55ee commit 604754a

2 files changed

Lines changed: 311 additions & 4 deletions

File tree

‎src/test/modules/gin/expected/vacuum_posting_tree.out‎

Lines changed: 192 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
Parsed test spec with 2 sessions
1+
Parsed test spec with 3 sessions
22

33
starting permutation: v_attach_notice v_delete v_vacuum v_detach_notice c_count c_check
44
injection_points_set_local
55
--------------------------
66

77
(1 row)
88

9+
injection_points_set_local
10+
--------------------------
11+
12+
(1 row)
13+
914
step v_attach_notice:
1015
SELECT injection_points_attach('gin-vacuum-delete-posting-page', 'notice');
1116

@@ -54,6 +59,11 @@ injection_points_set_local
5459

5560
(1 row)
5661

62+
injection_points_set_local
63+
--------------------------
64+
65+
(1 row)
66+
5767
step v_attach_notice:
5868
SELECT injection_points_attach('gin-vacuum-delete-posting-page', 'notice');
5969

@@ -129,3 +139,184 @@ gin_index_check
129139

130140
(1 row)
131141

142+
143+
starting permutation: v_attach_wait_delete v_delete v_vacuum i_insert_batch c_detach_wake_delete i_noop c_count c_check
144+
injection_points_set_local
145+
--------------------------
146+
147+
(1 row)
148+
149+
injection_points_set_local
150+
--------------------------
151+
152+
(1 row)
153+
154+
step v_attach_wait_delete:
155+
SELECT injection_points_attach('gin-vacuum-delete-posting-page', 'wait');
156+
157+
injection_points_attach
158+
-----------------------
159+
160+
(1 row)
161+
162+
step v_delete:
163+
DELETE FROM gin_pt WHERE k BETWEEN 5000 AND 25000;
164+
165+
step v_vacuum:
166+
VACUUM gin_pt;
167+
<waiting ...>
168+
step i_insert_batch:
169+
INSERT INTO gin_pt(i) SELECT array[1] FROM generate_series(1, 1000);
170+
<waiting ...>
171+
step c_detach_wake_delete:
172+
SELECT injection_points_detach('gin-vacuum-delete-posting-page');
173+
SELECT injection_points_wakeup('gin-vacuum-delete-posting-page');
174+
175+
injection_points_detach
176+
-----------------------
177+
178+
(1 row)
179+
180+
injection_points_wakeup
181+
-----------------------
182+
183+
(1 row)
184+
185+
step v_vacuum: <... completed>
186+
step i_insert_batch: <... completed>
187+
step i_noop:
188+
189+
step c_count:
190+
SELECT count(*) FROM gin_pt WHERE i @> array[1];
191+
192+
count
193+
-----
194+
10999
195+
(1 row)
196+
197+
step c_check:
198+
SELECT gin_index_check('gin_pt_idx');
199+
200+
gin_index_check
201+
---------------
202+
203+
(1 row)
204+
205+
206+
starting permutation: i_attach_error i_split_fail i_detach_error i_attach_wait_finish i_insert_one v_attach_wait v_vacuum c_wake_resume_then_finish v_noop c_count c_check
207+
injection_points_set_local
208+
--------------------------
209+
210+
(1 row)
211+
212+
injection_points_set_local
213+
--------------------------
214+
215+
(1 row)
216+
217+
step i_attach_error:
218+
SELECT injection_points_attach('gin-leave-leaf-split-incomplete', 'error');
219+
220+
injection_points_attach
221+
-----------------------
222+
223+
(1 row)
224+
225+
inserter: NOTICE: insert failed: error triggered for injection point gin-leave-leaf-split-incomplete
226+
step i_split_fail:
227+
DO $$
228+
BEGIN
229+
INSERT INTO gin_pt(i) SELECT array[1] FROM generate_series(1, 20000);
230+
EXCEPTION WHEN OTHERS THEN
231+
RAISE NOTICE 'insert failed: %', SQLERRM;
232+
END;
233+
$$;
234+
235+
step i_detach_error:
236+
SELECT injection_points_detach('gin-leave-leaf-split-incomplete');
237+
238+
injection_points_detach
239+
-----------------------
240+
241+
(1 row)
242+
243+
step i_attach_wait_finish:
244+
SELECT injection_points_attach('gin-finish-incomplete-split', 'wait');
245+
246+
injection_points_attach
247+
-----------------------
248+
249+
(1 row)
250+
251+
step i_insert_one:
252+
INSERT INTO gin_pt(i) VALUES (array[1]);
253+
<waiting ...>
254+
step v_attach_wait:
255+
SELECT injection_points_attach('gin-vacuum-posting-tree-resume', 'wait');
256+
257+
injection_points_attach
258+
-----------------------
259+
260+
(1 row)
261+
262+
step v_vacuum:
263+
VACUUM gin_pt;
264+
<waiting ...>
265+
step c_wake_resume_then_finish:
266+
SELECT injection_points_detach('gin-vacuum-posting-tree-resume');
267+
SELECT injection_points_wakeup('gin-vacuum-posting-tree-resume');
268+
DO $$
269+
BEGIN
270+
WHILE NOT EXISTS (
271+
SELECT 1 FROM pg_stat_activity
272+
WHERE query LIKE '%VACUUM gin_pt%'
273+
AND pid != pg_backend_pid()
274+
AND wait_event_type = 'Buffer')
275+
LOOP
276+
PERFORM pg_sleep(0.001);
277+
END LOOP;
278+
END;
279+
$$;
280+
SELECT injection_points_detach('gin-finish-incomplete-split');
281+
SELECT injection_points_wakeup('gin-finish-incomplete-split');
282+
283+
injection_points_detach
284+
-----------------------
285+
286+
(1 row)
287+
288+
injection_points_wakeup
289+
-----------------------
290+
291+
(1 row)
292+
293+
injection_points_detach
294+
-----------------------
295+
296+
(1 row)
297+
298+
injection_points_wakeup
299+
-----------------------
300+
301+
(1 row)
302+
303+
step i_insert_one: <... completed>
304+
step v_vacuum: <... completed>
305+
step v_noop:
306+
307+
step c_count:
308+
SELECT count(*) FROM gin_pt WHERE i @> array[1];
309+
310+
count
311+
-----
312+
30001
313+
(1 row)
314+
315+
step c_check:
316+
SELECT gin_index_check('gin_pt_idx');
317+
318+
gin_index_check
319+
---------------
320+
321+
(1 row)
322+

‎src/test/modules/gin/specs/vacuum_posting_tree.spec‎

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Vacuum deletes empty posting tree leaf pages on the fly during its
55
# left-to-right sweep of the leaf level, without locking out concurrent
66
# insertions into the tree (see "Page deletion" in the GIN README).
7-
# This test uses two injection points:
7+
# This test uses three injection points:
88
#
99
# - gin-vacuum-posting-tree-resume fires between two leaf pages of the
1010
# sweep, while no buffer locks or pins are held, so it can be used as
@@ -15,6 +15,18 @@
1515
# all exclusively locked. A 'wait' here pauses vacuum at its maximum
1616
# lock footprint.
1717
#
18+
# - gin-finish-incomplete-split fires when a backend is about to finish
19+
# an incompletely split page, holding the split page exclusively
20+
# locked and about to lock its parent.
21+
#
22+
# The last two permutations are deadlock provocations: they suspend one
23+
# side of the vacuum-vs-insert lock dance at its maximum lock footprint
24+
# and let the other side run into those locks. The page deletion
25+
# protocol acquires locks leaf-to-parent and left-to-right on both
26+
# sides, so the blocked side must always get unstuck once the suspended
27+
# side is resumed. If the protocol regressed and allowed a lock cycle,
28+
# these permutations would hang and time out.
29+
#
1830
# The expected notice counts assume the default 8KB BLCKSZ: the posting
1931
# tree built by the setup has 8 pages, of which 3 become empty and
2032
# deletable after the DELETE.
@@ -52,6 +64,10 @@ step v_attach_wait
5264
{
5365
SELECT injection_points_attach('gin-vacuum-posting-tree-resume', 'wait');
5466
}
67+
step v_attach_wait_delete
68+
{
69+
SELECT injection_points_attach('gin-vacuum-delete-posting-page', 'wait');
70+
}
5571
step v_delete
5672
{
5773
DELETE FROM gin_pt WHERE k BETWEEN 5000 AND 25000;
@@ -64,6 +80,57 @@ step v_detach_notice
6480
{
6581
SELECT injection_points_detach('gin-vacuum-delete-posting-page');
6682
}
83+
# Empty step: launching it waits for this session's VACUUM to complete,
84+
# which lets later steps of other sessions depend on that completion.
85+
step v_noop
86+
{
87+
}
88+
89+
session inserter
90+
setup
91+
{
92+
SELECT injection_points_set_local();
93+
}
94+
step i_attach_error
95+
{
96+
SELECT injection_points_attach('gin-leave-leaf-split-incomplete', 'error');
97+
}
98+
# The batch is large enough to certainly split the rightmost leaf of
99+
# the posting tree; the injected error then aborts the insertion,
100+
# leaving the split incomplete (no downlink for the new right half).
101+
step i_split_fail
102+
{
103+
DO $$
104+
BEGIN
105+
INSERT INTO gin_pt(i) SELECT array[1] FROM generate_series(1, 20000);
106+
EXCEPTION WHEN OTHERS THEN
107+
RAISE NOTICE 'insert failed: %', SQLERRM;
108+
END;
109+
$$;
110+
}
111+
step i_detach_error
112+
{
113+
SELECT injection_points_detach('gin-leave-leaf-split-incomplete');
114+
}
115+
step i_attach_wait_finish
116+
{
117+
SELECT injection_points_attach('gin-finish-incomplete-split', 'wait');
118+
}
119+
# Descends onto the incompletely split leaf and pauses just before
120+
# finishing the split, holding an exclusive lock on that leaf.
121+
step i_insert_one
122+
{
123+
INSERT INTO gin_pt(i) VALUES (array[1]);
124+
}
125+
step i_insert_batch
126+
{
127+
INSERT INTO gin_pt(i) SELECT array[1] FROM generate_series(1, 1000);
128+
}
129+
# Empty step: launching it waits for this session's insert to complete,
130+
# which lets later steps of other sessions depend on that completion.
131+
step i_noop
132+
{
133+
}
67134

68135
session checker
69136
setup
@@ -78,13 +145,43 @@ step c_insert
78145
{
79146
INSERT INTO gin_pt(i) SELECT array[1] FROM generate_series(1, 1000);
80147
}
81-
# Detach before wakeup: the current waiter is woken up, and the sweep
82-
# does not wait at any of the remaining between-pages points.
148+
# Detach before wakeup: the current waiter is woken up, and the point
149+
# does not make anyone wait again.
83150
step c_detach_wake
84151
{
85152
SELECT injection_points_detach('gin-vacuum-posting-tree-resume');
86153
SELECT injection_points_wakeup('gin-vacuum-posting-tree-resume');
87154
}
155+
step c_detach_wake_delete
156+
{
157+
SELECT injection_points_detach('gin-vacuum-delete-posting-page');
158+
SELECT injection_points_wakeup('gin-vacuum-delete-posting-page');
159+
}
160+
# This must be a single step: while vacuum is blocked on a buffer lock,
161+
# the isolationtester cannot detect it as waiting, so it would never
162+
# launch a further step. Everything needed to unwind the lock chain
163+
# has to happen within one step. The polling loop in the middle makes
164+
# the provocation deterministic: the inserter is only released after
165+
# vacuum is confirmed to be blocked behind the leaf the inserter holds.
166+
step c_wake_resume_then_finish
167+
{
168+
SELECT injection_points_detach('gin-vacuum-posting-tree-resume');
169+
SELECT injection_points_wakeup('gin-vacuum-posting-tree-resume');
170+
DO $$
171+
BEGIN
172+
WHILE NOT EXISTS (
173+
SELECT 1 FROM pg_stat_activity
174+
WHERE query LIKE '%VACUUM gin_pt%'
175+
AND pid != pg_backend_pid()
176+
AND wait_event_type = 'Buffer')
177+
LOOP
178+
PERFORM pg_sleep(0.001);
179+
END LOOP;
180+
END;
181+
$$;
182+
SELECT injection_points_detach('gin-finish-incomplete-split');
183+
SELECT injection_points_wakeup('gin-finish-incomplete-split');
184+
}
88185
step c_check
89186
{
90187
SELECT gin_index_check('gin_pt_idx');
@@ -102,3 +199,22 @@ permutation v_attach_notice v_delete v_vacuum v_detach_notice c_count c_check
102199
# posting tree root. Then release vacuum and let it finish deleting
103200
# the empty pages (3 notices).
104201
permutation v_attach_notice v_attach_wait v_delete v_vacuum c_count c_insert c_detach_wake v_detach_notice c_count c_check
202+
203+
# Deadlock provocation, vacuum side suspended: pause vacuum just before
204+
# it deletes the first empty leaf, holding exclusive locks on the leaf,
205+
# its left sibling, and the parent (here the posting tree root). A
206+
# concurrent insertion then blocks on the root. The tester cannot
207+
# detect a buffer lock wait, hence the (*) marker; the completion
208+
# report order is pinned to vacuum first. Once vacuum is released, it
209+
# deletes all 3 pages and the insertion goes through.
210+
permutation v_attach_wait_delete v_delete v_vacuum i_insert_batch(*, v_vacuum) c_detach_wake_delete i_noop c_count c_check
211+
212+
# Deadlock provocation, inserter side suspended: manufacture an
213+
# incompletely split leaf, then pause a backend that is about to finish
214+
# that split, while it holds the leaf exclusively locked. Vacuum is
215+
# paused at its first between-pages point, then released; its sweep
216+
# runs into the leaf held by the suspended inserter and blocks on it,
217+
# with vacuum's own lock footprint (coupled leaf pair) already in
218+
# place. Releasing the inserter lets it lock the parent, insert the
219+
# downlink and finish, which unblocks vacuum.
220+
permutation i_attach_error i_split_fail i_detach_error i_attach_wait_finish i_insert_one v_attach_wait v_vacuum(i_insert_one) c_wake_resume_then_finish v_noop c_count c_check

0 commit comments

Comments
 (0)