Gleb Kashkin писал(а) 2026-08-04 15:19:
Etsuro Fujita писал(а) 2026-08-04 14:26:
Hi,
On Tue, Aug 4, 2026 at 5:35 PM Alexander Pyhalov
<[email protected]> wrote:
Etsuro Fujita писал(а) 2026-08-03 16:57:
> Attached is an updated version of the
> patch.
Hi. Looks good to me.
Cool! I'll push the patch and backpatch it to all supported versions.
Thanks for reviewing!
Best regards,
Etsuro Fujita
Hi. It seems there is the last small issue with the patch.
The following scenario is possible:
1) During rescan, one child of the Append has already produced a tuple
and set as_needrequest
2) Another child still has callback_pending, but its postgres_fdw
connection is occupied by an async request belonging to a different
Append
3) While draining the second child, postgresForeignAsyncConfigureWait()
sees the different requestor and the stale nonempty as_needrequest, so
it returns without registering an event
4) The reset loop then repeats without making progress
See reproducer and fix in the patch. It should be applied to v2.
Sorry, misclicked. Here is the patch.
--
Best regards,
Gleb Kashkin,
Postgres Professional
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 58d432a9af1..ff802708e0d 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -11827,6 +11827,59 @@ SELECT o.x FROM (VALUES (2505), (3505)) o(x), LATERAL (SELECT a FROM async_pt WH
3505
(2 rows)
+-- Test rescanning an async Append that has both a child ready for a new
+-- request and a child waiting for another Append using the same connection.
+CREATE VIEW base_tbl2_slow AS
+ WITH delay AS MATERIALIZED (SELECT pg_sleep(0.1))
+ SELECT t.* FROM base_tbl2 t, delay;
+ALTER FOREIGN TABLE async_p2 OPTIONS (SET table_name 'base_tbl2_slow');
+SET statement_timeout = '10s';
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT o.a AS outer_a, i.a AS inner_a
+FROM (SELECT a FROM async_pt LIMIT 2) o,
+LATERAL (SELECT a FROM async_pt WHERE a >= o.a LIMIT 1) i;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------
+ Nested Loop
+ Output: async_pt.a, async_pt_4.a
+ -> Limit
+ Output: async_pt.a
+ -> Append
+ -> Async Foreign Scan on public.async_p1 async_pt_1
+ Output: async_pt_1.a
+ Remote SQL: SELECT a FROM public.base_tbl1
+ -> Async Foreign Scan on public.async_p2 async_pt_2
+ Output: async_pt_2.a
+ Remote SQL: SELECT a FROM public.base_tbl2_slow
+ -> Async Foreign Scan on public.async_p3 async_pt_3
+ Output: async_pt_3.a
+ Remote SQL: SELECT a FROM public.base_tbl3
+ -> Limit
+ Output: async_pt_4.a
+ -> Append
+ -> Async Foreign Scan on public.async_p1 async_pt_5
+ Output: async_pt_5.a
+ Remote SQL: SELECT a FROM public.base_tbl1 WHERE ((a >= $1::integer))
+ -> Async Foreign Scan on public.async_p2 async_pt_6
+ Output: async_pt_6.a
+ Remote SQL: SELECT a FROM public.base_tbl2_slow WHERE ((a >= $1::integer))
+ -> Async Foreign Scan on public.async_p3 async_pt_7
+ Output: async_pt_7.a
+ Remote SQL: SELECT a FROM public.base_tbl3 WHERE ((a >= $1::integer))
+(26 rows)
+
+SELECT o.a AS outer_a, i.a AS inner_a
+FROM (SELECT a FROM async_pt LIMIT 2) o,
+LATERAL (SELECT a FROM async_pt WHERE a >= o.a LIMIT 1) i;
+ outer_a | inner_a
+---------+---------
+ 1000 | 1000
+ 1005 | 1005
+(2 rows)
+
+RESET statement_timeout;
+ALTER FOREIGN TABLE async_p2 OPTIONS (SET table_name 'base_tbl2');
+DROP VIEW base_tbl2_slow;
-- Test COPY TO when foreign table is partition
COPY async_pt TO stdout; --error
ERROR: cannot copy from foreign table "async_p1"
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 525a6ff394f..bdde6dffe97 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -4065,6 +4065,26 @@ EXPLAIN (VERBOSE, COSTS OFF)
SELECT o.x FROM (VALUES (2505), (3505)) o(x), LATERAL (SELECT a FROM async_pt WHERE (a = 1505 AND o.x = 2505) OR a = o.x LIMIT 1) s ORDER BY o.x;
SELECT o.x FROM (VALUES (2505), (3505)) o(x), LATERAL (SELECT a FROM async_pt WHERE (a = 1505 AND o.x = 2505) OR a = o.x LIMIT 1) s ORDER BY o.x;
+-- Test rescanning an async Append that has both a child ready for a new
+-- request and a child waiting for another Append using the same connection.
+CREATE VIEW base_tbl2_slow AS
+ WITH delay AS MATERIALIZED (SELECT pg_sleep(0.1))
+ SELECT t.* FROM base_tbl2 t, delay;
+ALTER FOREIGN TABLE async_p2 OPTIONS (SET table_name 'base_tbl2_slow');
+SET statement_timeout = '10s';
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT o.a AS outer_a, i.a AS inner_a
+FROM (SELECT a FROM async_pt LIMIT 2) o,
+LATERAL (SELECT a FROM async_pt WHERE a >= o.a LIMIT 1) i;
+SELECT o.a AS outer_a, i.a AS inner_a
+FROM (SELECT a FROM async_pt LIMIT 2) o,
+LATERAL (SELECT a FROM async_pt WHERE a >= o.a LIMIT 1) i;
+
+RESET statement_timeout;
+ALTER FOREIGN TABLE async_p2 OPTIONS (SET table_name 'base_tbl2');
+DROP VIEW base_tbl2_slow;
+
-- Test COPY TO when foreign table is partition
COPY async_pt TO stdout; --error
diff --git a/src/backend/executor/nodeAppend.c b/src/backend/executor/nodeAppend.c
index 692f73171e5..83f4497d227 100644
--- a/src/backend/executor/nodeAppend.c
+++ b/src/backend/executor/nodeAppend.c
@@ -1145,6 +1145,15 @@ ExecAppendAsyncReset(AppendState *node)
{
bool found = false;
+ /*
+ * Discard results and new-request markers generated by the old scan.
+ * Do this on every iteration, since callbacks invoked below can add them
+ * again, and FDWs may use as_needrequest when configuring wait events.
+ */
+ node->as_nasyncresults = 0;
+ bms_free(node->as_needrequest);
+ node->as_needrequest = NULL;
+
i = -1;
while ((i = bms_next_member(node->as_asyncplans, i)) >= 0)
{
@@ -1178,10 +1187,7 @@ ExecAppendAsyncReset(AppendState *node)
areq->result = NULL;
}
- node->as_nasyncresults = 0;
node->as_nasyncremain = 0;
- bms_free(node->as_needrequest);
- node->as_needrequest = NULL;
}
/* ----------------------------------------------------------------