Skip to content

Commit 91ce01a

Browse files
committed
Revise GIN README
We find GIN concurrency bugs from time to time. One of the problems here is that concurrency of GIN isn't well-documented in README. So, it might be even hard to distinguish design bugs from implementation bugs. This commit revised concurrency section in GIN README providing more details. Some examples are illustrated in ASCII art. Also, this commit add the explanation of how is tuple layout in internal GIN B-tree page different in comparison with nbtree. Discussion: https://postgr.es/m/CAPpHfduXR_ywyaVN4%2BOYEGaw%3DcPLzWX6RxYLBncKw8de9vOkqw%40mail.gmail.com Author: Alexander Korotkov Reviewed-by: Peter Geoghegan Backpatch-through: 9.4
1 parent 1414821 commit 91ce01a

File tree

1 file changed

+168
-41
lines changed

1 file changed

+168
-41
lines changed

src/backend/access/gin/README

Lines changed: 168 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,35 @@ fit on one pending-list page must have those pages to itself, even if this
215215
results in wasting much of the space on the preceding page and the last
216216
page for the tuple.)
217217

218+
GIN packs downlinks and pivot keys into internal page tuples in a different way
219+
than nbtree does. Lehman & Yao defines it as following.
220+
221+
P_0, K_1, P_1, K_2, P_2, ... , K_n, P_n, K_{n+1}
222+
223+
There P_i is a downlink and K_i is a key. K_i splits key space between P_{i-1}
224+
and P_i (0 <= i <= n). K_{n+1} is high key.
225+
226+
In internal page tuple is key and downlink grouped together. nbtree packs
227+
keys and downlinks into tuples as following.
228+
229+
(K_{n+1}, None), (-Inf, P_0), (K_1, P_1), ... , (K_n, P_n)
230+
231+
There tuples are shown in parentheses. So, highkey is stored separately. P_i
232+
is grouped with K_i. P_0 is grouped with -Inf key.
233+
234+
GIN packs keys and downlinks into tuples in a different way.
235+
236+
(P_0, K_1), (P_1, K_2), ... , (P_n, K_{n+1})
237+
238+
P_i is grouped with K_{i+1}. -Inf key is not needed.
239+
240+
There are couple of additional notes regarding K_{n+1} key.
241+
1) In entry tree rightmost page, a key coupled with P_n doesn't really matter.
242+
Highkey is assumed to be infinity.
243+
2) In posting tree, a key coupled with P_n always doesn't matter. Highkey for
244+
non-rightmost pages is stored separately and accessed via
245+
GinDataPageGetRightBound().
246+
218247
Posting tree
219248
------------
220249

@@ -277,50 +306,148 @@ followed by the packed items.
277306
Concurrency
278307
-----------
279308

