fix(wallet-toolbox): pass trx to readSettings from verifyReadyForDatabaseAccess - #444
Merged
ty-everett merged 1 commit intoAug 9, 2026
Conversation
…baseAccess
StorageKnex.readSettings took no trx parameter and called this.toDb() with no
argument, so it always ran on the pool. verifyReadyForDatabaseAccess(trx) then
dropped its own trx when lazily populating the settings cache:
this._settings ??= await this.readSettings()
knex forces {min:1,max:1} on the sqlite dialect, so when that cache is cold and
the first access happens inside a caller's transaction, the settings read asks
for a second connection that can never be granted: the transaction will not
release until the query returns, and the query cannot run until the transaction
releases. It fails with "KnexTimeoutError: Timeout acquiring a connection. The
pool is probably full. Are you missing a .transacting(trx) call?".
verifyReadyForDatabaseAccess is on every write path, so any StorageKnex whose
first database access is transactional self-deadlocks. It was already using trx
correctly for the PRAGMA two lines below.
This is the same defect as ef710c3 (bsv-blockchain#426) fixed in getProvenOrRawTx, and the
last remaining toDb() call in the file that cannot receive a caller's
transaction.
The abstract already declared the parameter -- StorageReader.readSettings(trx?:
sdk.TrxToken) -- and StorageIdb, StorageMySQLDojoReader and getBeefForTxid all
accept it. Only StorageKnex dropped it, so this restores the existing contract
rather than changing an API.
Adds a regression test with acquireConnectionTimeout lowered to 5s so a
regression fails in seconds rather than the 60s default. Verified it fails
without the fix (both cases, with the KnexTimeoutError above) and passes with
it. bsv-blockchain#426 corrected getProvenOrRawTx without locking the behaviour down; this
covers both call sites.
imranterranode
requested review from
BraydenLangley,
sirdeggen,
tonesnotes and
ty-everett
as code owners
August 8, 2026 06:34
|
ty-everett
approved these changes
Aug 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Follow-up to #439, which was closed as already-fixed — ef710c3 (#426) had already corrected
getProvenOrRawTxon 2026-08-04. This is the sibling defect flagged in that review, branched from currentmain.Summary
StorageKnex.readSettingstook notrxparameter and calledthis.toDb()with no argument, so it always ran on the pool.verifyReadyForDatabaseAccess(trx)then dropped its owntrxwhen lazily populating the settings cache:knex forces
{ min: 1, max: 1 }on the sqlite dialect, so there is exactly one connection. When the settings cache is cold and the first access happens inside a caller's transaction, that read asks for a second connection which can never be granted — the transaction will not release until the query returns, and the query cannot run until the transaction releases. AfteracquireConnectionTimeoutit fails with:verifyReadyForDatabaseAccessis on every write path, so anyStorageKnexinstance whose first database access is transactional self-deadlocks.This restores an existing contract rather than changing an API
The abstract already declares the parameter, and every other implementation accepts it:
StorageReader.ts:77abstract readSettings (trx?: sdk.TrxToken)StorageIdb.ts:202readSettings(_trx?: TrxToken)StorageMySQLDojoReader.ts:89readSettings(trx?: TrxToken)getBeefForTxid.ts:340readSettings (trx?: TrxToken)StorageKnex.ts:137readSettings ()← the only one dropping itIt is also the last remaining
toDb()call inStorageKnexthat cannot receive a caller's transaction.Regression test
#426correctedgetProvenOrRawTxwithout locking the behaviour down, so this adds a test covering both call sites —verifyReadyForDatabaseAccess(trx)with a cold cache, andreadSettings(trx)directly.acquireConnectionTimeoutis lowered to 5s so a regression fails in seconds rather than the 60s default. The setup deliberately migrates a storage and then discards it, becausemakeAvailable()primes_settingsand would mask the bug entirely — the cache must be cold for the transactional read to be the first access.Verified the test actually catches the defect. Reverting only
StorageKnex.tsand keeping the test:With the fix applied:
Tests: 2 passed, 2 total.Testing
Build green across the dependency chain. Full package suite on this branch (Node 24.13.0, pnpm 10.33.2):
The 5 failures are pre-existing and unrelated — verified by running the same suites on clean
mainand getting identical results. Four areCannot find module 'vitest'(vitest suites picked up by jest inclient/test/,mobile/test/), and the fifth isbenchmarks/action-batch.bench.test.ts. Neither touches storage.Why this matters downstream
Same failure mode as #426: the proof-fetching task dies before processing any request,
proven_tx_reqsbacklogs at zero attempts, and BEEF payment ancestry never terminates at a proven parent — so payment headers grow unbounded until Cloudflare's 32 KB request-header cap refuses them. That growth is a sawtooth when proofs arrive and a one-way ratchet when they do not.Measured on the published npm
2.4.4(which predates #426) in a locally-built BSV Desktop against a sandbox wallet making real mainnet payments: patching the equivalent line movedproven_tx_reqscompleted 18 → 119, outstanding 139 → 38, and the payment header 33,058 → 13,666 bytes — a wallet Cloudflare had been refusing started paying again. This PR does not change that ceiling; it removes one more way to deadlock into it permanently.