Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wSCzP-0031zo-01 for pgsql-hackers@arkaria.postgresql.org; Wed, 27 May 2026 12:03:59 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wSCzM-0088BQ-2A for pgsql-hackers@arkaria.postgresql.org; Wed, 27 May 2026 12:03:57 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wSCzM-0088BH-0p for pgsql-hackers@lists.postgresql.org; Wed, 27 May 2026 12:03:57 +0000 Received: from mail-ed1-f48.google.com ([209.85.208.48]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wSCzK-000000010dX-3gRW for pgsql-hackers@postgresql.org; Wed, 27 May 2026 12:03:56 +0000 Received: by mail-ed1-f48.google.com with SMTP id 4fb4d7f45d1cf-689be822f34so4873596a12.0 for ; Wed, 27 May 2026 05:03:54 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1779883433; cv=none; d=google.com; s=arc-20240605; b=Rlm8GQko6ut3H1X1xZozPYFFDqiOUD7T677Kds4aNB59o0eetuc2NhtGRtZvLxBktR UpOsxGy1JyHwHS8HfYCE7ck/9nGRL9QtWYuHg1QKNsYPzCPHcqxqE9A9trjQB/VT547Q R5Ie7TQg1Al5hz/p9b5+lGuaEypUI775SliEvsMlZZCXf1Acl1XF3QI/yRRmltNPt+Dr GofC8HMSRby5QAmjUX5D7ZVBE6aspTUY3lo5CStlPqVkSDZr/C1cQnExrD6Hj6f34hZ7 /H66auGqYze8XytwBNoRm94vdSLRLZWR+TleQNJCx/rqJCye4/XrCCdom9qaD/UfSeYV 7/Uw== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version; bh=3DATirnctgkvn3t+cPRR2i4AT76MTxsfvRELD7hjIkk=; fh=Ldd/YJ+V81CtE7ta2/LIWPK60RpIzYUwo+idw+Oz/yU=; b=F0/rXWlewb3no1NWrOboYP9TK2EwCIBJvzeZEjRr1M0vvNikubHePBFWz5/vqDkLOC 1QnlmE9sZoxnWNNT42ind3ymyV/D9VVtWp2DLWqxKiOH7X4qW1eO0tTwcmmoL1/egHVy tc8Zj6fgk8a+oK4rFHXizdAnaCBVG0wYzbY0828SjFOvOq1v1IJmBBS1H1AmdasMhWs1 7PFalTna0euBIQSp4HhO25KpVasn2A1hSZZtFmhPUEVQ8lX+u5ALReY4XYKpRLoKZle4 tD3TVZDcePxv+/xXDVImVL74lim4/LIZmk7EOvBcBEcX9qfrV332DfypDdpRtfHfLY42 dB1g==; darn=postgresql.org ARC-Authentication-Results: i=1; mx.google.com; arc=none X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1779883433; x=1780488233; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=3DATirnctgkvn3t+cPRR2i4AT76MTxsfvRELD7hjIkk=; b=V7ulqDioVHjrRwK0Z91O6p6wBrcV6srTKtYLaIqn4ooJxfprGrb1clZ9xAwUEp1l46 NXYHD06xAt9DXDype8g2ak4W59MLMyoT6p/SlpzrRmSwZ2VvT6ah5M4zxAPExPDHuZfe h8DCslpTFqGw6OBgfNjnTrasoY63W5XFNdI+aHuslbo4ihnZZASjzD8FzOCSl/saor9H ltbSm6Q9DV9LwoBOP2DiFG5fXrSL845yFynqo0k6065kzd0GjZglUt/eUmugSGKUSuo2 Ut6g8d04Gva0YhnyIP6Uhr321Pp3jckmvp+9HeIoa1i4K3DYxWhWiE6EtuhlaBhO+eF8 sQbw== X-Forwarded-Encrypted: i=1; AFNElJ9dGrMbKsHMPqw9DDQ130PnIFv+XHCYNK2tywu/VjRzM9f20BCujHecIXpHbKWUCkeI7xRWh0syLUAdvQF+@postgresql.org X-Gm-Message-State: AOJu0Yyps7SVXVsYnxy8FtcPqYPwfPocgcUpi1VdY9jEt87hEwltzPHg NDgkWO+2prz5a3wCowfF56HzfVL/W0AUKOu/Xea0DDfV0pqfNMI3HD5+w8A4NQPnTZRuqRw/PHa fuBrfG8YvPLV67CCSfSdDi2siT8tKJ+4= X-Gm-Gg: Acq92OGbR9KzT6LSpj5HAw3X6ArYqNe7KbcKRpmNZ/incS3nWKOMaanrS/qstfiBGXS SOgfkTQSEJgHOzJkdMipcnu2CW6Pd64KTXjFLwHybY0hK681QGw1FJVmiFd/eV9WkQeh7oqZI5M DcBNjjnmPaBswcTeO6yHzQaxA26OeJzMDacurW68Vgh/sr5EDQWc/HX03ewdONVjN7QgfkS61tF 0nrgFPAzXp47v+7E5bnofcXmWkr1rVcizelZv+MUS7vsB6tR1nhVM3TwxCf6IYbbcSbyN1DLDic XP49TQQ= X-Received: by 2002:a05:6402:5488:b0:689:b30c:4205 with SMTP id 4fb4d7f45d1cf-689b30c42bdmr6391822a12.0.1779883432099; Wed, 27 May 2026 05:03:52 -0700 (PDT) MIME-Version: 1.0 References: <54c35fb9-da3a-4754-ab8c-46ed0b612465@vondra.me> <684c70d7-180e-461d-9377-600c2db581ba@vondra.me> <605328.1747710381@sss.pgh.pa.us> <691261.1747755511@sss.pgh.pa.us> In-Reply-To: From: Thom Brown Date: Wed, 27 May 2026 13:03:24 +0100 X-Gm-Features: AVHnY4K85REb9zeuzvobOUVnpy8fEk3bVhd2WpuSdSsvdu_xvh8aXeoVAws8t60 Message-ID: Subject: Re: generic plans and "initial" pruning To: Amit Langote Cc: Chao Li , Tom Lane , Tender Wang , Alexander Lakhin , Tomas Vondra , Robert Haas , Alvaro Herrera , Andres Freund , Daniel Gustafsson , David Rowley , PostgreSQL Hackers Content-Type: text/plain; charset="UTF-8" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk On Sat, 4 Apr 2026 at 13:11, Amit Langote wrote: > > Attached is a redesigned version. While working on the previous > design, I grew increasingly uncomfortable with CachedPlanPrepData -- > it was smuggling executor state out of GetCachedPlan() through an > out-parameter, which papered over the real problem: GetCachedPlan() > was doing too much. The main change in this version is architectural: > GetCachedPlan() no longer acquires execution locks. Callers now own > that responsibility, which is natural because each call site iterates > stmt_list differently and manages execution state in its own way -- > and it lets them choose between conservative lock-all and > pruning-aware locking where appropriate. > > Non-portal call sites remain on the conservative path for now. > _SPI_execute_plan requires care around snapshot setup, which happens > after plan fetch rather than before. SQL functions have a different > issue: init_execution_state() fetches the plan while postquel_start() > handles execution, with execution_state containers in between, making > it harder to thread a prepped QueryDesc through. The portal path and > EXPLAIN EXECUTE cover the most common > prepared-statement-with-partitions workloads; the remaining sites can > be converted incrementally. > > This is now starting to feel closer to what Tom suggested back in > January 2023 [1], where he proposed getting rid of > AcquireExecutorLocks() inside GetCachedPlan() entirely and pushing > lock acquisition out to callers. He noted that "we'd be pushing the > responsibility for looping back and re-planning out to fairly > high-level calling code" and that "we'd definitely be changing some > fundamental APIs." That is the direction I came around to over the > last couple of weeks while wrestling with CachedPlanPrepData. The > reverted approach also tried to follow Tom's direction but moved > locking into ExecutorStart(), which forced it to handle plan > invalidation from inside the executor by mutating the CachedPlan > in-place. This version moves locking out to the callers instead, so > the executor and plan cache never reach into each other. > > The series is now four patches: > > 0001: Move execution lock acquisition out of GetCachedPlan(). Adds > AcquireExecutorLocks() as a caller-facing function with validity check > and retry. Adds PortalLockCachedPlan() in pquery.c to centralize the > portal retry logic. All callers are converted. No behavioral change. > > 0002: Refactor executor's initial partition pruning setup. Cleanup > only, no behavioral change. > > 0003: Introduce ExecutorPrep() and refactor executor startup. Factors > range table init, permission checks, and initial pruning out of > InitPlan(). Scaffolding for 0004; all callers still go through the > normal ExecutorStart() path. > > 0004: Use pruning-aware locking for single-statement cached plans. > Adds ExecutorPrepAndLock() which locks unprunable relations, runs > ExecutorPrep() to determine surviving partitions, then locks only > those. Extends PortalLockCachedPlan() with a pruning-aware path for > eligible plans. Multi-statement CachedPlans (from rule rewriting) > always use conservative locking. In principle, this could be relaxed > if the planner can prove that no pruning expression reads state > modified by an earlier statement, but that is left for a future patch. > Includes regression tests. > > In case it's not clear, I'm not targeting v19 at this point. I'd like > to get this into v20 CF1 and would welcome review from anyone > interested. After not having looked at this in close to 2 years, I thought I'd give it another look. Not found any user-facing issues, and I'm liking seeing so few locks in pg_locks. I can see that with pruning disabled, the fallback works, pruning-aware locking is working via SPI through plpgsql, running ALTER between executions and also invalidating indexes force replans, and it's looking good. But I also think there might be a bug in patch 0001, but I'd appreciate checking my reasoning because I'm not fully confident I've been diligent enough. When PortalStart() opens a SELECT cursor that's backed by a cached plan, it does roughly the following. It builds a queryDesc (an executor-side struct), one of whose fields is a pointer into the plan tree inside the portal's cached plan. Then it calls PortalLockCachedPlan() to acquire the necessary locks, and finally hands the queryDesc over to the executor. My worry is about what happens if the cached plan turns out to be stale, for instance because someone ran DDL on a referenced table. In that case PortalLockCachedPlan() throws the old plan away (via ReleaseCachedPlan) and fetches a freshly-built replacement, updtating the portal's own pointers to match. But the queryDesc from earlier isn't touched. Its plan pointer still references the old, now-released plan. From what I can see, once that old plan's last reference is dropped its memory can be freed, which would leave the executor reading from freed memory in the next step. The bit I'm least sure about is whether the old plan's memory really does get reclaimed straight away when its refcount hits zero. If something keeps it alive longer then this isn't a bug, or at least not as bad as I'm making out. I had a look but couldn't convince myself either way from the code alone. To actually hit this you'd need a cursor on a cached plan, plus an invalidation arriving in the small window between the portal being set up and the cursor being opened. The race condition is brief, and I've not been able to hit it in testing. The thing that got me thinking this is real: patch 0004 modifies PortalLockCachedPlan() so that whenever it replans, it also rebuilds the queryDesc. That's pretty much the fix I'd expect for this, which makes me suspect somebody hit it at some point. But 0004 only applies that fix on the new pruning-aware code path, and it was mentioned in the thread that 0001 to 0003 might land before 0004. If so, master would carry the bug in the gap between the two. I suspect a way to deal with it would be to move the CreateQueryDesc call in the SELECT case to after PortalLockCachedPlan() returns, which is what the other portal strategies already seem to do. Alternatively, you could bring 0004's changes in this area into 0001 and have PortalLockCachedPlan() always rebuild the queryDesc when it replans. If I've got this wrong and there's some lifetime mechanism I missed that keeps the old plan's memory alive, then it's a non-issue and I'm misreading the code. If I have got it wrong, could you please add comments to make what is going on clearer? Regards Thom