Subject: Bug in asynchronous Append

From: Alexander Korotkov <[email-masked]>
Date: Sat, 4 Jul 2026 01:00:16 +0300

Hi!

ExecReScanAppend() unconditionally resets callback_pending for all
AsyncRequests.  The problem is that postgres_fdw keeps its own knowledge
for the same fact: PgFdwConnState.pendingAreq – a pointer to "pending async
request" for a given connection.  That connection can be shared by several
partitions/foreign tables (postgres_fdw caches one connection per
server+usermapping pair). The blind reset in nodeAppend.c only touches the
local AsyncRequest.callback_pending; it never touches
PgFdwConnState.pendingAreq, which correctly points to the still-dangling
request.

Later, when another partition sharing that same connection gets its own
ReScan (for instance, its chgParam changed because of the LATERAL
parameter, and it already has a cursor open), it sends "CLOSE cursor" via
pgfdw_exec_query().  Before sending any new command on the connection, that
function first drains whatever request is still outstanding on it:

if (state && state->pendingAreq)
    process_pending_request(state->pendingAreq);

And process_pending_request() starts with:

Assert(areq->callback_pending);

– which fails, because the flag was corrupted some rounds earlier.

The attached patch contains both the reproduction case and the fix.  The
fix postpones the reset of the callback_pending flag
to ExecAppendAsyncBegin().  ExecAppendAsyncBegin() performs this cleanup
along with ExecReScan(), which completes the async fetch.

------
Regards,
Alexander Korotkov
Supabase

---

From: Etsuro Fujita <[email-masked]>
Date: Sat, 4 Jul 2026 18:04:59 +0900

Hi Alexander,

On Sat, Jul 4, 2026 at 7:00 AM Alexander Korotkov <[email-masked]> wrote:
> ExecReScanAppend() unconditionally resets callback_pending for all AsyncRequests.  The problem is that postgres_fdw keeps its own knowledge for the same fact: PgFdwConnState.pendingAreq – a pointer to "pending async request" for a given connection.  That connection can be shared by several partitions/foreign tables (postgres_fdw caches one connection per server+usermapping pair). The blind reset in nodeAppend.c only touches the local AsyncRequest.callback_pending; it never touches PgFdwConnState.pendingAreq, which correctly points to the still-dangling request.
>
> Later, when another partition sharing that same connection gets its own ReScan (for instance, its chgParam changed because of the LATERAL parameter, and it already has a cursor open), it sends "CLOSE cursor" via pgfdw_exec_query().  Before sending any new command on the connection, that function first drains whatever request is still outstanding on it:
>
> The attached patch contains both the reproduction case and the fix.  The fix postpones the reset of the callback_pending flag to ExecAppendAsyncBegin().  ExecAppendAsyncBegin() performs this cleanup along with ExecReScan(), which completes the async fetch.

Interesting!  Thanks for the report and patch!  Will review.

Best regards,
Etsuro Fujita

---

From: Alexander Pyhalov <[email-masked]>
Date: Mon, 06 Jul 2026 17:51:45 +0300

Alexander Korotkov писал(а) 2026-07-04 01:00:
> AsyncRequests.  The problem is that postgres_fdw keeps its own
> knowledge for the same fact: PgFdwConnState.pendingAreq – a pointer
> to "pending async request" for a given connection.  That connection
> can be shared by several partitions/foreign tables (postgres_fdw
> caches one connection per server+usermapping pair). The blind reset in
> nodeAppend.c only touches the local AsyncRequest.callback_pending; it
> never touches PgFdwConnState.pendingAreq, which correctly points to
> the still-dangling request.
> 
> Later, when another partition sharing that same connection gets its
> own ReScan (for instance, its chgParam changed because of the LATERAL
> parameter, and it already has a cursor open), it sends "CLOSE cursor"
> via pgfdw_exec_query().  Before sending any new command on the
> connection, that function first drains whatever request is still
> outstanding on it:
> 
> The attached patch contains both the reproduction case and the fix.
> The fix postpones the reset of the callback_pending flag to
> ExecAppendAsyncBegin().  ExecAppendAsyncBegin() performs this cleanup

Hi. The analysis seems correct to me as well as fix.
-- 
Best regards,
Alexander Pyhalov,
Postgres Professional