280-
The entry tree and each posting tree is a B-tree, with right-links connecting
281-
sibling pages at the same level. This is the same structure that is used in
309+
The entry tree and each posting tree are B-trees, with right-links connecting
310+
sibling pages at the same level. This is the same structure that is used in
282311
the regular B-tree indexam (invented by Lehman & Yao), but we don't support
283-
scanning a GIN trees backwards, so we don't need left-links.
284-
285-
To avoid deadlocks, B-tree pages must always be locked in the same order:
286-
left to right, and bottom to top. When searching, the tree is traversed from
287-
top to bottom, so the lock on the parent page must be released before
288-
descending to the next level. Concurrent page splits move the keyspace to
289-
right, so after following a downlink, the page actually containing the key
290-
we're looking for might be somewhere to the right of the page we landed on.
291-
In that case, we follow the right-links until we find the page we're looking
292-
for.
293-
294-
To delete a page, the page's left sibling, the target page, and its parent,
295-
are locked in that order, and the page is marked as deleted. However, a
296-
concurrent search might already have read a pointer to the page, and might be
297-
just about to follow it. A page can be reached via the right-link of its left
298-
sibling, or via its downlink in the parent.
299-
300-
To prevent a backend from reaching a deleted page via a right-link, when
301-
following a right-link the lock on the previous page is not released until
302-
the lock on next page has been acquired.
303-
304-
The downlink is more tricky. A search descending the tree must release the
305-
lock on the parent page before locking the child, or it could deadlock with
306-
a concurrent split of the child page; a page split locks the parent, while
307-
already holding a lock on the child page. So, deleted page cannot be reclaimed
308-
immediately. Instead, we have to wait for every transaction, which might wait
309-
to reference this page, to finish. Corresponding processes must observe that
312+
scanning a GIN trees backwards, so we don't need left-links. The entry tree
313+
leaves don't have dedicated high keys, instead greatest leaf tuple serves as
314+
high key. That works because tuples are never deleted from the entry tree.
315+
316+
The algorithms used to operate entry and posting trees are considered below.
317+
318+
### Locating the leaf page
319+
320+
When we search for leaf page in GIN btree to perform a read, we descend from
321+
the root page to the leaf through using downlinks taking pin and shared lock on
322+
one page at once. So, we release pin and shared lock on previous page before
323+
getting them on the next page.
324+
325+
The picture below shows tree state after finding the leaf page. Lower case
326+
letters depicts tree pages. 'S' depicts shared lock on the page.
327+
328+
a
329+
/ | \
330+
b c d
331+
/ | \ | \ | \
332+
eS f g h i j k
333+
334+
### Steping right
335+
336+
Concurrent page splits move the keyspace to right, so after following a
337+
downlink, the page actually containing the key we're looking for might be
338+
somewhere to the right of the page we landed on. In that case, we follow the
339+
right-links until we find the page we're looking for.
340+
341+
During stepping right we take pin and shared lock on the right sibling before
342+
releasing them from the current page. This mechanism was designed to protect
343+
from stepping to delete page. We step to the right sibling while hold lock on
344+
the rightlink pointing there. So, it's guaranteed that nobody updates rightlink
345+
concurrently and doesn't delete right sibling accordingly.
346+
347+
The picture below shows two pages locked at once during stepping right.
348+
349+
a
350+
/ | \
351+
b c d
352+
/ | \ | \ | \
353+
eS fS g h i j k
354+
355+
### Insert
356+
357+
While finding appropriate leaf for insertion we also descend from the root to
358+
leaf, while shared locking one page at once in. But during insertion we don't
359+
release pins from root and internal pages. That could save us some lookups to
360+
the buffers hash table for downlinks insertion assuming parents are not changed
361+
due to concurrent splits. Once we reach leaf we re-lock the page in exclusive
362+
mode.
363+
364+
The picture below shows leaf page locked in exclusive mode and ready for
365+
insertion. 'P' and 'E' depict pin and exclusive lock correspondingly.
366+
367+
368+
aP
369+
/ | \
370+
b cP d
371+
/ | \ | \ | \
372+
e f g hE i j k
373+
374+
375+
If insert causes a page split, the parent is locked in exclusive mode before
376+
unlocking the left child. So, insertion algorithm can exclusively lock both
377+
parent and child pages at once starting from child.
378+
379+
The picture below shows tree state after leaf page split. 'q' is new page
380+
produced by split. Parent 'c' is about to have downlink inserted.
381+
382+
aP
383+
/ | \
384+
b cE d
385+
/ | \ / | \ | \
386+
e f g hE q i j k
387+
388+
389+
### Page deletion
390+
391+
Vacuum never deletes tuples or pages from the entry tree. It traverses entry
392+
tree leafs in logical order by rightlinks and removes deletable TIDs from
393+
posting lists. Posting trees are processed by links from entry tree leafs. They
394+
are vacuumed in two stages.
395+
396+
At first stage, ginVacuumPostingTreeLeaves() removes deletable TIDs are removed
397+
from leafs. ginVacuumPostingTreeLeaves() traverses the whole tree in depth-first
398+
manner. It starts from the super-exclusive lock on the tree root. This lock
399+
prevents all the concurrent insertions into this tree while we're deleting
400+
pages. However, there are still might be some in-progress readers, who traversed
401+
root before we locked it.
402+
403+
The picture below shows tree during removing deletable TIDs from leftmost tree
404+
lead.
405+
406+
aE
407+
/ | \
408+
bE c d
409+
/ | \ | \ | \
410+
eE f g h i j k
411+
412+
ginVacuumPostingTreeLeaves() algorithm keeps exclusive locks on pages comprising
413+
currently investigated path.
414+
415+
If first stage detects at least one empty page, then at the second stage
416+
ginScanToDelete() deletes empty pages. ginScanToDelete() keeps lock on the root
417+
acquired by ginVacuumPostingTreeLeaves(). It also traverses the whole tree in
418+
depth-first manner, but keeps one page exclusively locked at once. That's safe
419+
because root lock guarantees there is no concurrent page modifications. When
420+
page is about to be deleted, pages are relocked in following order: left,
421+
deletable, parent. This order guarantees no deadlock with concurrent stepping
422+
right.
423+
424+
The picture below shows tree state before deletion of page 'g'.
425+
426+
aE
427+
/ | \
428+
bE c d
429+
/ | \ | \ | \
430+
e fE gE h i j k
431+
432+
A search concurrent to page deletion might already have read a pointer to the
433+
page to be deleted, and might be just about to follow it. A page can be reached
434+
via the right-link of its left sibling, or via its downlink in the parent.
435+
436+
To prevent a backend from reaching a deleted page via a right-link, stepping
437+
right algorithm doesn't release lock on the current page until lock of the
438+
right page is acquired.
439+
440+
The downlink is more tricky. A search descending the tree must release the lock
441+
on the parent page before locking the child, or it could deadlock with a
442+
concurrent split of the child page; a page split locks the parent, while already
443+
holding a lock on the child page. So, deleted page cannot be reclaimed
444+
immediately. Instead, we have to wait for every transaction, which might wait
445+
to reference this page, to finish. Corresponding processes must observe that
310446
the page is marked deleted and recover accordingly.
311447

312-
The previous paragraph's reasoning only applies to searches, and only to
313-
posting trees. To protect from inserters following a downlink to a deleted
314-
page, vacuum simply locks out all concurrent insertions to the posting tree,
315-
by holding a super-exclusive lock on the posting tree root. Inserters hold a
316-
pin on the root page, but searches do not, so while new searches cannot begin
317-
while root page is locked, any already-in-progress scans can continue
318-
concurrently with vacuum. In the entry tree, we never delete pages.
319-
320-
(This is quite different from the mechanism the btree indexam uses to make
321-
page-deletions safe; it stamps the deleted pages with an XID and keeps the
322-
deleted pages around with the right-link intact until all concurrent scans
323-
have finished.)
448+
During the replay of page deletion at standby, the page's left sibling, the
449+
target page, and its parent, are locked in that order. This order guarantees
450+
no deadlock with concurrent reads.
324451

325452
Compatibility
326453
-------------

0 commit comments

Comments
 (0)