git bisectでPostgreSQLのバグ混入コミットを特定しよう！

これは、[PostgreSQL Advent Calendar 2024](https://qiita.com/advent-calendar/2024/postgresql)の8日目の記事です。

今回は、`git bisect`を使って、PostgreSQLのバグ混入コミットを特定する方法を試してみます。

### バグの概要
今回取り上げるのは、PostgreSQL開発コミュニティのメーリングリストの以下スレッドで報告・議論されている一時テーブルに関するPostgreSQLのバグです。このバグは、本記事の執筆時点(2024年11月30日)でまだ解決されていません。
* https://postgr.es/m/CAMEv5_syU0ZopE-2Wr8A8QksqrCyYT2hW06Rgw4RSPdyJO-=fw@mail.gmail.com
* https://postgr.es/m/CAJDiXgj72Axj0d4ojKdRWG_rnkfs4uWY414NL=15sCvh7-9rwg@mail.gmail.com

通常、一時テーブルにはそのテーブルを作成したセッションからしかレコードを挿入できません。しかし、このバグにより、別のセッションから初回のINSERTが成功してしまいます。

例えば、セッションAで以下のように一時テーブルを作成したとき、
```sql
=# CREATE TEMP TABLE test (i int);
CREATE TABLE

=# SELECT schemaname, tablename FROM pg_tables WHERE tablename = 'test';
 schemaname | tablename 
------------+-----------
 pg_temp_3  | test
(1 row)
```

セッションBから一時テーブルにINSERTを実行すると、本来はエラーになるべきです。
```sql
=# INSERT INTO pg_temp_3.test VALUES(1);
ERROR:  cannot access temporary tables of other sessions
```

しかし、バグのあるPostgreSQLのバージョン(例えば記事執筆時点のv17最新マイナーバージョンのv17.2)では初回だけ成功してしまいます。
```sql
=# INSERT INTO pg_temp_3.test VALUES(1);
INSERT 0 1

=# INSERT INTO pg_temp_3.test VALUES(2);
ERROR:  cannot access temporary tables of other sessions
```

### バグ混入コミットを特定する
`git bisect`を使って、以下のステップでバグ混入コミットを特定していきます。

**① 手元環境にPostgreSQLのgitレポジトリがなければ、`git clone`でクローンします**
```bash
$ git clone https://github.com/postgres/postgres.git
$ cd postgres
```

**② `git bisect start`で探索する範囲を指定します**
今回は、「v15までは正常に動作していた(バグはなかった)」という報告[^1]を参考に、探索範囲としてv16リリース付近(2023-09-14)のコミットf062cddafeから、v15beta版リリース前(2022-05-01)のコミットed57cac84dまでを指定して`git bisect start`を実行します。
```bash
$ git log --oneline -n 1 --until=2023-09-14
f062cddafe Fix the ALTER SUBSCRIPTION to reflect the change in run_as_owner option.

$ git log --oneline -n 1 --until=2022-05-01
ed57cac84d pg_walinspect: fix case where flush LSN is in the middle of a record.

$ git bisect start f062cddafe ed57cac84d
```
[^1]: [It looks like this did work as expected up through v15.](https://postgr.es/m/260666.1732142857@sss.pgh.pa.us)

**③ バグが含まれているか確認するテストスクリプトを用意します**
今回は、バグが含まれているか確認するために、PostgreSQLをソースからビルドして一時テーブルの動作を検証する以下のテストスクリプトを`~/test.sh`に用意します。
```bash
#!/bin/sh

# ゴミファイルを削除した上で、PostgreSQLをビルドする
# --without-icuは手元環境の都合で指定している
git clean -d -x -f > /dev/null
./configure --prefix=$(pwd)/test --quiet --without-icu
make -j 4 -s install

# PostgreSQLを起動する
cd test
bin/initdb -D data --no-locale --encoding=UTF8
bin/pg_ctl -D data start

# 1つ目のセッションで一時テーブルを作成する
PSQL="bin/psql --no-psqlrc -d postgres -Atc"
$PSQL "BEGIN; CREATE TEMP TABLE t(i int); COMMIT; SELECT pg_sleep(1000)" &

# 2つ目のセッションで一時テーブルにINSERTして、その成功/失敗を変数に保存する
sleep 3
TBL=$($PSQL "SELECT schemaname || '.' || tablename FROM pg_tables WHERE tablename = 't'")
$PSQL "INSERT INTO $TBL VALUES (1)"
RES=$?

# PostgreSQLを停止して、実行環境を削除する
bin/pg_ctl -D data stop -m i
cd ..
rm -rf test

# 別セッションからの一時テーブルへのINSERTが成功した場合は、
# バグが含まれていることになるため、テストスクリプトとしては
# 失敗(終了ステータス=1)を報告させる。さもなければ、
# INSERTは失敗していてバグは含まれていないことから
# 成功(終了ステータス=0)を報告させる
if [ $RES -eq 0 ]; then
  exit 1
else
  exit 0
fi
```

**④ テストスクリプトを指定して`git bisect run`を実行し、バグ混入コミットを自動で特定します**
手元環境では、`git bisect run`を実行してしばらく放置すると、45分程度でバグ混入コミットを特定できました。
```bash
$ git bisect run ~/test.sh

...

00d1e02be24987180115e371abaeb84738257ae2 is the first bad commit
commit 00d1e02be24987180115e371abaeb84738257ae2
Author: Andres Freund <andres@anarazel.de>
Date:   Thu Apr 6 16:35:21 2023 -0700

    hio: Use ExtendBufferedRelBy() to extend tables more efficiently
    
    While we already had some form of bulk extension for relations, it was fairly
    limited. It only amortized the cost of acquiring the extension lock, the
    relation itself was still extended one-by-one. Bulk extension was also solely
    triggered by contention, not by the amount of data inserted.

...

bisect found first bad commit
```

ちなみに、探索の過程を確認すると、以下の通りでした。
```bash
$ git bisect log
# bad: [f062cddafe6b3dcb6d090111c7cd4afa0c7fa4b3] Fix the ALTER SUBSCRIPTION to reflect the change in run_as_owner option.
# good: [ed57cac84d1c5642737dab1e4c4b8cb4f0c4305f] pg_walinspect: fix case where flush LSN is in the middle of a record.
git bisect start 'f062cddafe' 'ed57cac84d'
# good: [b5d0f8ec01c021452203b2fd3921c9b55f6c3cd3] Allow parent's WaitEventSets to be freed after fork().
git bisect good b5d0f8ec01c021452203b2fd3921c9b55f6c3cd3
# bad: [f3fa31327ecba75ee0e946abaa56dbf471ba704b] Add pg_buffercache_usage_counts() to contrib/pg_buffercache.
git bisect bad f3fa31327ecba75ee0e946abaa56dbf471ba704b
# good: [4fc53819a45fe6e7233a69bb279557b2070dcc40] meson: windows: Fix tmp_install + prefix computation with meson 1.0.1
git bisect good 4fc53819a45fe6e7233a69bb279557b2070dcc40
# good: [869650fa86adf74fca7f566c9317f6310f8b400c] Support language tags in older ICU versions (53 and earlier).
git bisect good 869650fa86adf74fca7f566c9317f6310f8b400c
# good: [8aaa04b32d790da595684de58ae4fc2db96becff] Track shared buffer hits in pg_stat_io
git bisect good 8aaa04b32d790da595684de58ae4fc2db96becff
# good: [b8059bdf1ef8f7e215ec5010fcf6c3bd9c491eef] docs: html: load stylesheet via custom.css.source
git bisect good b8059bdf1ef8f7e215ec5010fcf6c3bd9c491eef
# good: [acab1b0914e426d28789731f50f5964dd4d2f054] Convert many uses of ReadBuffer[Extended](P_NEW) to ExtendBufferedRel()
git bisect good acab1b0914e426d28789731f50f5964dd4d2f054
# good: [7d71d3dd080b9b147402db3365fe498f74704231] Refresh cost-based delay params more frequently in autovacuum
git bisect good 7d71d3dd080b9b147402db3365fe498f74704231
# bad: [8fcb32db98eda1ad2a0c0b40b1cbb5d9a7aa68f0] Add more protections in WAL record APIs against overflows
git bisect bad 8fcb32db98eda1ad2a0c0b40b1cbb5d9a7aa68f0
# bad: [00d1e02be24987180115e371abaeb84738257ae2] hio: Use ExtendBufferedRelBy() to extend tables more efficiently
git bisect bad 00d1e02be24987180115e371abaeb84738257ae2
# good: [1cbbee03385763b066ae3961fc61f2cd01a0d0d7] Add VACUUM/ANALYZE BUFFER_USAGE_LIMIT option
git bisect good 1cbbee03385763b066ae3961fc61f2cd01a0d0d7
# first bad commit: [00d1e02be24987180115e371abaeb84738257ae2] hio: Use ExtendBufferedRelBy() to extend tables more efficiently
```

**⑤ 最後に、`git bisect reset`でレポジトリの状態を元に戻します**
```bash
$ git bisect reset
```

### さいごに
今回特定したバグ混入コミット 00d1e02be24987180115e371abaeb84738257ae2 は、リレーションの拡張を効率化し、PostgreSQLの性能を向上させるものでした。しかし、この変更により、INSERT時に、他セッションの一時テーブルへのアクセスをチェックしない実行パスが生まれてしまい、今回の問題が発生したようです。

PostgreSQLでは、機能や性能が改善される過程で、まれに新しいバグが紛れ込むことがあります。そのようなバグを発見した際は、ぜひ`git bisect`を活用して原因となったコミットを特定してみてください！