Skip to content

fix(wallet-toolbox): cold makeAvailable() inside a caller's transaction self-deadlocks on SQLite's single-connection pool #540

Description

@E-Jacko

Area

Wallet

Affected package or service and version

@bsv/wallet-toolboxsrc/storage/StorageKnex.ts:489, src/storage/StorageReader.ts:58-62. Current on main at 82ab4d3c; reproduced against a built 2.10.2.

Defect

getRawTxOfKnownValidTransaction() warms the settings cache inline:

// StorageKnex.ts:489
if (!this.isAvailable()) await this.makeAvailable()

makeAvailable() takes no trx at all:

// StorageReader.ts:58-62
async makeAvailable (): Promise<TableSettings> {
  if (this._settings != null) return this._settings
  this._settings = await this.readSettings()
  return this._settings
}

so its readSettings() always runs outside whatever transaction the caller holds — even though StorageKnex.readSettings(trx?) accepts one (:177), and even though getRawTxOfKnownValidTransaction has a trx in scope that it passes to the very next statements (:490, :492).

On SQLite's single-connection pool, a caller already inside a write transaction that reaches this path while the settings cache is cold requests a second connection that can never be granted, and self-deadlocks until acquireConnectionTimeout fires.

This is reachable from inside the package — these all pass a live trx into that method:

  • src/storage/methods/createAction.ts:1538
  • src/storage/StorageProvider.ts:1819
  • src/storage/StorageKnex.ts:1413validateRawTransaction(t, args.trx):1648

Minimal reproduction

Open a store cold (do not call makeAvailable()), then reach the path from inside a transaction:

const storage = new StorageKnex({ chain, knex, /* ... */ })   // cold: _settings empty
await storage.transaction(async trx => {
  await storage.getRawTxOfKnownValidTransaction(txid, undefined, undefined, trx)
})

Expected behavior

Either the settings read joins the caller's transaction (readSettings(trx) already supports it), or the cold-inside-transaction case throws a clear error. Not a connection-pool timeout.

Actual behavior and evidence

Same script twice, differing only in whether makeAvailable() was called at boot first:

COLD (no boot warm): available before trx=false  ok=false  5002ms
                     KnexTimeoutError: Knex: Timeout acquiring a connection. The pool is
                     probably full. Are you missing a .transacting(trx) call?
WARM (boot warm)   : available before trx=true   ok=true   1ms

better-sqlite3 + knex 3.3 with acquireConnectionTimeout: 5000. The 5002 ms is the timeout firing, not work being done.

Consumers can only mitigate this by warming settings at boot before serving, which masks the cause rather than fixing it, and silently stops working if any future path reaches a cold instance inside a transaction.

Environment

macOS, Node 22, better-sqlite3 13 / knex 3.3 — the versions @bsv/wallet-toolbox itself declares.

Notes on the fix

Thread trx through the warm on this path — makeAvailable(trx?) delegating to the existing readSettings(trx) — or, at minimum, detect cold-inside-transaction and throw a named error instead of exhausting the pool. verifyReadyForDatabaseAccess(trx?) (:1663) already demonstrates the trx-aware settings read.

Related but separate, and not claimed here as a live defect: countChangeInputs and allocateChangeInput accept no trx and the latter opens its own knex.transaction(...). I traced the callers and found no in-package path reaching them from inside a transaction (createAction's funding allocates in memory over candidates fetched with the caller's trx), so that is an interface-shape concern for outside consumers rather than a reachable deadlock. Mentioned only so it is not mistaken for part of this report.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions