BUG #19557: Parallel leader stuckat ParallelFinish leaked InterruptHoldoffCount - Mailing list pgsql-bugs

From PG Bug reporting form
Subject BUG #19557: Parallel leader stuckat ParallelFinish leaked InterruptHoldoffCount
Date
Msg-id 19557-d88cb23f38eb9b91@postgresql.org
Whole thread
List pgsql-bugs
The following bug has been logged on the website:

Bug reference:      19557
Logged by:          Jacob Mørk Brejnbjerg
Email address:      jacob@brejnbjerg.com
PostgreSQL version: 18.4
Operating system:   Ubuntu 24.04 (kernel 6.8.0-124-generic)
Description:

A backend executing a parallel query has been stuck for 40+ hours in
wait_event IPC/ParallelFinish. All of its parallel workers exited cleanly
(exit code 0, nothing logged). pg_cancel_backend() and
pg_terminate_backend() have no effect. We kept the backend alive and
debugged it with gdb + debug symbols; the evidence shows a leaked
InterruptHoldoffCount, plus a design interaction that turns any such leak
into a permanently unkillable backend.

Possibly relevant configuration: io_method = io_uring,
max_parallel_workers_per_gather = 48,
shared_preload_libraries = 'timescaledb,pg_parquet'
(timescaledb 2.27.2, pg_parquet 0.5.1).
PostgreSQL 18.4 (Ubuntu 18.4-1.pgdg24.04+1).

The query is a COPY (SELECT <aggregation over one table> GROUP BY ...)
TO STDOUT (FORMAT parquet) issued via pg_parquet's COPY hook; the inner
select runs a parallel plan (Gather, 10+ workers planned) over
TimescaleDB compressed-hypertable chunks, i.e. an AIO-read-heavy scan.

Backtrace of the stuck leader (main thread; the only other thread is an
io_uring kernel worker, "iou-wrk-<pid>"):

#0  epoll_wait (epfd=754, events=..., maxevents=1, timeout=-1)
#1  WaitEventSetWait ()
#2  WaitLatch ()
#3  WaitForParallelWorkersToFinish ()
#4  ExecParallelFinish ()
#5-#8  (Gather shutdown path, static functions)
#9  standard_ExecutorRun ()
#10-#11 PortalRun ()
#12-#17 pg_parquet.so (COPY TO hook driving the portal)
#18 prev_ProcessUtility (timescaledb process_utility.c:124)
#19 timescaledb_ddl_command_start (...)
#20-#22 PortalRun ()
#23-#24 PostgresMain ()

Key state read from the live process via gdb:

InterruptPending       = 1
ProcDiePending         = 1    <- pg_terminate_backend arrived, never
serviced
QueryCancelPending     = 1    <- pg_cancel_backend arrived, never serviced
InterruptHoldoffCount  = 1    <- held, forever
CritSectionCount       = 0
num_held_lwlocks       = 0    <- NO LWLock is held
ParallelMessagePending = 1    <- worker messages queued, unprocessable

/proc signal state: SigPnd/ShdPnd are all zero - the SIGTERM/SIGINT were
delivered and consumed by the handlers (which set the flags above); the
backend wakes from WaitLatch, but CHECK_FOR_INTERRUPTS() is a no-op with
InterruptHoldoffCount > 0, so it just re-sleeps.

Why this is a permanent hang and not just an uncancellable wait:
worker-exit accounting is itself interrupt-driven. The workers' detach
notifications are only processed via CHECK_FOR_INTERRUPTS() ->
ProcessParallelMessages(), which is what clears each worker's error_mqh.
With the holdoff leaked, ProcessParallelMessages never runs
(ParallelMessagePending stays 1 - verified in the live process), so
WaitForParallelWorkersToFinish() believes the workers are still attached
and waits forever. The same leaked holdoff blocks ProcDiePending. One
leaked holdoff therefore produces both symptoms; only SIGKILL
(crash-restart) or a debugger can clear the backend.

On the origin of the leak: num_held_lwlocks = 0 rules out a stuck
LWLockAcquire, and the server log shows NO error from this backend for
its entire lifetime, which we believe rules out the
"ereport(ERROR) raised under HOLD, caught by PG_TRY" pattern (the raise
would have been logged by errfinish). That points to a straight-line
code path that called HOLD_INTERRUPTS() and returned without
RESUME_INTERRUPTS(). We audited the read path: bufmgr.c, read_stream.c,
shm_mq.c, condition_variable.c contain no bare holds; timescaledb 2.27.2
and pg_parquet 0.5.1 (and pgrx 0.16.1) contain no HOLD_INTERRUPTS at
all. The remaining bare holds on this workload's path are in
src/backend/storage/aio/aio.c (four HOLD/RESUME regions, including
pgaio_io_reclaim which runs completion callbacks under HOLD) and the
parallel-message path itself. Given io_method = io_uring and an
AIO-heavy scan we suspect an AIO path, but we could not identify the
exact site from the live process.

Frequency: once in ~6 days of heavy production use of this COPY path
(thousands of executions/day). Not deterministically reproducible so
far.

We have a gcore of the stuck backend and can keep it (and/or the live
process) available - happy to run additional gdb commands against it.

Separately, it may be worth considering a defensive measure: because
WaitForParallelWorkersToFinish depends on interrupt processing for its
own termination condition, any holdoff leak from any subsystem converts
into a silent, unkillable backend. Detecting "waiting on ParallelFinish
with INTERRUPTS_CAN_BE_PROCESSED() false and ParallelMessagePending set"
and at least logging (or treating it as an invariant violation) would
make this failure mode visible instead of silent.





pgsql-bugs by date:

Previous
From: Matthias van de Meent
Date:
Subject: Re: BUG #19519: REPACK can fail due to missing chunk for toast value
Next
From: Peter Eisentraut
Date:
Subject: Re: BUG #19548: Missing dependency between a graph edge and the related PK