--- description: 320-allographerpostgresql-非同期待機改善-設計書ブランチでの開発時に読み込む alwaysApply: false --- allographer(PostgreSQL) 非同期待機改善 ブランチルール === このブランチで実装することは以下の通りです。 - allographer の PostgreSQL 実装に残っている `sleepAsync(10)` ベースの非同期待機を、libpq ソケット readiness ベースの待機へ置き換える。 - `query*`, `queryPlain*`, `exec*`, `getColumns*` など、同一問題を持つ待機経路を共通化して修正する。 - `dbOpen` 時に PostgreSQL 接続を non-blocking 化し、libpq 非同期 API の前提を明示的に保証する。 - 接続プール空き待機を `sleepAsync(10)` ポーリングから通知ベースへ移行し、プール枯渇時の待機遅延を抑制する。 - 公開 API 互換を維持したまま、httpbeast/httpx 利用時の DB 完了後の回収遅延を縮小する。 ## 進捗 - [x] `src/allographer/query_builder/libs/postgres/postgres_impl.nim` の待機ループを調査し、`sleepAsync(10)` 依存箇所を一覧化する - [x] `src/allographer/query_builder/libs/postgres/postgres_rdb.nim` を確認し、既存宣言で再利用できる libpq API と不足宣言を整理する(追加宣言不要。`pqsocket` / `pqflush` / `pqsetnonblocking` 等は既存) - [x] PostgreSQL 待機ヘルパ `waitPgReadable` / `waitPgWritable` / `cancelQuery` の責務と配置を決める(`postgres_impl.nim` 内の非公開プロシージャ) - [x] `PQsocket` / `PQconsumeInput` / `PQisBusy` / `PQgetResult` / `PQflush` に基づく共通クエリ待機ループを設計する(`pgFlushOutgoing` / `pgNextResult` / `pgEnsureIdle`) - [x] `query*`, `queryPlain*`, `exec*`, `getColumns*` の各経路を共通待機ロジックへ寄せる - [x] タイムアウト時の `cancelQuery` 実行とエラーメッセージ方針を統一する(タイムアウトは `cancelQuery` 後に `DbError`) - [x] `src/allographer/query_builder/models/postgres/postgres_open.nim` で `pqsetnonblocking(conn, 1)` と検証処理を追加する - [x] `src/allographer/query_builder/models/postgres/postgres_types.nim` に接続プール waiter キューを追加する(`waiters*: seq[Future[void]]`) - [x] `src/allographer/query_builder/models/postgres/postgres_exec.nim` の `getFreeConn` / `returnConn` を通知ベースへ置換する - [x] PostgreSQL の単体・結合・回帰テストを追加または更新する(`tests/postgres/test_pool_wait.nim` 追加、既存 `test*.nim` 回帰) - [x] `sleepAsync(10)` が PostgreSQL クエリ待機とプール空き待機から除去されたことを確認する - [ ] README / documents / ルール文書に仕様差分が必要か確認し、必要分を同期する(利用者向け公開 API 変更なしのため未着手。必要なら追記) - [x] MariaDB: `mariadb_rdb.nim` に `mysql_read_query_result`(`cdecl`)を追加 - [x] MariaDB: `mariadb_impl.nim` は接続ブロッキング前提で `mysql_real_query` / `mysql_read_query_result` / `mysql_fetch_row` / `free_result` を同期利用(`mysql_read_query_result_*` / `fetch_row_*` の async ループは環境でタイムアウト・デッドロックのため不使用) - [x] MariaDB: `mariadb_open.nim` は `MYSQL_OPT_NONBLOCK` を付与しない(同上) - [x] MariaDB: `mariadb_types` / `mariadb_exec` のプール空き待ちは通知ベース(`tests/mariadb/test_pool_wait.nim`) ## 参考資料 - allographer の PostgreSQL 実装: `src/allographer/query_builder/libs/postgres/postgres_impl.nim` - allographer の PostgreSQL 実行管理: `src/allographer/query_builder/models/postgres/postgres_exec.nim` - allographer の PostgreSQL 接続初期化: `src/allographer/query_builder/models/postgres/postgres_open.nim` - allographer の PostgreSQL 型定義: `src/allographer/query_builder/models/postgres/postgres_types.nim` - libpq 非同期 API: `PQsocket`, `PQconsumeInput`, `PQisBusy`, `PQgetResult`, `PQsetnonblocking`, `PQflush`, `PQgetCancel`, `PQcancel`, `PQfreeCancel` - Nim `asyncdispatch` のイベントループ仕様 - httpbeast / httpx の POSIX selector ループ実装 ## 調査結果・設計まとめ ### 背景 `httpbeast` / `httpx` 利用時のみ、DB クエリ待機が `asynchttpserver` より大きく悪化する事象がある。 現状の allographer PostgreSQL 実装は、libpq 非同期 API を使いながらも、実際の待機を `await sleepAsync(10)` に依存している。 ### 問題の本質 - 現状の PostgreSQL 待機は、ソケット readiness 待ちではなく 10ms 刻みのポーリングである - Nim の `asyncdispatch` は `poll` / `waitFor` / `runForever` が回らない限りタイマーも進まない - httpbeast / httpx の POSIX 経路は `selectInto(-1, ...)` による無期限ブロックを中心としており、`asyncdispatch.poll(0)` は限定的にしか回らない - そのため `sleepAsync(10)` は実時間 10ms で復帰せず、「次に HTTP 側ループがたまたま dispatcher を回すまで」遅延する - 結果として、DB は完了済みでも allographer 側の結果回収が遅れ、体感レイテンシが悪化する ### 設計目標 1. PostgreSQL クエリ待機から `sleepAsync` ポーリングを除去する 2. libpq ソケットの可読・可書込イベントで待機する 3. 接続プール空き待ちも通知ベースへ移行し、タイマー依存を減らす 4. `dbOpen`, `table`, `select`, `find`, `update` などの公開 API 互換を維持する ### 非目標 1. Query Builder API の破壊的変更 2. MySQL / SQLite への同時展開(MariaDB は PostgreSQL と同方針で非同期待機を実装済み) 3. 初回対応で全 OS 最適化を完了させること ### 実装方針 #### 1. libpq 待機を FD イベント待機へ変更する 変更対象: - `src/allographer/query_builder/libs/postgres/postgres_impl.nim` - `src/allographer/query_builder/libs/postgres/postgres_rdb.nim` 追加する内部ヘルパ: - `waitPgReadable(db: PPGconn, timeoutMs: int): Future[bool]` - `waitPgWritable(db: PPGconn, timeoutMs: int): Future[bool]` - `cancelQuery(db: PPGconn)` 要点: - `pqsocket(db)` で FD を得る - 必要に応じて dispatcher に `AsyncFD` を登録する - read / write readiness を Future 完了で待つ - タイムアウト管理を行う - 登録リークや未解除ハンドラを残さない #### 2. クエリ実行ループを共通化する 送信フェーズ: 1. `pqsendQueryParams(...)` を呼ぶ 2. `pqflush == 1` の間は `waitPgWritable` で待機する 受信フェーズ: 1. `pqconsumeInput` 2. `pqisBusy == 1` なら `waitPgReadable` で待機する 3. ready 後に `pqgetResult` を drain する タイムアウト時は `cancelQuery` を実行し、エラーを返す。 #### 3. `dbOpen` で non-blocking を保証する 変更対象: - `src/allographer/query_builder/models/postgres/postgres_open.nim` 仕様: 1. `pqsetnonblocking(conn, 1)` を実行する 2. 失敗時は `DbError` 3. `pqisnonblocking(conn) == 1` を確認し、保証できない場合は例外化する #### 4. 接続プール空き待ちを通知ベースに変更する 変更対象: - `src/allographer/query_builder/models/postgres/postgres_types.nim` - `src/allographer/query_builder/models/postgres/postgres_exec.nim` 方針: - `Connections` に `waiters*: Deque[Future[void]]` を追加する - `getFreeConn` は空きがなければ waiter を登録して待機する - `returnConn` は `isBusy = false` にした後、未完了 waiter を 1 つだけ起こす - 余剰 waiter や完了済み waiter は掃除する ### 失敗時挙動 1. `pqsocket < 0` は `DbError` 2. タイムアウト時は `cancelQuery` 実行後に `DbError` 3. `cancelQuery` 失敗時は追加情報付きで例外化 4. dispatcher 登録・解除エラーは握り潰さず上位へ伝播する ### テスト方針 単体テスト: 1. `waitPgReadable` が read event で復帰する 2. timeout 時に `false` を返す 3. `cancelQuery` が後始末まで行う 4. `getFreeConn` が通知で復帰する 結合テスト: 1. httpbeast / httpx + Postgres のレイテンシ回帰を確認する 2. `asynchttpserver` と比較して極端な乖離がないことを確認する 3. プール枯渇時の待機が 10ms 刻みポーリングに依存しないことを確認する 回帰テスト: 1. 既存 CRUD クエリの結果互換 2. transaction 系 API の動作 3. 例外時メッセージが意味を保っていること ### 受け入れ基準 1. PostgreSQL クエリ待機ループから `await sleepAsync(10)` が除去されている 2. httpbeast / httpx で DB 完了後の回収遅延が顕著に増幅しない 3. プール空き待機で `sleepAsync` ポーリングを使わない 4. 公開 API 変更なしで既存テストと追加テストが成立する