From c15974a90cd584a39ca45eb096226afd2702169c Mon Sep 17 00:00:00 2001 From: fox0430 Date: Thu, 13 Aug 2026 18:14:11 +0900 Subject: [PATCH 1/2] fix(pg_errors, pg_pool): add PoolErrorKind to PgPoolError for programmatic failure-mode discrimination --- async_postgres/pg_client/transaction.nim | 15 ++-- async_postgres/pg_errors.nim | 38 ++++++++-- async_postgres/pg_pool.nim | 61 ++++++++-------- async_postgres/pg_pool_cluster.nim | 4 +- tests/test_e2e_pool.nim | 3 +- tests/test_pool.nim | 90 +++++++++++++++++++----- tests/test_pool_cluster.nim | 3 +- 7 files changed, 150 insertions(+), 64 deletions(-) diff --git a/async_postgres/pg_client/transaction.nim b/async_postgres/pg_client/transaction.nim index cac0d1b..1b397ef 100644 --- a/async_postgres/pg_client/transaction.nim +++ b/async_postgres/pg_client/transaction.nim @@ -150,13 +150,12 @@ proc buildTxBeginAndTimeout*( proc buildRollbackCleanup*(connSym, rollbackTimeout: NimNode): NimNode = ## Build the shared `onCleanupSkipped`-wired ROLLBACK cleanup used on a failed ## attempt by the conn/pool/cluster transaction macros: skip ROLLBACK on an - ## invalidated connection or when the server already ended the transaction - ## (reporting both via `onCleanupSkipped`), otherwise ROLLBACK with - ## `rollbackTimeout` as the per-call timeout and report a swallowed failure. + ## invalidated connection (reported as `csrConnInvalidated` via + ## `onCleanupSkipped`) or a server-ended transaction (`tsIdle`, silent), + ## otherwise ROLLBACK with `rollbackTimeout` and report a swallowed failure. ## ## Cancelled and plain-failure ROLLBACKs are both reported and swallowed so - ## the enclosing `except` can re-raise the original body error — re-raising - ## the cancel would surface a fresh chronos `CancelledError` and mask it. + ## the enclosing `except` can re-raise the original body error. let cleanupErrSym = genSym(nskLet, "cleanupErr") let cleanupCancelSym = genSym(nskLet, "cleanupCancel") let cleanupDefectSym = genSym(nskLet, "cleanupDefect") @@ -195,9 +194,9 @@ proc buildSavepointRollbackCleanup*( ): NimNode = ## Build the shared `onCleanupSkipped`-wired ROLLBACK TO SAVEPOINT cleanup used ## on a failed body by `withSavepoint` / `withSavepointDeadline`: skip on an - ## invalidated connection or when the surrounding transaction has already - ## ended (reporting both via `onCleanupSkipped`), otherwise ROLLBACK TO - ## SAVEPOINT with `rollbackTimeout` as the per-call timeout and report a + ## invalidated connection (reported as `csrConnInvalidated` via + ## `onCleanupSkipped`) or an ended surrounding transaction (`tsIdle`, silent), + ## otherwise ROLLBACK TO SAVEPOINT with `rollbackTimeout` and report a ## swallowed failure. The caller binds `spNameSym` (already quoted via ## `quoteIdentifier`) in the surrounding scope. ## diff --git a/async_postgres/pg_errors.nim b/async_postgres/pg_errors.nim index 519216c..ceac0ab 100644 --- a/async_postgres/pg_errors.nim +++ b/async_postgres/pg_errors.nim @@ -97,13 +97,35 @@ type ## ``PgTimeoutError`` before any ``PgConnectionError`` clause if you need to ## distinguish those. + PoolErrorKind* = enum + ## Machine-readable category of a `PgPoolError`. + pekUnknown + ## Default: a `PgPoolError` built without an explicit `kind`; do not + ## treat as `pekClosed`. + pekClosed ## The pool is permanently closed; retrying cannot succeed. + pekAcquireTimeout + ## An acquire deadline elapsed (`acquireTimeout` or cluster fallback); + ## retrying later may succeed. + pekQueueFull + ## The waiter queue is full (`maxWaiters` bound); retrying later may succeed. + pekConnectFailed + ## A connect attempt failed during acquire (underlying error in `parent`); + ## retrying may succeed. + pekBatchFailed + ## A pipelined batch was unservable; no connection was acquired. + pekDefectWrapped + ## A user-code `Defect` (body/release block or session reset) wrapped to + ## cross an async boundary; preserved as `parent`. + PgPoolError* = object of PgError - ## Pool-level acquire failure: acquire timeout, pool closed, waiter queue - ## full, or a failed connect attempt during acquire (the underlying error, - ## e.g. ``PgConnectionError``, is preserved as ``parent``). + ## Pool-level acquire/operation failure (closed, acquire timeout, queue + ## full, connect failed, unservable batch, or wrapped user-code `Defect`; + ## the underlying error is preserved as ``parent``). ## - ## Also raised by the pooled `with*` macros / `runAndRelease` when a body - ## `Defect` is wrapped (Defect as ``parent``). + ## `kind` classifies the failure programmatically; the message string is + ## informational only. Errors built without `newPoolError` have + ## `kind == pekUnknown`. + kind*: PoolErrorKind ## Failure category (see `PoolErrorKind`). PgNotifyOverflowError* = object of PgError dropped*: int ## Number of notifications dropped due to queue overflow @@ -114,6 +136,12 @@ type reconnectionAttempted*: bool ## True if the pump attempted reconnection before giving up. +template newPoolError*( + errKind: PoolErrorKind, message: string, parentErr: ref Exception = nil +): untyped = + ## Create a `PgPoolError` with the given `errKind` (see `PoolErrorKind`). + (ref PgPoolError)(kind: errKind, msg: message, parent: parentErr) + const # Commonly dispatched-on SQLSTATE codes SqlStateNotNullViolation* = "23502" diff --git a/async_postgres/pg_pool.nim b/async_postgres/pg_pool.nim index 3121234..0a36aea 100644 --- a/async_postgres/pg_pool.nim +++ b/async_postgres/pg_pool.nim @@ -360,7 +360,7 @@ proc resetSession*(pool: PgPool, conn: PgConnection) {.async.} = # Incomplete reset: mark csClosed so the conn is discarded, then re-raise # wrapped (a raw Defect cannot cross a chronos async boundary). conn.state = csClosed - raise newException(PgPoolError, d.msg, d) + raise newPoolError(pekDefectWrapped, d.msg, d) proc computeConnectBackoff*(initial, maxDelay: Duration, failures: int): Duration = ## Exponential backoff for repeated connect failures: returns @@ -503,7 +503,7 @@ proc spawnConnectForWaiter(pool: PgPool) = # decrements `waiterCount` a second time (failLastWaiter below already # did) and permanently corrupts the FIFO fast-path guard. discard pool.failLastWaiter( - newException(PgPoolError, "Pool connect for waiter failed", e) + newPoolError(pekConnectFailed, "Pool connect for waiter failed", e) ) finally: if not consumed and pool.active > 0: @@ -864,7 +864,7 @@ type AcquireResult = tuple[conn: PgConnection, wasCreated: bool] proc acquireImpl(pool: PgPool): Future[AcquireResult] {.async.} = if pool.closed: - raise newException(PgPoolError, "Pool is closed") + raise newPoolError(pekClosed, "Pool is closed") let now = Moment.now() let acquireStart = now @@ -882,10 +882,10 @@ proc acquireImpl(pool: PgPool): Future[AcquireResult] {.async.} = template raiseAcquireTimeout() = pool.metrics.timeoutCount.inc - raise newException(PgPoolError, "Pool acquire timeout") + raise newPoolError(pekAcquireTimeout, "Pool acquire timeout") template raisePoolClosed() = - raise newException(PgPoolError, "Pool is closed") + raise newPoolError(pekClosed, "Pool is closed") template recordAcquire() = pool.metrics.acquireCount.inc @@ -1028,8 +1028,8 @@ proc acquireImpl(pool: PgPool): Future[AcquireResult] {.async.} = # microseconds early). if hasDeadline and remainingBudget() <= milliseconds(1): pool.metrics.timeoutCount.inc - raise newException(PgPoolError, "Pool acquire timeout", e) - raise newException(PgPoolError, "Pool connect failed", e) + raise newPoolError(pekAcquireTimeout, "Pool acquire timeout", e) + raise newPoolError(pekConnectFailed, "Pool connect failed", e) pool.metrics.createCount.inc # A successful caller-driven connect signals the DB is reachable — # let the maintenance loop resume immediate replenishment. @@ -1046,8 +1046,8 @@ proc acquireImpl(pool: PgPool): Future[AcquireResult] {.async.} = # Either max connections are reached or waiters are queued ahead of us; # queue up and wait for delivery. if pool.config.maxWaiters >= 0 and pool.waiterCount >= pool.config.maxWaiters: - raise newException( - PgPoolError, + raise newPoolError( + pekQueueFull, "Pool acquire queue full (maxWaiters=" & $pool.config.maxWaiters & ")", ) # Compute the remaining budget before queueing; whatever the idle @@ -1084,7 +1084,7 @@ proc acquireImpl(pool: PgPool): Future[AcquireResult] {.async.} = except AsyncTimeoutError: pool.metrics.timeoutCount.inc pool.settleAbandonedWaiter(waiter) - raise newException(PgPoolError, "Pool acquire timeout") + raise newPoolError(pekAcquireTimeout, "Pool acquire timeout") except CancelledError as e: # External cancellation (e.g. a caller's `wait()`-style deadline such as # pool.withTransactionDeadline or a cluster fallback timeout) can land @@ -1109,7 +1109,8 @@ proc acquire*(pool: PgPool): Future[PgConnection] {.async.} = ## release. Raises `PgPoolError` on every failure mode: acquire timeout, ## pool closed, waiter queue full, or a failed connect attempt — for ## connect failures the underlying error (e.g. `PgConnectionError`) is - ## preserved as the `parent` of the raised `PgPoolError`. + ## preserved as the `parent` of the raised `PgPoolError`. Use the `kind` + ## field (`PoolErrorKind`) to distinguish the failure mode programmatically. if pool.config.tracer == nil: let ar = await pool.acquireImpl() return ar.conn @@ -1193,7 +1194,7 @@ proc runAndReleaseImpl[T]( if bodyErr != nil: raise bodyErr if bodyDefect != nil: - raise newException(PgPoolError, bodyDefect.msg, bodyDefect) + raise newPoolError(pekDefectWrapped, bodyDefect.msg, bodyDefect) when T isnot void: return res @@ -1235,7 +1236,7 @@ proc buildReleaseAndReraise*(releaseCall, bodyErrSym, bodyDefectSym: NimNode): N # Same-frame Defect from the release path: wrap like the body Defect # (see runAndReleaseImpl), unless it would shadow the body error. if `bodyErrSym` == nil and `bodyDefectSym` == nil: - raise newException(PgPoolError, `releaseDefectSym`.msg, `releaseDefectSym`) + raise newPoolError(pekDefectWrapped, `releaseDefectSym`.msg, `releaseDefectSym`) except CatchableError as `releaseErrSym`: # Never shadow a body error with a release failure. if `bodyErrSym` == nil and `bodyDefectSym` == nil: @@ -1245,7 +1246,7 @@ proc buildReleaseAndReraise*(releaseCall, bodyErrSym, bodyDefectSym: NimNode): N if `bodyDefectSym` != nil: # Wrap the Defect (see runAndReleaseImpl): chronos re-raises raw # Defects eagerly. - raise newException(PgPoolError, `bodyDefectSym`.msg, `bodyDefectSym`) + raise newPoolError(pekDefectWrapped, `bodyDefectSym`.msg, `bodyDefectSym`) macro withConnection*(pool: PgPool, conn, body: untyped): untyped = ## Acquire a connection, execute `body`, then release it back to the pool. @@ -1380,7 +1381,7 @@ proc executeBatch( failPendingOp(op, e) except Defect as d: for op in batch: - failPendingOp(op, newException(PgPoolError, d.msg, d)) + failPendingOp(op, newPoolError(pekDefectWrapped, d.msg, d)) try: await pool.resetSessionAndRelease(conn) except CancelledError as e: @@ -1463,21 +1464,25 @@ proc dispatchHomogeneous( failPendingOp(op, e) except Defect as d: # Wrap the Defect so the op's future fails instead of hanging. - failPendingOp(op, newException(PgPoolError, d.msg, d)) + failPendingOp(op, newPoolError(pekDefectWrapped, d.msg, d)) return # Multi-op path: acquire connections and distribute. var conns: seq[PgConnection] + var acquireErr: ref Exception let nConns = min(ops.len, max(1, maxConns)) for i in 0 ..< nConns: try: let conn = await pool.acquire() conns.add(conn) - except CatchableError: + except CatchableError as e: + acquireErr = e break if conns.len == 0: - let err = newException(PgPoolError, "Failed to acquire connection for batch") + let err = newPoolError( + pekBatchFailed, "Failed to acquire connection for batch", acquireErr + ) for op in ops: failPendingOp(op, err) return @@ -1591,7 +1596,7 @@ proc exec*( ## and stays unlimited. if pool.config.pipelined: if pool.closed: - raise newException(PgPoolError, "Pool is closed") + raise newPoolError(pekClosed, "Pool is closed") let fut = newFuture[CommandResult]("PgPool.exec.pipelined") pool.pendingOps.addLast( PendingPoolOp( @@ -1614,7 +1619,7 @@ proc exec*( ## see the `seq[PgParam]` overload for the batch timeout semantics. if pool.config.pipelined: if pool.closed: - raise newException(PgPoolError, "Pool is closed") + raise newPoolError(pekClosed, "Pool is closed") let fut = newFuture[CommandResult]("PgPool.exec.pipelined") pool.pendingOps.addLast( PendingPoolOp( @@ -1648,7 +1653,7 @@ proc query*( ## and stays unlimited. if pool.config.pipelined: if pool.closed: - raise newException(PgPoolError, "Pool is closed") + raise newPoolError(pekClosed, "Pool is closed") let fut = newFuture[QueryResult]("PgPool.query.pipelined") pool.pendingOps.addLast( PendingPoolOp( @@ -1679,7 +1684,7 @@ proc query*( ## see the `seq[PgParam]` overload for the batch timeout semantics. if pool.config.pipelined: if pool.closed: - raise newException(PgPoolError, "Pool is closed") + raise newPoolError(pekClosed, "Pool is closed") let fut = newFuture[QueryResult]("PgPool.query.pipelined") pool.pendingOps.addLast( PendingPoolOp( @@ -2263,7 +2268,7 @@ macro withTransactionDeadline*(pool: PgPool, args: varargs[untyped]): untyped = `releasedSym` = true # Wrap the Defect (see runAndReleaseImpl) unless it shadows the body error. if `bodyErrSym` == nil and `bodyDefectSym` == nil: - raise newException(PgPoolError, `dSym`.msg, `dSym`) + raise newPoolError(pekDefectWrapped, `dSym`.msg, `dSym`) except CatchableError as `releaseErrSym`: # Already released: re-raise only when the body succeeded. `releasedSym` = true @@ -2275,7 +2280,7 @@ macro withTransactionDeadline*(pool: PgPool, args: varargs[untyped]): untyped = if `bodyDefectSym` != nil: # Wrap the Defect (see runAndReleaseImpl): chronos re-raises raw # Defects eagerly. - raise newException(PgPoolError, `bodyDefectSym`.msg, `bodyDefectSym`) + raise newPoolError(pekDefectWrapped, `bodyDefectSym`.msg, `bodyDefectSym`) checkNoBodyEscapePost( block: `body`, @@ -2459,7 +2464,7 @@ macro withTransactionRetryDeadline*( `releasedSym` = true # Wrap the Defect (see runAndReleaseImpl) unless it shadows the body error. if `bodyErrSym` == nil and `bodyDefectSym` == nil: - raise newException(PgPoolError, `dSym`.msg, `dSym`) + raise newPoolError(pekDefectWrapped, `dSym`.msg, `dSym`) except CatchableError as `releaseErrSym`: # Already released: re-raise only when the body succeeded. `releasedSym` = true @@ -2471,7 +2476,7 @@ macro withTransactionRetryDeadline*( if `bodyDefectSym` != nil: # Wrap the Defect (see runAndReleaseImpl): chronos re-raises raw # Defects eagerly. - raise newException(PgPoolError, `bodyDefectSym`.msg, `bodyDefectSym`) + raise newPoolError(pekDefectWrapped, `bodyDefectSym`.msg, `bodyDefectSym`) checkNoBodyEscapePost( block: `body`, @@ -2547,12 +2552,12 @@ proc close*(pool: PgPool, timeout = ZeroDuration): Future[void] {.async.} = while pool.waiters.len > 0: let waiter = pool.waiters.popFirst() if not waiter.isAbandoned: - waiter.fut.fail(newException(PgPoolError, "Pool closed")) + waiter.fut.fail(newPoolError(pekClosed, "Pool closed")) pool.waiterCount = 0 # Fail all pending pipeline ops pool.dispatchScheduled = false - let closeErr = newException(PgPoolError, "Pool closed") + let closeErr = newPoolError(pekClosed, "Pool closed") while pool.pendingOps.len > 0: let op = pool.pendingOps.popFirst() failPendingOp(op, closeErr) diff --git a/async_postgres/pg_pool_cluster.nim b/async_postgres/pg_pool_cluster.nim index 4ed1de6..5803e2a 100644 --- a/async_postgres/pg_pool_cluster.nim +++ b/async_postgres/pg_pool_cluster.nim @@ -229,8 +229,8 @@ proc acquireRead( return (conn, cluster.primary) except AsyncTimeoutError: asyncSpawn drainAbandonedAcquire(primaryFut) - raise newException( - PgPoolError, + raise newPoolError( + pekAcquireTimeout, "Pool cluster fallback acquire timeout (replica error: " & replicaErr.msg & ")", replicaErr, ) diff --git a/tests/test_e2e_pool.nim b/tests/test_e2e_pool.nim index cf2d2a9..fa40917 100644 --- a/tests/test_e2e_pool.nim +++ b/tests/test_e2e_pool.nim @@ -454,7 +454,8 @@ suite "E2E: Pool Stress": conn2.release() except PgError as e: raised = true - doAssert "timeout" in e.msg.toLowerAscii() + doAssert e of PgPoolError + doAssert (ref PgPoolError)(e).kind == pekAcquireTimeout doAssert raised diff --git a/tests/test_pool.nim b/tests/test_pool.nim index 3dea072..4c7b928 100644 --- a/tests/test_pool.nim +++ b/tests/test_pool.nim @@ -750,6 +750,7 @@ suite "Pool withConnection release-path Defect": caught = e doAssert caught != nil, "the body Defect must surface as PgPoolError" + doAssert caught.kind == pekDefectWrapped doAssert caught.parent of AssertionDefect, "the body Defect must be the parent, not the release Defect" doAssert caught.parent.msg == "body defect", @@ -789,6 +790,7 @@ suite "Pool withConnection release-path Defect": caught = e doAssert caught != nil, "the release-path Defect must surface as PgPoolError" + doAssert caught.kind == pekDefectWrapped doAssert caught.parent of AssertionDefect, "the Defect must be preserved as parent" # asyncdispatch appends an async traceback to the message. @@ -860,6 +862,7 @@ suite "Pool withConnection release-path Defect": caught = e doAssert caught != nil, "the body Defect must surface as PgPoolError" + doAssert caught.kind == pekDefectWrapped doAssert caught.parent of AssertionDefect, "the body Defect must be the parent, not the release Defect" # asyncdispatch appends an async traceback to the message. @@ -1750,13 +1753,16 @@ suite "Max waiters": check pool.waiters.len == 2 # Third should be rejected immediately - var msg = "" + var caught: ref PgPoolError try: discard waitFor pool.acquire() - except PgError as e: - msg = e.msg + except PgPoolError as e: + caught = e + except PgError: + discard - check "queue full" in msg.toLowerAscii() + check caught != nil + check caught.kind == pekQueueFull check pool.waiters.len == 2 # Clean up @@ -2886,25 +2892,27 @@ suite "FIFO fairness": doAssert pool.waiterCount == 1 doAssert pool.active == 1 - var errA = "" + var errA: ref PgPoolError = nil try: discard await futA except PgPoolError as e: - errA = e.msg - doAssert "Pool connect failed" in errA + errA = e + doAssert errA != nil + doAssert errA.kind == pekConnectFailed # With the fix, B is served by the spawn (which also fails against the # unresponsive mock) and returns fast; without it, B would time out on - # acquireTimeout with "Pool acquire timeout". - var errB = "" + # acquireTimeout with pekAcquireTimeout. + var errB: ref PgPoolError = nil let bStart = Moment.now() try: discard await futB except PgPoolError as e: - errB = e.msg + errB = e let bElapsed = Moment.now() - bStart - doAssert "Pool connect for waiter failed" in errB - doAssert "acquire timeout" notin errB + doAssert errB != nil + doAssert errB.kind == pekConnectFailed + doAssert errB.kind != pekAcquireTimeout doAssert bElapsed < seconds(2) await pool.close() @@ -2932,14 +2940,15 @@ suite "Error type granularity": discard pool.acquire() # fills the waiter queue - var caught = false + var caught: ref PgPoolError try: discard waitFor pool.acquire() - except PgPoolError: - caught = true + except PgPoolError as e: + caught = e except PgError: discard - check caught + check caught != nil + check caught.kind == pekQueueFull # Clean up pool.release(mockConn()) @@ -2998,6 +3007,48 @@ suite "Error type granularity": waitFor t() + test "pipelined batch acquire failure fails every op with pekBatchFailed": + # Multi-op dispatch arm: every acquire fails (connection refused), so the + # batch cannot be served — each op's future must fail with pekBatchFailed + # (not hang, and not leak the underlying acquire error kind). + proc t() {.async.} = + let ms = startMockServer() + let port = ms.port + await closeServer(ms) # guaranteed connection-refused port + + let pool = makePool(maxSize = 1) + pool.config.pipelined = true + pool.config.connConfig.host = "127.0.0.1" + pool.config.connConfig.port = port + pool.config.connConfig.connectTimeout = milliseconds(500) + + # Two ops queued in the same tick form one batch (multi-op dispatch arm). + let futA = pool.exec("SELECT 1") + let futB = pool.exec("SELECT 2") + + for fut in [futA, futB]: + var caught: ref PgPoolError + try: + discard await fut + except PgPoolError as e: + caught = e + doAssert caught != nil, "batch op must fail with PgPoolError, not hang" + doAssert caught.kind == pekBatchFailed + doAssert caught.parent != nil, + "the failed acquire must be preserved as the parent" + doAssert caught.parent of PgPoolError + doAssert (ref PgPoolError)(caught.parent).kind == pekConnectFailed + + await pool.close() + + waitFor t() + + test "PgPoolError without newPoolError has kind pekUnknown": + # Legacy construction (newException) leaves kind at its zero value, which + # must not be mistaken for pekClosed (see PoolErrorKind). + let err = newException(PgPoolError, "legacy construction") + check err.kind == pekUnknown + test "spawn connect failure fails waiter with PgPoolError with parent": # Waiter path: a broken-conn release kicks off spawnConnectForWaiter; # its connect failure must reach the queued acquire as PgPoolError @@ -3099,7 +3150,7 @@ suite "Error type granularity": doAssert pool.waiterCount == 1 # waiter still queued, not failed doAssert pool.active == 0 # reservation released by `finally` - # close() settles the still-queued waiter with "Pool closed". + # close() settles the still-queued waiter with pekClosed. await pool.close() var caught: ref PgPoolError try: @@ -3107,6 +3158,7 @@ suite "Error type granularity": except PgPoolError as e: caught = e doAssert caught != nil + doAssert caught.kind == pekClosed await closeServer(ms) waitFor t() @@ -3539,7 +3591,7 @@ suite "Pool acquire close-race": doAssert acquireFut.failed let err = acquireFut.readError() doAssert err of PgPoolError - doAssert "closed" in err.msg + doAssert (ref PgPoolError)(err).kind == pekClosed doAssert pool.active == 0 doAssert pool.metrics.createCount == 1 doAssert pool.metrics.closeCount == 1 @@ -3589,7 +3641,7 @@ suite "Pool acquire close-race": doAssert acquireFut.failed let err = acquireFut.readError() doAssert err of PgPoolError - doAssert "closed" in err.msg + doAssert (ref PgPoolError)(err).kind == pekClosed doAssert pool.active == 0 doAssert pool.idle.len == 0 diff --git a/tests/test_pool_cluster.nim b/tests/test_pool_cluster.nim index e8f1894..a8975c9 100644 --- a/tests/test_pool_cluster.nim +++ b/tests/test_pool_cluster.nim @@ -379,7 +379,8 @@ suite "Fallback": raised = e check raised != nil - check "fallback acquire timeout" in raised.msg + check raised of PgPoolError + check (ref PgPoolError)(raised).kind == pekAcquireTimeout # The replica failure that triggered the fallback is preserved as the cause. check raised.parent != nil check "Pool is closed" in raised.parent.msg From 17d4e69978421a38c2a8354a22f97b13d1033bf0 Mon Sep 17 00:00:00 2001 From: fox0430 Date: Thu, 13 Aug 2026 18:45:27 +0900 Subject: [PATCH 2/2] nph --- async_postgres/pg_errors.nim | 3 +-- async_postgres/pg_pool.nim | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/async_postgres/pg_errors.nim b/async_postgres/pg_errors.nim index ceac0ab..abcf383 100644 --- a/async_postgres/pg_errors.nim +++ b/async_postgres/pg_errors.nim @@ -111,8 +111,7 @@ type pekConnectFailed ## A connect attempt failed during acquire (underlying error in `parent`); ## retrying may succeed. - pekBatchFailed - ## A pipelined batch was unservable; no connection was acquired. + pekBatchFailed ## A pipelined batch was unservable; no connection was acquired. pekDefectWrapped ## A user-code `Defect` (body/release block or session reset) wrapped to ## cross an async boundary; preserved as `parent`. diff --git a/async_postgres/pg_pool.nim b/async_postgres/pg_pool.nim index 0a36aea..4e59b1f 100644 --- a/async_postgres/pg_pool.nim +++ b/async_postgres/pg_pool.nim @@ -1480,9 +1480,8 @@ proc dispatchHomogeneous( break if conns.len == 0: - let err = newPoolError( - pekBatchFailed, "Failed to acquire connection for batch", acquireErr - ) + let err = + newPoolError(pekBatchFailed, "Failed to acquire connection for batch", acquireErr) for op in ops: failPendingOp(op, err) return