From 6081b406eb6ed3f5f54cb2b6383583d939d383fb Mon Sep 17 00:00:00 2001 From: Vlad Lesin Date: Mon, 1 Jun 2026 14:25:08 +0300 Subject: [PATCH 1/2] Detect peer disconnect in QueueRead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MULTI_PROCESS=YES uses two backends: the main backend (e.g. TYPE=FUNCTION) reads the load source and pushes heap tuples into a shared queue; a parallel writer backend (TYPE=TUPLE, WRITER=DIRECT over libpq) pops them from the queue and writes to storage. On success the main backend sends a zero-length queue message via write_queue(NULL, 0) in ParallelWriterClose() when onError is false. If the reader backend fails first (e.g. ParserInit() on invalid INFILE in the load_function test), cleanup runs before any queue EOF is sent: pg_bulkload() → WriterInit() before ParserInit() in PG_TRY → ParserInit() fails → PG_CATCH → WriterClose(wt, true) → ParallelWriterClose(onError=true) → PQgetCancel() / PQcancel() if PQisBusy → PQfinish() (no write_queue(NULL, 0)) The postmaster still delivers cancel (PQcancel → ProcessCancelRequestPacket() → SendCancelRequest() → SIGINT → StatementCancelHandler() sets QueryCancelPending) [PostgreSQL: backend_startup.c, procsignal.c, postgres.c]. Depending on timing, ProcessInterrupts() in the writer may see DoingCommandRead=true while PostgresMain() is between extended-protocol messages (ReadCommand() after Bind, before Execute) and clear QueryCancelPending without ERROR: canceling statement due to user request [postgres.c: ProcessInterrupts(), main-loop comment at ReadCommand]. Then the writer continues pg_bulkload(), may hold AccessExclusiveLock [DirectWriterInit() / table_open in writer_direct.c], and blocks in QueueRead() waiting for tuples the main backend will never send [TupleParserRead() in parser_tuple.c]. Symptom: orphaned writer backend (TYPE=TUPLE in pg_stat_activity), lock on the target relation, following pg_bulkload or installcheck appearing to hang. After each sleep in QueueRead(), call shmctl(IPC_STAT) on the queue segment (Unix). If shm_nattch <= 1, only this backend remains attached: the main backend has detached without sending EOF. Raise an error so the writer backend exits and releases its locks instead of waiting forever. No equivalent check exists on Windows (#ifndef WIN32); a separate mechanism would be needed there. --- lib/pgut/pgut-ipc.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/pgut/pgut-ipc.c b/lib/pgut/pgut-ipc.c index 3a7730b..4f530f3 100644 --- a/lib/pgut/pgut-ipc.c +++ b/lib/pgut/pgut-ipc.c @@ -370,6 +370,24 @@ QueueRead(Queue *self, void *buffer, uint32 len, bool need_lock) CHECK_FOR_INTERRUPTS(); pg_usleep(SPIN_SLEEP_MSEC * 1000); +#ifndef WIN32 + /* + * Detect if the writer detached without sending the + * terminator (e.g. due to an error on the writer side). + * When shm_nattch drops to 1, only we are still attached; + * the writer is gone and no more data will ever arrive. + */ + { + struct shmid_ds ds; + + if (shmctl(self->handle, IPC_STAT, &ds) < 0 || + ds.shm_nattch <= 1) + ereport(ERROR, + (errcode(ERRCODE_CONNECTION_FAILURE), + errmsg("parallel writer has disconnected"))); + } +#endif + goto retry; } From 767c8dc5878316eca709510b1ed895c4d4e9a9fa Mon Sep 17 00:00:00 2001 From: Vlad Lesin Date: Mon, 1 Jun 2026 18:13:26 +0300 Subject: [PATCH 2/2] Use lock groups for MULTI_PROCESS reader and writer On PostgreSQL 9.6+, keep the reader's AccessShareLock and let the writer join the same lock group before taking AccessExclusiveLock, instead of releasing the reader lock before starting the writer. PG < 9.6 keeps the old UnlockRelation path (#if PG_VERSION_NUM >= 90600). Reader: BecomeLockGroupLeader(), publish leader PGPROC/PID in the shared queue header, keep AccessShareLock while the writer runs. Writer: QueueOpen, BecomeLockGroupMember() before the first table lock, then direct write with AccessExclusiveLock. Related PostgreSQL core fixes (lock-group ProcKill bugs): https://www.postgresql.org/message-id/flat/d2983796-2603-41b7-a66e-fc8489ddb954%40gmail.com [PATCH] Fix ProcKill lock-group vs procLatch recycle race All PostgreSQL versions since 9.6 are affected (lock groups were added in 9.6). Upstream addresses the problem in two commits on REL_14_STABLE through REL_18_STABLE (not in released minors yet as of 14.23; expected in the next 14.x minor, e.g. 14.24). Not backpatched to 13 or older: 1) Fix race conditions in ProcKill()'s lock-group freelist handling Refactor lock-group teardown so freelist updates are coordinated under leader_lwlock and a single freeProcsLock pass. Fixes a double push of the leader's PGPROC onto the freelist and a leak of the last follower's slot when leader and member exit concurrently. 2) Fix procLatch ownership race in ProcKill() Call SwitchBackToLocalLatch() and DisownLatch() before any PGPROC can return to the freelist. Fixes "latch already owned by PID ..." when a recycled slot still has an owned procLatch (e.g. follower pushes the leader's PGPROC before the leader reaches DisownLatch). This pg_bulkload change is affected by the same mechanism: MULTI_PROCESS on PG 9.6+ forms a lock group between the reader and writer backends, so both ProcKill() issues can theoretically surface when those backends shut down together (error/cancel paths). Using lock groups here does not replace those server-side fixes; run PostgreSQL builds that include both commits (or wait for the corresponding 14+ minor releases). --- Makefile | 2 +- bin/Makefile | 2 +- docs/pg_bulkload-ja.html | 35 ++++++++++++++++++++-- docs/pg_bulkload.html | 43 ++++++++++++++++++++++++--- include/reader.h | 3 ++ lib/Makefile | 2 +- lib/parser_tuple.c | 26 ++++++++++++---- lib/pg_bulkload.c | 64 ++++++++++++++++++++++++++++++++++++++++ lib/pgut/pgut-ipc.c | 54 +++++++++++++++++++++++++++++++++ lib/pgut/pgut-ipc.h | 4 +++ lib/writer_parallel.c | 24 +++++++++++++++ util/Makefile | 2 +- 12 files changed, 245 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index db107ba..56fdd9d 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ endif endif ifdef USE_PGXS -PG_CONFIG = pg_config +PG_CONFIG ?= pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) else diff --git a/bin/Makefile b/bin/Makefile index eb9a88f..bac63ac 100644 --- a/bin/Makefile +++ b/bin/Makefile @@ -21,7 +21,7 @@ endif endif ifdef USE_PGXS -PG_CONFIG = pg_config +PG_CONFIG ?= pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) else diff --git a/docs/pg_bulkload-ja.html b/docs/pg_bulkload-ja.html index 740a4e5..a81df0d 100644 --- a/docs/pg_bulkload-ja.html +++ b/docs/pg_bulkload-ja.html @@ -471,8 +471,9 @@

フォーマット共通の設定項目

「WRITER=PARALLEL」と指定した場合は、MULTI_PROCESS は無視されます。 なお、ロード先のデータベースに対してパスワード認証を必要とする場合には .pgpass を設定しなければなりません。 詳細は使用上の注意と制約を参照して下さい。 -pg_bulkloadのMULTI_PROCESSやPARALLELを有効にして実行する場合、他のPostgreSQLバックエンドプロセスがテーブルスキーマを変更しないようにしてください。 -データを読み取るプロセスとデータを書き出すプロセスで見られるテーブルスキーマが異なり、問題が発生する可能性があります。 +リーダーおよびライターのバックエンドが対象テーブルをどのようにロックするか、 +および他セッションがスキーマを変更してはいけない条件については、 +パラレルモードにおけるテーブルロックを参照してください。 @@ -664,6 +665,36 @@

kill -9は使わない

パラレルロードで使用する場合

パラレルロードで使用する場合(MULTI_PROCESS=YES または WRITER=PARALLEL)、以下のことに注意しなければなりません:

+

+pg_bulkload は 2 つの PostgreSQL バックエンドを使います。 +リーダーバックエンドがメインの pg_bulkload() を実行し(入力の読み取り・パース・検証)、 +ライターバックエンドが libpq 経由で起動され、TYPE=TUPLE の pg_bulkload() でテーブルへのダイレクトライトを行います。 +

+

パラレルモードにおけるテーブルロック

+

+PostgreSQL 9.6 以降では、リーダーバックエンドが PostgreSQL のロックグループのリーダーとなり、 +対象テーブルに AccessShareLock を保持したまま動作します。 +ライターバックエンドはテーブルにロックを取得する前にそのロックグループに参加し、 +ダイレクトライトのために AccessExclusiveLock を取得します。 +両バックエンドが同一ロックグループに属するため、これらのロックは互いにブロックせず、 +ライターが動作している間もリーダーは共有ロックを解放しません。 +ロードが進行中はライターの AccessExclusiveLock により、 +他セッションが対象テーブルに対する DDL を実行することもブロックされます。 +そのため、リーダーとライターが一貫したテーブル定義を参照するために、 +対象テーブルについて他セッションにスキーマ変更を避けるよう特別に注意する必要は通常ありません。 +

+

+PostgreSQL 9.6 より前のバージョン向けにビルドした場合は、 +リーダーバックエンドがライターバックエンドが AccessExclusiveLock を取得する前に +AccessShareLock を解放します。その間に他セッションがテーブル定義を変更できるため、 +リーダーとライターで異なるテーブル定義を参照する可能性があります。 +ロード中は他セッションから対象テーブルに対する DDL などのスキーマ変更を行わないでください。 +

+

+PostgreSQL のバージョンにかかわらず、ロードが参照する他のオブジェクト +(例: FILTER 関数が参照する型やテーブル)の定義を、 +ロード中に変更しないでください。 +

認証における制約

MULTI_PROCESS=YESかつロード対象のデータベースにlocalhostから接続するのにパスワードが必要な場合、たとえパスワードを正しくプロンプトに入力しても、パスワード認証に失敗してしまいます。この問題を回避するには、以下のいずれかを設定してください。