{"record":{"id":"8990f21fd3f49775","repo":"gastownhall/beads","slug":"failed-to-read-borrowed-conn-s-active-branch-w","errorCode":null,"errorMessage":"failed to read borrowed conn's active branch: %w","messagePattern":"failed to read borrowed conn's active branch: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/storage/dolt/transaction.go","lineNumber":390,"sourceCode":"//\n// Pool invariant: DOLT_CHECKOUT is session-level, and the borrow cleanup\n// returns the connection to the pool as-is — so switching its branch here\n// would leak a foreign branch into the pool for an unrelated later caller.\n// Every other production checkout site (federation staging, compact, flatten)\n// restores the branch before releasing the connection; the borrow path\n// preserves the same invariant by refusing instead of switching. Today no\n// shipped flow diverges a pool session's branch from the regular tx's branch\n// (DoltStore.Checkout has no non-test callers), so this is defense in depth\n// for future Checkout callers and for multi-connection tests.\n//\n// Instead of an unconditional checkout it verifies the session is already on\n// the requested branch — the overwhelmingly common case — and sends the\n// caller to the fresh-dial fallback otherwise. Same round-trip count as the\n// checkout it replaces (one statement), so the borrow fast path stays free.\nfunc beginBorrowedTx(ctx context.Context, conn *sql.Conn, branch string) (*sql.Tx, error) {\n\tvar active string\n\tif err := conn.QueryRowContext(ctx, \"SELECT active_branch()\").Scan(&active); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read borrowed conn's active branch: %w\", err)\n\t}\n\tif active != branch {\n\t\treturn nil, fmt.Errorf(\"borrowed conn is on branch %q, want %q: refusing to switch a pooled session's branch\", active, branch)\n\t}\n\ttx, err := conn.BeginTx(ctx, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to begin ignored tx: %w\", err)\n\t}\n\treturn tx, nil\n}\n\n// beginTxOnConn checks a connection out to branch and begins a transaction on\n// it. Only the fallback path uses it: the fallback owns a dedicated\n// single-connection pool, so checking its session out is safe. Every Dolt SQL\n// session has its own active branch, so the explicit checkout is required on\n// a fresh dial.\nfunc beginTxOnConn(ctx context.Context, conn *sql.Conn, branch string) (*sql.Tx, error) {\n\tif _, err := conn.ExecContext(ctx, \"CALL DOLT_CHECKOUT(?)\", branch); err != nil {","sourceCodeStart":372,"sourceCodeEnd":408,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/dolt/transaction.go#L372-L408","documentation":"beginBorrowedTx first queries `SELECT active_branch()` on the borrowed pooled connection to confirm it is already on the regular transaction's branch. This error wraps that read failing — a stale/dead pooled session, ctx cancellation (including the 250ms ignoredTxBorrowTimeout parent), or a non-Dolt server. On error the borrow path falls back to a fresh dial, so this is usually transient.","triggerScenarios":"The borrowed pooled connection died since it was parked (server restart, wait_timeout); ctx canceled during the query; server degraded so even trivial queries fail.","commonSituations":"Idle pools left open across server restarts; hosted-gateway load balancers dropping idle sessions; very short deadlines propagating into transaction setup.","solutions":["Retry — the code automatically discards the borrowed conn and retries via a fresh-dial fallback, so this only surfaces if you wrap/observe it directly","Tune server idle timeouts (wait_timeout) above your busiest transaction duration","Check ctx deadlines; widen them if the 250ms borrow timeout or parent deadline is expiring","Enable pool health checks / connection lifetimes (ConnMaxLifetime) shorter than server idle timeout"],"exampleFix":"// before\ndb.SetConnMaxLifetime(0) // sessions can idle past server wait_timeout and die\n// after\ndb.SetConnMaxLifetime(30 * time.Second) // recycle before server drops idle sessions","handlingStrategy":"fallback","validationCode":"if err := db.PingContext(ctx); err != nil { /* recycle pool before writes */ }","typeGuard":null,"tryCatchPattern":"// the library already falls back to a fresh dial; when observing this error:\nif err != nil && strings.Contains(err.Error(), \"failed to read borrowed conn's active branch\") {\n    log.Warn(\"stale borrowed session; fresh-dial fallback engaged\")\n}","preventionTips":["Set ConnMaxLifetime shorter than the server's idle/wait_timeout","Recycle or Ping the pool after any Dolt server restart","Avoid propagating tiny parent deadlines into transaction setup","Monitor dropped-connection metrics from load balancers/gateways"],"tags":["go","dolt","connection-pool","stale-connection"],"backgroundTag":"stale-pooled-connection","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}