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
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+ }
5571step 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
68135session checker
69136setup
@@ -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 .
83150step 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+ }
88185step 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).
104201permutation 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