Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1pjPQa-000692-GZ for pgsql-hackers@arkaria.postgresql.org; Mon, 03 Apr 2023 19:01:17 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.92) (envelope-from ) id 1pjPQZ-0000z9-Gl for pgsql-hackers@arkaria.postgresql.org; Mon, 03 Apr 2023 19:01:15 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1pjPQ2-00032r-D5 for pgsql-hackers@lists.postgresql.org; Mon, 03 Apr 2023 19:00:43 +0000 Received: from out5-smtp.messagingengine.com ([66.111.4.29]) by makus.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1pjPPx-0006yl-1F for pgsql-hackers@postgresql.org; Mon, 03 Apr 2023 19:00:41 +0000 Received: from compute1.internal (compute1.nyi.internal [10.202.2.41]) by mailout.nyi.internal (Postfix) with ESMTP id F213B5C01E9; Mon, 3 Apr 2023 15:00:32 -0400 (EDT) Received: from mailfrontend2 ([10.202.2.163]) by compute1.internal (MEProxy); Mon, 03 Apr 2023 15:00:32 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=anarazel.de; h= cc:cc:content-type:content-type:date:date:from:from:in-reply-to :in-reply-to:message-id:mime-version:references:reply-to:sender :subject:subject:to:to; s=fm2; t=1680548432; x=1680634832; bh=qH td/g0HD1FueZLRrHdaexdTbJg6MBLbIlqkpBvU0+4=; b=SSvUVBra3xm5XxAsF/ Q/af1TEt9IaHHkf7Kq+AlQ4UgC4il3uJGL0+cF6910eXh+4WYFB8xFlMm6jKcidr diL/0xXZz33cl4Hqnk5jxYWrNnfuJkLLdSrzDV+mzQQ5NTWzGP+7WdH8eZUp02dW Yf596nBhJUnu5swAiW6hprtp/TVOJUyxFDpQd3jSsrSHgtC7beTcxzrYLuLetbZC rc0PRrj6KjLyN1Sh4+qD/GEMPnge8n3gSoCmxMGdwcl24FFzjRDteDXkk9zAmFIf 0GvU1ACAADu4fFug13qcWBOLPPiye3Gx9YYmYpU3QjkVGsQIZqRdEw/RsMldnkAV BUSg== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:cc:content-type:content-type:date:date :feedback-id:feedback-id:from:from:in-reply-to:in-reply-to :message-id:mime-version:references:reply-to:sender:subject :subject:to:to:x-me-proxy:x-me-proxy:x-me-sender:x-me-sender :x-sasl-enc; s=fm2; t=1680548432; x=1680634832; bh=qHtd/g0HD1Fue ZLRrHdaexdTbJg6MBLbIlqkpBvU0+4=; b=YC0mj2uw3b7xsB3oyun4xD2U2iBes qBRMPsbYe4U5tvmImbfsWXhLjHt7zwPUwS1dfnJpIk5wESzWMWcESc0S42WWU5zg N5Q7B4i/5/REwOuBjKEK2zQRTOb+9P1WTZ+0Nq+YxEeAxTBhcjDq1Haqet8mnLPh kqX7UWadBfAMNY8voUpXfWWHK1k7PzxKEtwwjaB1ChDxi+CnaHoWudqO3+Ac7MmW 3zndL2r8tpcNOyoWYmetb/QRvT5BFEpexrGAKoNu0rIEymW1+2csDDxefLGD0RjK auKIZx/n9SccdvCrQ0257sV2609Uw++M+OmKpevgePauud9b0Ru+iKwww== X-ME-Sender: X-ME-Received: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedvhedrvdeijedgudeffecutefuodetggdotefrod ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfgfvfdpuffrtefokffrpgfnqfgh necuuegrihhlohhuthemuceftddtnecusecvtfgvtghiphhivghnthhsucdlqddutddtmd enucfjughrpeffhffvvefukfhfgggtuggjsehmtderredttddvnecuhfhrohhmpeetnhgu rhgvshcuhfhrvghunhguuceorghnughrvghssegrnhgrrhgriigvlhdruggvqeenucggtf frrghtthgvrhhnpeekieeugeevudeguedtieefffeivdfgueegkefhfedtveefkefflefg geevhfdujeenucevlhhushhtvghrufhiiigvpedtnecurfgrrhgrmhepmhgrihhlfhhroh hmpegrnhgurhgvshesrghnrghrrgiivghlrdguvg X-ME-Proxy: Feedback-ID: id4a34324:Fastmail Received: by mail.messagingengine.com (Postfix) with ESMTPA; Mon, 3 Apr 2023 15:00:32 -0400 (EDT) Date: Mon, 3 Apr 2023 12:00:30 -0700 From: Andres Freund To: Tomas Vondra Cc: Tom Lane , pgsql-hackers@postgresql.org, Tomas Vondra , Anastasia Lubennikova , Pavan Deolasee , Jeff Janes Subject: Re: hio.c does visibilitymap_pin()/IO while holding buffer lock Message-ID: <20230403190030.fk2frxv6faklrseb@awork3.anarazel.de> References: <20230325025740.wzvchp2kromw4zqz@awork3.anarazel.de> <0c11032a-d91b-8875-ab7b-4554776390c5@enterprisedb.com> <20230325163903.eu67kqcqo4llhc4u@awork3.anarazel.de> <2602455.1679763437@sss.pgh.pa.us> <20230325181707.at3kwsh7uvkhfype@awork3.anarazel.de> <20230329012102.zpviu2e7kyfrovxn@awork3.anarazel.de> <20230329021721.l3rujmv3jjmktt5i@awork3.anarazel.de> <20230402224024.aealekgu2df4cnhs@awork3.anarazel.de> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="rlua5hhgdcrdaj2n" Content-Disposition: inline In-Reply-To: List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --rlua5hhgdcrdaj2n Content-Type: text/plain; charset=us-ascii Content-Disposition: inline Hi, On 2023-04-03 14:25:59 +0200, Tomas Vondra wrote: > On 4/3/23 00:40, Andres Freund wrote: > > Hi, > > > > On 2023-03-28 19:17:21 -0700, Andres Freund wrote: > >> On 2023-03-28 18:21:02 -0700, Andres Freund wrote: > >>> Here's a draft patch. > >> > >> Attached is v2, with a stupid bug fixed and a bit of comment / pgindent > >> polish. > > > > I'd welcome some review (Tomas?), but otherwise I'm planning to push ahead > > with this. > > > > I guess the 0001 part was already pushed, so I should be looking only at > 0002, correct? Yes. > I think 0002 makes RelationGetBufferForTuple() harder to understand. I'm > not saying it's incorrect, but I find it hard to reason about the new > combinations of conditions :-( > I mean, it only had this condition: > > if (otherBuffer != InvalidBuffer) > { > ... > } > > but now it have > > if (unlockedTargetBuffer) > { > ... > } > else if (otherBuffer != InvalidBuffer) > { > ... > } > > if (unlockedTargetBuffer || otherBuffer != InvalidBuffer) > { > ... > } > > Not sure how to improve that :-/ but not exactly trivial to figure out > what's going to happen. It's not great, I agree. I tried to make it easier to read in this version by a) changing GetVisibilityMapPins() as I proposed b) added a new variable "recheckVmPins", that gets set in if (unlockedTargetBuffer) and if (otherBuffer != InvalidBuffer) c) reformulated comments > Maybe this > > * If we unlocked the target buffer above, it's unlikely, but possible, > * that another backend used space on this page. > > might say what we're going to do in this case. I mean - I understand > some backend may use space in unlocked page, but what does that mean for > this code? What is it going to do? (The same comment talks about the > next condition in much more detail, for example.) There's a comment about that detail further below. But you're right, it wasn't clear as-is. How about now? > Also, isn't it a bit strange the free space check now happens outside > any if condition? It used to happen in the > > if (otherBuffer != InvalidBuffer) > { > ... > } > > block, but now it happens outside. Well, the alternative is to repeat it in the two branches, which doesn't seem great either. Particularly because there'll be a third branch after the bulk extension patch. Greetings, Andres Freund --rlua5hhgdcrdaj2n Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v3-0001-hio-Relax-rules-for-calling-GetVisibilityMapPins.patch" From 49183283d6701d22cf12f6a6280ed1aba02fc063 Mon Sep 17 00:00:00 2001 From: Andres Freund Date: Mon, 3 Apr 2023 11:18:10 -0700 Subject: [PATCH v3 1/2] hio: Relax rules for calling GetVisibilityMapPins() Author: Reviewed-by: Discussion: https://postgr.es/m/ Backpatch: --- src/backend/access/heap/hio.c | 37 ++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/backend/access/heap/hio.c b/src/backend/access/heap/hio.c index 7479212d4e0..e8aea42f8a9 100644 --- a/src/backend/access/heap/hio.c +++ b/src/backend/access/heap/hio.c @@ -131,9 +131,9 @@ ReadBufferBI(Relation relation, BlockNumber targetBlock, * For each heap page which is all-visible, acquire a pin on the appropriate * visibility map page, if we haven't already got one. * - * buffer2 may be InvalidBuffer, if only one buffer is involved. buffer1 - * must not be InvalidBuffer. If both buffers are specified, block1 must - * be less than block2. + * To avoid complexity in the callers, either buffer1 or buffer2 may be + * InvalidBuffer if only one buffer is involved. For the same reason, block2 + * may be smaller than block1. */ static void GetVisibilityMapPins(Relation relation, Buffer buffer1, Buffer buffer2, @@ -143,6 +143,26 @@ GetVisibilityMapPins(Relation relation, Buffer buffer1, Buffer buffer2, bool need_to_pin_buffer1; bool need_to_pin_buffer2; + /* + * Swap buffers around to handle case of a single block/buffer, and to + * handle if lock ordering rules require to lock block2 first. + */ + if (!BufferIsValid(buffer1) || + (BufferIsValid(buffer2) && block1 > block2)) + { + Buffer tmpbuf = buffer1; + Buffer *tmpvmbuf = vmbuffer1; + BlockNumber tmpblock = block1; + + buffer1 = buffer2; + vmbuffer1 = vmbuffer2; + block1 = block2; + + buffer2 = tmpbuf; + vmbuffer2 = tmpvmbuf; + block2 = tmpblock; + } + Assert(BufferIsValid(buffer1)); Assert(buffer2 == InvalidBuffer || block1 <= block2); @@ -502,14 +522,9 @@ loop: * done a bit of extra work for no gain, but there's no real harm * done. */ - if (otherBuffer == InvalidBuffer || targetBlock <= otherBlock) - GetVisibilityMapPins(relation, buffer, otherBuffer, - targetBlock, otherBlock, vmbuffer, - vmbuffer_other); - else - GetVisibilityMapPins(relation, otherBuffer, buffer, - otherBlock, targetBlock, vmbuffer_other, - vmbuffer); + GetVisibilityMapPins(relation, buffer, otherBuffer, + targetBlock, otherBlock, vmbuffer, + vmbuffer_other); /* * Now we can check to see if there's enough free space here. If so, -- 2.38.0 --rlua5hhgdcrdaj2n Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v3-0002-hio-Don-t-pin-the-VM-while-holding-buffer-lock-wh.patch" From 27f9ace30d9862f05b73676526a1b0233ef1faea Mon Sep 17 00:00:00 2001 From: Andres Freund Date: Mon, 3 Apr 2023 11:26:16 -0700 Subject: [PATCH v3 2/2] hio: Don't pin the VM while holding buffer lock while extending Discussion: http://postgr.es/m/20230325025740.wzvchp2kromw4zqz@awork3.anarazel.de --- src/backend/access/heap/hio.c | 126 +++++++++++++++++++++++----------- 1 file changed, 85 insertions(+), 41 deletions(-) diff --git a/src/backend/access/heap/hio.c b/src/backend/access/heap/hio.c index e8aea42f8a9..15912f6415f 100644 --- a/src/backend/access/heap/hio.c +++ b/src/backend/access/heap/hio.c @@ -134,14 +134,17 @@ ReadBufferBI(Relation relation, BlockNumber targetBlock, * To avoid complexity in the callers, either buffer1 or buffer2 may be * InvalidBuffer if only one buffer is involved. For the same reason, block2 * may be smaller than block1. + * + * Returns whether buffer locks were temporarily released. */ -static void +static bool GetVisibilityMapPins(Relation relation, Buffer buffer1, Buffer buffer2, BlockNumber block1, BlockNumber block2, Buffer *vmbuffer1, Buffer *vmbuffer2) { bool need_to_pin_buffer1; bool need_to_pin_buffer2; + bool released_locks = false; /* * Swap buffers around to handle case of a single block/buffer, and to @@ -175,9 +178,10 @@ GetVisibilityMapPins(Relation relation, Buffer buffer1, Buffer buffer2, && PageIsAllVisible(BufferGetPage(buffer2)) && !visibilitymap_pin_ok(block2, *vmbuffer2); if (!need_to_pin_buffer1 && !need_to_pin_buffer2) - return; + break; /* We must unlock both buffers before doing any I/O. */ + released_locks = true; LockBuffer(buffer1, BUFFER_LOCK_UNLOCK); if (buffer2 != InvalidBuffer && buffer2 != buffer1) LockBuffer(buffer2, BUFFER_LOCK_UNLOCK); @@ -203,6 +207,8 @@ GetVisibilityMapPins(Relation relation, Buffer buffer1, Buffer buffer2, || (need_to_pin_buffer1 && need_to_pin_buffer2)) break; } + + return released_locks; } /* @@ -365,6 +371,8 @@ RelationGetBufferForTuple(Relation relation, Size len, BlockNumber targetBlock, otherBlock; bool needLock; + bool unlockedTargetBuffer; + bool recheckVmPins; len = MAXALIGN(len); /* be conservative */ @@ -645,6 +653,9 @@ loop: if (needLock) UnlockRelationForExtension(relation, ExclusiveLock); + unlockedTargetBuffer = false; + targetBlock = BufferGetBlockNumber(buffer); + /* * We need to initialize the empty new page. Double-check that it really * is empty (this should never happen, but if it does we don't want to @@ -654,75 +665,108 @@ loop: if (!PageIsNew(page)) elog(ERROR, "page %u of relation \"%s\" should be empty but is not", - BufferGetBlockNumber(buffer), + targetBlock, RelationGetRelationName(relation)); PageInit(page, BufferGetPageSize(buffer), 0); MarkBufferDirty(buffer); /* - * The page is empty, pin vmbuffer to set all_frozen bit. + * The page is empty, pin vmbuffer to set all_frozen bit. We don't want to + * do IO while the buffer is locked, so we unlock the page first if IO is + * needed (necessitating checks below). */ if (options & HEAP_INSERT_FROZEN) { - Assert(PageGetMaxOffsetNumber(BufferGetPage(buffer)) == 0); - visibilitymap_pin(relation, BufferGetBlockNumber(buffer), vmbuffer); + Assert(PageGetMaxOffsetNumber(page) == 0); + + if (!visibilitymap_pin_ok(targetBlock, *vmbuffer)) + { + unlockedTargetBuffer = true; + LockBuffer(buffer, BUFFER_LOCK_UNLOCK); + visibilitymap_pin(relation, targetBlock, vmbuffer); + } } /* - * Lock the other buffer. It's guaranteed to be of a lower page number - * than the new page. To conform with the deadlock prevent rules, we ought - * to lock otherBuffer first, but that would give other backends a chance - * to put tuples on our page. To reduce the likelihood of that, attempt to - * lock the other buffer conditionally, that's very likely to work. - * Otherwise we need to lock buffers in the correct order, and retry if - * the space has been used in the mean time. + * Reacquire locks if necessary. * - * Alternatively, we could acquire the lock on otherBuffer before - * extending the relation, but that'd require holding the lock while - * performing IO, which seems worse than an unlikely retry. + * If the target buffer was unlocked above, or is unlocked while + * reacquiring the lock on otherBuffer below, it's unlikely, but possible, + * that another backend used space on this page. We check for that below, + * and retry if necessary. */ - if (otherBuffer != InvalidBuffer) + recheckVmPins = false; + if (unlockedTargetBuffer) { + /* released lock on target buffer above */ + if (otherBuffer != InvalidBuffer) + LockBuffer(otherBuffer, BUFFER_LOCK_EXCLUSIVE); + LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE); + recheckVmPins = true; + } + else if (otherBuffer != InvalidBuffer) + { + /* + * We did not release the target buffer, and otherBuffer is valid, + * need to lock the other buffer. It's guaranteed to be of a lower + * page number than the new page. To conform with the deadlock + * prevent rules, we ought to lock otherBuffer first, but that would + * give other backends a chance to put tuples on our page. To reduce + * the likelihood of that, attempt to lock the other buffer + * conditionally, that's very likely to work. + * + * Alternatively, we could acquire the lock on otherBuffer before + * extending the relation, but that'd require holding the lock while + * performing IO, which seems worse than an unlikely retry. + */ Assert(otherBuffer != buffer); - targetBlock = BufferGetBlockNumber(buffer); Assert(targetBlock > otherBlock); if (unlikely(!ConditionalLockBuffer(otherBuffer))) { + unlockedTargetBuffer = true; LockBuffer(buffer, BUFFER_LOCK_UNLOCK); LockBuffer(otherBuffer, BUFFER_LOCK_EXCLUSIVE); LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE); } + recheckVmPins = true; + } - /* - * Because the buffers were unlocked for a while, it's possible, - * although unlikely, that an all-visible flag became set or that - * somebody used up the available space in the new page. We can use - * GetVisibilityMapPins to deal with the first case. In the second - * case, just retry from start. - */ - GetVisibilityMapPins(relation, otherBuffer, buffer, - otherBlock, targetBlock, vmbuffer_other, - vmbuffer); + /* + * If one of the buffers was unlocked (always the case if otherBuffer is + * valid), it's possible, although unlikely, that an all-visible flag + * became set. We can use GetVisibilityMapPins to deal with that. It's + * possible that GetVisibilityMapPins() might need to temporarily release + * buffer locks, in which case we'll need to check if there's still enough + * space on the page below. + */ + if (recheckVmPins) + { + if (GetVisibilityMapPins(relation, otherBuffer, buffer, + otherBlock, targetBlock, vmbuffer_other, + vmbuffer)) + unlockedTargetBuffer = true; + } - /* - * Note that we have to check the available space even if our - * conditional lock succeeded, because GetVisibilityMapPins might've - * transiently released lock on the target buffer to acquire a VM pin - * for the otherBuffer. - */ - if (len > PageGetHeapFreeSpace(page)) + /* + * If the target buffer was temporarily unlocked since the relation + * extension, it's possible, although unlikely, that all the space on the + * page was already used. If so, we just retry from the start. If we + * didn't unlock, something has gone wrong if there's not enough space - + * the test at the top should have prevented reaching this case. + */ + pageFreeSpace = PageGetHeapFreeSpace(page); + if (len > pageFreeSpace) + { + if (unlockedTargetBuffer) { - LockBuffer(otherBuffer, BUFFER_LOCK_UNLOCK); + if (otherBuffer != InvalidBuffer) + LockBuffer(otherBuffer, BUFFER_LOCK_UNLOCK); UnlockReleaseBuffer(buffer); goto loop; } - } - else if (len > PageGetHeapFreeSpace(page)) - { - /* We should not get here given the test at the top */ elog(PANIC, "tuple is too big: size %zu", len); } @@ -735,7 +779,7 @@ loop: * current backend to make more insertions or not, which is probably a * good bet most of the time. So for now, don't add it to FSM yet. */ - RelationSetTargetBlock(relation, BufferGetBlockNumber(buffer)); + RelationSetTargetBlock(relation, targetBlock); return buffer; } -- 2.38.0 --rlua5hhgdcrdaj2n--