v19-0005-Move-async-infrastructure-into-shared-AppendBase.patch

application/octet-stream

Filename: v19-0005-Move-async-infrastructure-into-shared-AppendBase.patch
Type: application/octet-stream
Part: 3
Message: Re: Asynchronous MergeAppend

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: format-patch
Series: patch v19-0005
Subject: Move async infrastructure into shared AppendBase functions
File+
src/backend/executor/execAppend.c 256 3
src/backend/executor/nodeAppend.c 10 230
src/backend/executor/nodeMergeAppend.c 4 1
src/include/executor/execAppend.h 7 0
From 89b1264a6bfb0592b3d84bc723ce2904e4cc0093 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Sun, 5 Apr 2026 04:42:32 +0300
Subject: [PATCH v19 5/6] Move async infrastructure into shared AppendBase
 functions

Move all async-related code from nodeAppend.c into the shared
execAppend.c, preparing MergeAppend to support async foreign scan
subplans.

  - ExecInitAppendBase() now detects async-capable subplans and allocates
    async request/result state.
  - ExecReScanAppendBase() now resets async request state.
  - ExecAppendBaseAsyncBegin(): fire async requests (moved from
    nodeAppend.c's ExecAppendAsyncBegin).
  - ExecAppendBaseAsyncEventWait(): wait/poll for async events (moved
    from nodeAppend.c's ExecAppendAsyncEventWait).
  - classify_matching_subplans_common(): new helper to split valid
    subplans into sync and async sets.
  - MergeAppend now uses valid_subplans_identified flag instead of
    checking valid_subplans == NULL.

Discussion: https://postgr.es/m/59be194c5a409fb9fc9f2031581b8a44%40postgrespro.ru
Author: Matheus Alcantara <matheusssilv97@gmail.com>
Co-authored-by: Alexander Korotkov <aekorotkov@gmail.com>
Reviewed-by: Alexander Pyhalov <a.pyhalov@postgrespro.ru>
Reviewed-by: Alena Rybakina <a.rybakina@postgrespro.ru>
---
 src/backend/executor/execAppend.c      | 259 ++++++++++++++++++++++++-
 src/backend/executor/nodeAppend.c      | 240 +----------------------
 src/backend/executor/nodeMergeAppend.c |   5 +-
 src/include/executor/execAppend.h      |   7 +
 4 files changed, 277 insertions(+), 234 deletions(-)

diff --git a/src/backend/executor/execAppend.c b/src/backend/executor/execAppend.c
index 9599d10a952..cc3b368eb7b 100644
--- a/src/backend/executor/execAppend.c
+++ b/src/backend/executor/execAppend.c
@@ -14,8 +14,14 @@
 #include "postgres.h"
 
 #include "executor/execAppend.h"
+#include "executor/execAsync.h"
 #include "executor/execPartition.h"
 #include "executor/executor.h"
+#include "miscadmin.h"
+#include "storage/latch.h"
+#include "storage/waiteventset.h"
+
+#define EVENT_BUFFER_SIZE			16
 
 /*  Begin all of the subscans of an AppendBase node. */
 void
@@ -29,7 +35,9 @@ ExecInitAppendBase(AppendBaseState *state,
 	PlanState **appendplanstates;
 	const TupleTableSlotOps *appendops;
 	Bitmapset  *validsubplans;
+	Bitmapset  *asyncplans;
 	int			nplans;
+	int			nasyncplans;
 	int			firstvalid;
 	int			i,
 				j;
@@ -87,12 +95,25 @@ ExecInitAppendBase(AppendBaseState *state,
 	 * While at it, find out the first valid partial plan.
 	 */
 	j = 0;
+	asyncplans = NULL;
+	nasyncplans = 0;
 	firstvalid = nplans;
 	i = -1;
 	while ((i = bms_next_member(validsubplans, i)) >= 0)
 	{
 		Plan	   *initNode = (Plan *) list_nth(node->subplans, i);
 
+		/*
+		 * Record async subplans.  When executing EvalPlanQual, we treat them
+		 * as sync ones; don't do this when initializing an EvalPlanQual plan
+		 * tree.
+		 */
+		if (initNode->async_capable && estate->es_epq_active == NULL)
+		{
+			asyncplans = bms_add_member(asyncplans, j);
+			nasyncplans++;
+		}
+
 		/*
 		 * Record the lowest appendplans index which is a valid partial plan.
 		 */
@@ -130,15 +151,38 @@ ExecInitAppendBase(AppendBaseState *state,
 		state->ps.resultopsfixed = false;
 	}
 
-	/* Initialize async state to safe defaults */
-	state->asyncplans = NULL;
-	state->nasyncplans = 0;
+	/* Initialize async state */
+	state->asyncplans = asyncplans;
+	state->nasyncplans = nasyncplans;
 	state->asyncrequests = NULL;
 	state->asyncresults = NULL;
 	state->needrequest = NULL;
 	state->eventset = NULL;
 	state->valid_asyncplans = NULL;
 
+	if (nasyncplans > 0)
+	{
+		state->asyncrequests = palloc0_array(AsyncRequest *, nplans);
+
+		i = -1;
+		while ((i = bms_next_member(asyncplans, i)) >= 0)
+		{
+			AsyncRequest *areq;
+
+			areq = palloc_object(AsyncRequest);
+			areq->requestor = (PlanState *) state;
+			areq->requestee = appendplanstates[i];
+			areq->request_index = i;
+			areq->callback_pending = false;
+			areq->request_complete = false;
+			areq->result = NULL;
+
+			state->asyncrequests[i] = areq;
+		}
+
+		state->asyncresults = palloc0_array(TupleTableSlot *, nasyncplans);
+	}
+
 	/*
 	 * Miscellaneous initialization
 	 */
@@ -149,6 +193,7 @@ void
 ExecReScanAppendBase(AppendBaseState *node)
 {
 	int			i;
+	int			nasyncplans = node->nasyncplans;
 
 	/*
 	 * If any PARAM_EXEC Params used in pruning expressions have changed, then
@@ -184,6 +229,214 @@ ExecReScanAppendBase(AppendBaseState *node)
 		if (subnode->chgParam == NULL)
 			ExecReScan(subnode);
 	}
+
+	/* Reset async state */
+	if (nasyncplans > 0)
+	{
+		i = -1;
+		while ((i = bms_next_member(node->asyncplans, i)) >= 0)
+		{
+			AsyncRequest *areq = node->asyncrequests[i];
+
+			/*
+			 * If a request from before this rescan is still genuinely in
+			 * flight (e.g. because a LIMIT above us stopped pulling tuples
+			 * before it could complete), leave it alone: resetting it here,
+			 * while the FDW still considers it outstanding (e.g.
+			 * postgres_fdw's per-connection pendingAreq), would
+			 * desynchronize our bookkeeping from its.
+			 * ExecAppendBaseAsyncBegin() drains it via the subplan's own
+			 * ReScan callback before firing a fresh request for it.
+			 */
+			if (areq->callback_pending)
+				continue;
+
+			areq->request_complete = false;
+			areq->result = NULL;
+		}
+
+		bms_free(node->needrequest);
+		node->needrequest = NULL;
+	}
+}
+
+/*  Wait or poll for file descriptor events and fire callbacks. */
+void
+ExecAppendBaseAsyncEventWait(AppendBaseState *node, int timeout,
+							 uint32 wait_event_info)
+{
+	int			nevents = node->nasyncplans + 2;	/* one for PM death and
+													 * one for latch */
+	int			noccurred;
+	int			i;
+	WaitEvent	occurred_event[EVENT_BUFFER_SIZE];
+
+	Assert(node->eventset == NULL);
+
+	node->eventset = CreateWaitEventSet(CurrentResourceOwner, nevents);
+	AddWaitEventToSet(node->eventset, WL_EXIT_ON_PM_DEATH, PGINVALID_SOCKET,
+					  NULL, NULL);
+
+	/* Give each waiting subplan a chance to add an event. */
+	i = -1;
+	while ((i = bms_next_member(node->asyncplans, i)) >= 0)
+	{
+		AsyncRequest *areq = node->asyncrequests[i];
+
+		if (areq->callback_pending)
+			ExecAsyncConfigureWait(areq);
+	}
+
+	/*
+	 * No need for further processing if none of the subplans configured any
+	 * events.
+	 */
+	if (GetNumRegisteredWaitEvents(node->eventset) == 1)
+	{
+		FreeWaitEventSet(node->eventset);
+		node->eventset = NULL;
+		return;
+	}
+
+	/*
+	 * Add the process latch to the set, so that we wake up to process the
+	 * standard interrupts with CHECK_FOR_INTERRUPTS().
+	 *
+	 * NOTE: For historical reasons, it's important that this is added to the
+	 * WaitEventSet after the ExecAsyncConfigureWait() calls.  Namely,
+	 * postgres_fdw calls "GetNumRegisteredWaitEvents(set) == 1" to check if
+	 * any other events are in the set.  That's a poor design, it's
+	 * questionable for postgres_fdw to be doing that in the first place, but
+	 * we cannot change it now.  The pattern has possibly been copied to other
+	 * extensions too.
+	 */
+	AddWaitEventToSet(node->eventset, WL_LATCH_SET, PGINVALID_SOCKET,
+					  MyLatch, NULL);
+
+	/* Return at most EVENT_BUFFER_SIZE events in one call. */
+	if (nevents > EVENT_BUFFER_SIZE)
+		nevents = EVENT_BUFFER_SIZE;
+
+	/*
+	 * If the timeout is -1, wait until at least one event occurs.  If the
+	 * timeout is 0, poll for events, but do not wait at all.
+	 */
+	noccurred = WaitEventSetWait(node->eventset, timeout, occurred_event,
+								 nevents, wait_event_info);
+	FreeWaitEventSet(node->eventset);
+	node->eventset = NULL;
+	if (noccurred == 0)
+		return;
+
+	/* Deliver notifications. */
+	for (i = 0; i < noccurred; i++)
+	{
+		WaitEvent  *w = &occurred_event[i];
+
+		/*
+		 * Each waiting subplan should have registered its wait event with
+		 * user_data pointing back to its AsyncRequest.
+		 */
+		if ((w->events & WL_SOCKET_READABLE) != 0)
+		{
+			AsyncRequest *areq = (AsyncRequest *) w->user_data;
+
+			if (areq->callback_pending)
+			{
+				/*
+				 * Mark it as no longer needing a callback.  We must do this
+				 * before dispatching the callback in case the callback resets
+				 * the flag.
+				 */
+				areq->callback_pending = false;
+
+				/* Do the actual work. */
+				ExecAsyncNotify(areq);
+			}
+		}
+
+		/* Handle standard interrupts */
+		if ((w->events & WL_LATCH_SET) != 0)
+		{
+			ResetLatch(MyLatch);
+			CHECK_FOR_INTERRUPTS();
+		}
+	}
+}
+
+/*  Begin executing async-capable subplans. */
+void
+ExecAppendBaseAsyncBegin(AppendBaseState *node)
+{
+	int			i;
+
+	/* Backward scan is not supported by async-aware Appends. */
+	Assert(ScanDirectionIsForward(node->ps.state->es_direction));
+
+	/* We should never be called when there are no subplans */
+	Assert(node->nplans > 0);
+
+	/* We should never be called when there are no async subplans. */
+	Assert(node->nasyncplans > 0);
+
+	/* Make a request for each of the valid async subplans. */
+	i = -1;
+	while ((i = bms_next_member(node->valid_asyncplans, i)) >= 0)
+	{
+		AsyncRequest *areq = node->asyncrequests[i];
+
+		Assert(areq->request_index == i);
+
+		/*
+		 * A request from before a rescan can still be genuinely in flight
+		 * if it never got a chance to complete (e.g. a LIMIT above us
+		 * stopped pulling tuples before it could).  Drain it via the
+		 * subplan's own ReScan callback before firing a fresh request, so
+		 * the FDW's own bookkeeping for the connection (e.g. postgres_fdw's
+		 * per-connection pendingAreq) stays in sync with ours.
+		 */
+		if (areq->callback_pending)
+		{
+			ExecReScan(node->plans[i]);
+			areq->callback_pending = false;
+			areq->request_complete = false;
+			areq->result = NULL;
+		}
+
+		/* Do the actual work. */
+		ExecAsyncRequest(areq);
+	}
+}
+
+/*
+ * classify_matching_subplans_common
+ *		Common part of classify_matching_subplans() for Append and MergeAppend.
+ *
+ * Splits valid_subplans into sync and async sets.  Returns false if there
+ * are no valid async subplans, true otherwise.
+ */
+bool
+classify_matching_subplans_common(Bitmapset **valid_subplans,
+								  Bitmapset *asyncplans,
+								  Bitmapset **valid_asyncplans)
+{
+	Assert(*valid_asyncplans == NULL);
+
+	/* Checked by classify_matching_subplans() */
+	Assert(!bms_is_empty(*valid_subplans));
+
+	/* Nothing to do if there are no valid async subplans. */
+	if (!bms_overlap(*valid_subplans, asyncplans))
+		return false;
+
+	/* Get valid async subplans. */
+	*valid_asyncplans = bms_intersect(asyncplans,
+									  *valid_subplans);
+
+	/* Adjust the valid subplans to contain sync subplans only. */
+	*valid_subplans = bms_del_members(*valid_subplans,
+									  *valid_asyncplans);
+	return true;
 }
 
 /*  Shuts down the subplans of an AppendBase node. */
diff --git a/src/backend/executor/nodeAppend.c b/src/backend/executor/nodeAppend.c
index de015380a35..2b215ac4a3a 100644
--- a/src/backend/executor/nodeAppend.c
+++ b/src/backend/executor/nodeAppend.c
@@ -85,7 +85,6 @@ struct ParallelAppendState
 };
 
 #define INVALID_SUBPLAN_INDEX		-1
-#define EVENT_BUFFER_SIZE			16
 
 static TupleTableSlot *ExecAppend(PlanState *pstate);
 static bool choose_next_subplan_locally(AppendState *node);
@@ -113,10 +112,6 @@ AppendState *
 ExecInitAppend(Append *node, EState *estate, int eflags)
 {
 	AppendState *appendstate = makeNode(AppendState);
-	Bitmapset  *asyncplans;
-	int			nasyncplans;
-	int			nplans;
-	int			i;
 
 	/* check for unsupported flags */
 	Assert(!(eflags & EXEC_FLAG_MARK));
@@ -141,59 +136,11 @@ ExecInitAppend(Append *node, EState *estate, int eflags)
 					   node->first_partial_plan,
 					   &appendstate->as_first_partial_plan);
 
-	nplans = appendstate->as.nplans;
-
-	/*
-	 * Detect async-capable subplans.  When executing EvalPlanQual, we treat
-	 * them as sync ones; don't do this when initializing an EvalPlanQual plan
-	 * tree.
-	 */
-	asyncplans = NULL;
-	nasyncplans = 0;
-	for (i = 0; i < nplans; i++)
-	{
-		if (appendstate->as.plans[i]->plan->async_capable &&
-			estate->es_epq_active == NULL)
-		{
-			asyncplans = bms_add_member(asyncplans, i);
-			nasyncplans++;
-		}
-	}
+	if (appendstate->as.nasyncplans > 0 && appendstate->as.valid_subplans_identified)
+		classify_matching_subplans(appendstate);
 
-	/* Initialize async state */
-	appendstate->as.asyncplans = asyncplans;
-	appendstate->as.nasyncplans = nasyncplans;
-	appendstate->as_nasyncresults = 0;
 	appendstate->as_nasyncremain = 0;
 
-	if (nasyncplans > 0)
-	{
-		appendstate->as.asyncrequests = (AsyncRequest **)
-			palloc0(nplans * sizeof(AsyncRequest *));
-
-		i = -1;
-		while ((i = bms_next_member(asyncplans, i)) >= 0)
-		{
-			AsyncRequest *areq;
-
-			areq = palloc_object(AsyncRequest);
-			areq->requestor = (PlanState *) appendstate;
-			areq->requestee = appendstate->as.plans[i];
-			areq->request_index = i;
-			areq->callback_pending = false;
-			areq->request_complete = false;
-			areq->result = NULL;
-
-			appendstate->as.asyncrequests[i] = areq;
-		}
-
-		appendstate->as.asyncresults = (TupleTableSlot **)
-			palloc0(nasyncplans * sizeof(TupleTableSlot *));
-
-		if (appendstate->as.valid_subplans_identified)
-			classify_matching_subplans(appendstate);
-	}
-
 	/* For parallel query, this will be overridden later. */
 	appendstate->choose_next_subplan = choose_next_subplan_locally;
 
@@ -313,43 +260,16 @@ ExecEndAppend(AppendState *node)
 void
 ExecReScanAppend(AppendState *node)
 {
+
 	int			nasyncplans = node->as.nasyncplans;
 
 	ExecReScanAppendBase(&node->as);
 
-	/* Reset async state */
+	/* Reset Append-specific state */
 	if (nasyncplans > 0)
 	{
-		int			i;
-
-		i = -1;
-		while ((i = bms_next_member(node->as.asyncplans, i)) >= 0)
-		{
-			AsyncRequest *areq = node->as.asyncrequests[i];
-
-			/*
-			 * Leave a request that is still marked as pending a callback
-			 * alone: it may genuinely still be in flight, or it may have an
-			 * unconsumed result already sitting on a connection shared with
-			 * another subplan (as can happen with postgres_fdw).  Blindly
-			 * clearing callback_pending here would desync our bookkeeping
-			 * from the async-capable node's own, which can lead it to
-			 * mishandle that connection later (e.g. postgres_fdw asserts that
-			 * a request it still considers in-process has callback_pending
-			 * set).  Such a request is instead drained lazily, right before
-			 * it would be reused, in ExecAppendAsyncBegin().
-			 */
-			if (areq->callback_pending)
-				continue;
-
-			areq->request_complete = false;
-			areq->result = NULL;
-		}
-
 		node->as_nasyncresults = 0;
 		node->as_nasyncremain = 0;
-		bms_free(node->as.needrequest);
-		node->as.needrequest = NULL;
 	}
 
 	/* Let choose_next_subplan_* function handle setting the first subplan */
@@ -769,39 +689,7 @@ ExecAppendAsyncBegin(AppendState *node)
 	if (node->as_nasyncremain == 0)
 		return;
 
-	/* Make a request for each of the valid async subplans. */
-	{
-		int			i = -1;
-
-		while ((i = bms_next_member(node->as.valid_asyncplans, i)) >= 0)
-		{
-			AsyncRequest *areq = node->as.asyncrequests[i];
-
-			Assert(areq->request_index == i);
-
-			/*
-			 * This request may still be marked as pending a callback, if
-			 * ExecReScanAppend() left it alone because it might have been
-			 * genuinely in flight (or had an unconsumed result waiting on a
-			 * connection shared with another subplan).  Drain it now, before
-			 * reusing it: ExecReScan() lets the async-capable node settle any
-			 * such outstanding state (e.g. postgres_fdw's
-			 * postgresReScanForeignScan() will wait for an in-progress request on
-			 * its own connection and consume its result), after which it's safe
-			 * to reset our own bookkeeping and issue a fresh request.
-			 */
-			if (areq->callback_pending)
-			{
-				ExecReScan(node->as.plans[i]);
-				areq->callback_pending = false;
-				areq->request_complete = false;
-				areq->result = NULL;
-			}
-
-			/* Do the actual work. */
-			ExecAsyncRequest(areq);
-		}
-	}
+	ExecAppendBaseAsyncBegin(&node->as);
 }
 
 /* ----------------------------------------------------------------
@@ -916,105 +804,12 @@ ExecAppendAsyncRequest(AppendState *node, TupleTableSlot **result)
 static void
 ExecAppendAsyncEventWait(AppendState *node)
 {
-	int			nevents = node->as.nasyncplans + 2;
 	long		timeout = node->as_syncdone ? -1 : 0;
-	WaitEvent	occurred_event[EVENT_BUFFER_SIZE];
-	int			noccurred;
-	int			i;
 
 	/* We should never be called when there are no valid async subplans. */
 	Assert(node->as_nasyncremain > 0);
 
-	Assert(node->as.eventset == NULL);
-	node->as.eventset = CreateWaitEventSet(CurrentResourceOwner, nevents);
-	AddWaitEventToSet(node->as.eventset, WL_EXIT_ON_PM_DEATH, PGINVALID_SOCKET,
-					  NULL, NULL);
-
-	/* Give each waiting subplan a chance to add an event. */
-	i = -1;
-	while ((i = bms_next_member(node->as.asyncplans, i)) >= 0)
-	{
-		AsyncRequest *areq = node->as.asyncrequests[i];
-
-		if (areq->callback_pending)
-			ExecAsyncConfigureWait(areq);
-	}
-
-	/*
-	 * No need for further processing if none of the subplans configured any
-	 * events.
-	 */
-	if (GetNumRegisteredWaitEvents(node->as.eventset) == 1)
-	{
-		FreeWaitEventSet(node->as.eventset);
-		node->as.eventset = NULL;
-		return;
-	}
-
-	/*
-	 * Add the process latch to the set, so that we wake up to process the
-	 * standard interrupts with CHECK_FOR_INTERRUPTS().
-	 *
-	 * NOTE: For historical reasons, it's important that this is added to the
-	 * WaitEventSet after the ExecAsyncConfigureWait() calls.  Namely,
-	 * postgres_fdw calls "GetNumRegisteredWaitEvents(set) == 1" to check if
-	 * any other events are in the set.  That's a poor design, it's
-	 * questionable for postgres_fdw to be doing that in the first place, but
-	 * we cannot change it now.  The pattern has possibly been copied to other
-	 * extensions too.
-	 */
-	AddWaitEventToSet(node->as.eventset, WL_LATCH_SET, PGINVALID_SOCKET,
-					  MyLatch, NULL);
-
-	/* Return at most EVENT_BUFFER_SIZE events in one call. */
-	if (nevents > EVENT_BUFFER_SIZE)
-		nevents = EVENT_BUFFER_SIZE;
-
-	/*
-	 * If the timeout is -1, wait until at least one event occurs.  If the
-	 * timeout is 0, poll for events, but do not wait at all.
-	 */
-	noccurred = WaitEventSetWait(node->as.eventset, timeout, occurred_event,
-								 nevents, WAIT_EVENT_APPEND_READY);
-	FreeWaitEventSet(node->as.eventset);
-	node->as.eventset = NULL;
-	if (noccurred == 0)
-		return;
-
-	/* Deliver notifications. */
-	for (i = 0; i < noccurred; i++)
-	{
-		WaitEvent  *w = &occurred_event[i];
-
-		/*
-		 * Each waiting subplan should have registered its wait event with
-		 * user_data pointing back to its AsyncRequest.
-		 */
-		if ((w->events & WL_SOCKET_READABLE) != 0)
-		{
-			AsyncRequest *areq = (AsyncRequest *) w->user_data;
-
-			if (areq->callback_pending)
-			{
-				/*
-				 * Mark it as no longer needing a callback.  We must do this
-				 * before dispatching the callback in case the callback resets
-				 * the flag.
-				 */
-				areq->callback_pending = false;
-
-				/* Do the actual work. */
-				ExecAsyncNotify(areq);
-			}
-		}
-
-		/* Handle standard interrupts */
-		if ((w->events & WL_LATCH_SET) != 0)
-		{
-			ResetLatch(MyLatch);
-			CHECK_FOR_INTERRUPTS();
-		}
-	}
+	ExecAppendBaseAsyncEventWait(&node->as, timeout, WAIT_EVENT_APPEND_READY);
 }
 
 /* ----------------------------------------------------------------
@@ -1072,10 +867,7 @@ ExecAsyncAppendResponse(AsyncRequest *areq)
 static void
 classify_matching_subplans(AppendState *node)
 {
-	Bitmapset  *valid_asyncplans;
-
 	Assert(node->as.valid_subplans_identified);
-	Assert(node->as.valid_asyncplans == NULL);
 
 	/* Nothing to do if there are no valid subplans. */
 	if (bms_is_empty(node->as.valid_subplans))
@@ -1085,21 +877,9 @@ classify_matching_subplans(AppendState *node)
 		return;
 	}
 
-	/* Nothing to do if there are no valid async subplans. */
-	if (!bms_overlap(node->as.valid_subplans, node->as.asyncplans))
-	{
+	/* No valid async subplans identified. */
+	if (!classify_matching_subplans_common(&node->as.valid_subplans,
+										   node->as.asyncplans,
+										   &node->as.valid_asyncplans))
 		node->as_nasyncremain = 0;
-		return;
-	}
-
-	/* Get valid async subplans. */
-	valid_asyncplans = bms_intersect(node->as.asyncplans,
-									 node->as.valid_subplans);
-
-	/* Adjust the valid subplans to contain sync subplans only. */
-	node->as.valid_subplans = bms_del_members(node->as.valid_subplans,
-											  valid_asyncplans);
-
-	/* Save valid async subplans. */
-	node->as.valid_asyncplans = valid_asyncplans;
 }
diff --git a/src/backend/executor/nodeMergeAppend.c b/src/backend/executor/nodeMergeAppend.c
index 2575d6a499f..8d8a502e928 100644
--- a/src/backend/executor/nodeMergeAppend.c
+++ b/src/backend/executor/nodeMergeAppend.c
@@ -153,9 +153,12 @@ ExecMergeAppend(PlanState *pstate)
 		 * run-time pruning is disabled then the valid subplans will always be
 		 * set to all subplans.
 		 */
-		if (node->as.valid_subplans == NULL)
+		if (!node->as.valid_subplans_identified)
+		{
 			node->as.valid_subplans =
 				ExecFindMatchingSubPlans(node->as.prune_state, false, NULL);
+			node->as.valid_subplans_identified = true;
+		}
 
 		/*
 		 * First time through: pull the first tuple from each valid subplan,
diff --git a/src/include/executor/execAppend.h b/src/include/executor/execAppend.h
index a8f41bad921..7f53ad89213 100644
--- a/src/include/executor/execAppend.h
+++ b/src/include/executor/execAppend.h
@@ -22,5 +22,12 @@ extern void ExecInitAppendBase(AppendBaseState *state,
 							   int *first_valid_partial_plan);
 extern void ExecEndAppendBase(AppendBaseState *node);
 extern void ExecReScanAppendBase(AppendBaseState *node);
+extern void ExecAppendBaseAsyncBegin(AppendBaseState *node);
+extern void ExecAppendBaseAsyncEventWait(AppendBaseState *node,
+										 int timeout,
+										 uint32 wait_event_info);
+extern bool classify_matching_subplans_common(Bitmapset **valid_subplans,
+											  Bitmapset *asyncplans,
+											  Bitmapset **valid_asyncplans);
 
 #endif							/* EXECAPPEND_H */
-- 
2.39.5 (Apple Git-154)