Skip to content

fix(driver/postgres): direct-mode COPY leaves orphaned pgRequest in pending queue when ctx is canceled #241

@FumingPower3925

Description

@FumingPower3925

Bug

`copyFromDirect` / `copyToDirect` path in `driver/postgres/conn.go`:

```go
if c.useDirect {
select {
case <-req.doneCh:
if req.err != nil { return 0, req.err }
case <-ctx.Done():
return 0, ctx.Err() // <— ORPHAN
}
}
```

When `ctx` is canceled mid-copy:

  1. The caller returns `ctx.Err()`.
  2. `req` stays enqueued in `c.pending`.
  3. The `startDirectReader` goroutine is stopped by its `defer stop()` via `SetReadDeadline` — it exits without ever signaling `req.doneCh`.
  4. On the NEXT query on the same conn, `dispatch` pops `req` as the head of `pending` but the response bytes belong to the new query — wire-format desync, undefined behavior.

Contrast: loop-mode path is correct

In loop mode the same code path calls `c.wait(ctx, req)`, which handles `ctx.Done` properly: sends `CancelRequest`, waits bounded 30s for the server's Error + ReadyForQuery, and only returns after the event loop has driven `req` to completion via `onRecv`. No orphan.

Fix

Mirror `wait()`'s cancellation shape in direct-mode COPY:

  1. On `ctx.Done`, send `CancelRequest` to the server (PG side-channel).
  2. Continue driving the reader goroutine for up to ~30s to consume the server's Error + RFQ.
  3. If `req.doneCh` closes within that window, good — return `ctx.Err()`.
  4. If not, forcibly fail the conn (`c.failAll`) so the pool discards it on release.

Apply to both `copyFrom` direct-mode branch and `copyTo` direct-mode branch.

Reproducer (sketch)

```go
ctx, cancel := context.WithCancel(context.Background())
go func() { time.Sleep(5 * time.Millisecond); cancel() }()
_, err := pool.CopyFrom(ctx, "big_table", cols, slowSrc)
// err == context.Canceled, good.
// Now reuse the same conn for a normal query:
_ = pool.QueryRow(context.Background(), "SELECT 1").Scan(&x)
// Response will be the tail of the canceled copy's wire bytes, not SELECT 1's.
```

Scope

  • driver/postgres/conn.go:copyFrom (direct-mode ctx.Done branch)
  • driver/postgres/conn.go:copyTo (direct-mode ctx.Done branch)

Priority

Blocker for v1.4.0 tag. Wire-format desync is the worst class of driver bug — silent corruption that outlives the cancellation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority/critical-pathBlocks other milestone work

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions