PG15/PG16：Segfault on logical replication to partitioned table with foreign children

# はじめに

にゃーん。趣味でポスグレをやっている者だ。

この記事は[PostgreSQL 16 全部ぬこ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/pg16-all-nuko) 11日目の記事です。

今回はPostgreSQL 16に追加された、ロジカルレプリケーション＆postgres_fdwの組み合わせ時のバグフィックスについて書いてみます。

# 概要

|項目|内容|
|:--|:--|
|タイトル|[Segfault on logical replication to partitioned table with foreign children](https://commitfest.postgresql.org/40/3989/)|
|Topic|Bug Fixes|
|ステータス|commited|
|Last modified|2022-11-02|
|概要|SUBSCRIBERが外部テーブルの場合にSUBSCRIBERがクラッシュする問題の解決|

# バグ内容（PostgreSQL 15.0）

[バグレポートメール](https://www.postgresql.org/message-id/flat/6b93e3748ba43298694f376ca8797279d7945e29.camel@gmail.com)の例を元にPostgreSQL 15.0で再現してみました（バグレポートメールによると、PostgreSQL 14でも発生するとのこと）。

## 環境
PostgreSQL 15.0のソースセットを``--enable-caseert``オプション付きでconfigureしてビルドしたバイナリで実施しました。

## PUBLISHER側の定義

```sql
$ ~/pgsql/pgsql-15/bin/psql -p 10001 -e -f pub-create.sql
CREATE TABLE parent (id int, num int);
CREATE TABLE
CREATE PUBLICATION parent_pub FOR TABLE parent;
CREATE PUBLICATION
$
```

## SUBSCRIBER側の定義

```sql
$ ~/pgsql/pgsql-15/bin/psql -p 10002 -e -f sub-create.sql
CREATE EXTENSION postgres_fdw;
CREATE EXTENSION
CREATE SERVER loopback foreign data wrapper postgres_fdw options (port '10002', dbname 'postgres');
CREATE SERVER
CREATE USER MAPPING FOR CURRENT_USER SERVER loopback;
CREATE USER MAPPING
CREATE TABLE parent (id int, num int) partition by range (id);
CREATE TABLE
CREATE FOREIGN TABLE p1 PARTITION OF parent DEFAULT SERVER loopback;
CREATE FOREIGN TABLE
CREATE TABLE p1_loc(id int, num int);
CREATE TABLE
CREATE SUBSCRIPTION parent_sub CONNECTION 'port=10001 dbname=postgres' PUBLICATION parent_pub;
psql:sub-create.sql:17: NOTICE:  created replication slot "parent_sub" on publisher
CREATE SUBSCRIPTION$
```

## PUBLISHERテーブルへの挿入
この状態で、PUBLISHER側のインスタンスにある``parent``テーブルに挿入をかける。

```shell
$ ~/pgsql/pgsql-15/bin/psql -p 10001 -c "INSERT INTO parent VALUES (1, 1)"
INSERT 0 1
$
```

PUBLISHER側の挿入は問題なく実行される。
では、このときにSUBSCRIBERで何が起きているのか。
SUBSCRIBER側にpsqlで入ってみると・・・

```shell
=# \d
WARNING:  terminating connection because of crash of another server process
DETAIL:  The postmaster has commanded this server process to roll back the current transaction and exit, because another server process exited abnormally and possibly corrupted shared memory.
HINT:  In a moment you should be able to reconnect to the database and repeat your command.
server closed the connection unexpectedly
        This probably means the server terminated abnormally
        before or while processing the request.
=#
```
なんと、SUBSCRIBERが落ちてしまっているようですね。
なお、この状態でpsコマンドでpostgresのプロセスを見てみると

```shell
ec2-user 16214     1  9 11:53 ?        00:00:39 /home/ec2-user/pgsql/pgsql-15/bin/postgres -D /data/pgdata/15-sub
```
親のpostgresプロセスのみ存在しているという怪しい状態になっています。普通はこの親プロセスから起動された各種のバックグラウンドプロセスも存在するはずなのですが・・・。
で、もう一度、psqlで入ろうとすると、

```shell
$ ~/pgsql/pgsql-15/bin/psql -p 10002 postgres
psql: error: connection to server on socket "/tmp/.s.PGSQL.10002" failed: FATAL:  the database system is in recovery mode
$
```
まだリカバリモードだと言われてしまい、クライアントからのログインができない状態のままになっています。
このときに、起動中のサーバログには

```
LOG:  logical replication apply worker for subscription "parent_sub" has started
TRAP: FailedAssertion("rel->rd_rel->relkind == RELKIND_RELATION", File: "execReplication.c", Line: 414, PID: 19385)
postgres: logical replication worker for subscription 16402 (ExceptionalCondition+0x90)[0x925d70]
（略）
postgres: logical replication worker for subscription 16402 (_start+0x2a)[0x49c2ba]
LOG:  unexpected EOF on client connection with an open transaction
2022-11-04 12:12:16.682 JST [10957] LOG:  background worker "logical replication worker" (PID 19385) was terminated by signal 6: Aborted

```
というロジカルレプリケーションラウンチャの起動とアボートが繰り返し出力されているようです。

* リカバリモードの状態でpsql等で接続しようとすると、リカバリモード中なので接続は拒否される。
* リカバリモードが終了した後は、psql等で接続はできるが、既にそのときにはラウンチャがアボートし、PostgreSQLサーバ全体がクラッシュするので、コマンドを実行しようとしても、既にサーバがクラッシュしている旨のエラーになる。


こうなると問題を引き起こしていると思われるSUBSCRIPTIONを削除することも簡単にできないので、もうこのインスタンスを復旧するには、シングルユーザモードでSUBSCRITIONを定義したインスタンスにログインし、SUBSCRITIONを削除するしかありません（あるいは、データベースクラスタ全体を作成し直すか・・・）。

### シングルユーザモードによる復旧例
```
$ ~/pgsql/pgsql-15/bin/postgres --single postgres -D /data/pgdata/15-sub/
(起動直後に前回クラッシュしたとかチェックポイントのログが出るが割愛)
PostgreSQL stand-alone backend 15.0
backend> DROP SUBSCRIPTION parent_sub
backend> 
```


このバグ、結構深刻な問題を引き起こしてしまうようですね・・・。

# Bug Fix確認

PostgreSQL開発版の現時点の版でBug Fixがされているのかを確認してみます。

## 前提
2022-11-04時点の最新版(commit 3e77b2eb3466aff9fe19e209ebc0d899ea869494)を``--enable-debug --enable-cassert``オプション付きでconfigureしてビルドした版を使って確認しました。

PUBLISHER側とSUBSCRIBER側のDDL定義はPostgreSQL 15.0と同様、特にエラー等は発生しません。

この状態で、PUBLISHER側にINSERTを発行します。

```
$ ~/pgsql/pgsql-15/bin/psql -p 10001 -c "INSERT INTO parent VALUES (1, 1)"
INSERT 0 1
$
```

## PUBLISHER側
PUBLISHER側の挙動は同じ。そのまま1件挿入されています。

## SUBSCRIBER側
PostgreSQL 15.0とは異なり、SUBSCRIBER側のPostgreSQLはアボート等も発生せず正常に起動したままになっています。
PUBLISHER側で挿入されたレコードの論理WALは、SUBSCRIBER側にも伝播されているようですが、SUBSCRIBER側のテーブルには反映はされていません。また、サーバログには以下のように論理レプリケーション用のワーカの起動と、対象テーブルへの反映はできないというエラーメッセージが延々と出力されます（これは``public.p1``が外部テーブルのため）。

```
LOG:  logical replication apply worker for subscription "parent_sub" has started
ERROR:  cannot use relation "public.p1" as logical replication target
DETAIL:  This operation is not supported for foreign tables.
```

論理レプリケーションによる外部テーブルへの更新の反映はできないので、この動作は正常です。

ただ、サーバログに延々とこのメッセージが表示されるのは、ちょっと嫌な感じ。サーバログも膨れ上がってしまうし・・・（レプリケーション系のサーバログはだいたいそういう問題があるとはいえ）。

# おわりに
今回は外部テーブルへ論理レプリケーションを適用しようとするとクラッシュするというバグと、PostgreSQL 16での修正について確認しました。
なお、この問題のFixは、PostgreSQL 15.1にも反映されています。

論理レプリケーションには、まだ思わぬ罠が残っていたりするものですね・・・。