Stop discarding accepted shares when the DB is momentarily locked - #32
Merged
Conversation
The writer thread dequeues a batch, then opens a transaction. On a failed BEGIN it logged one line, bumped pg_errors and moved on — the events were already out of the ring and were never retried, so shares a miner had been told were accepted simply vanished from the ledger. The connection also set no busy_timeout, so SQLite returned SQLITE_BUSY the instant any other connection held the write lock rather than waiting at all. A single concurrent writer was enough: a manual sqlite3 session, a backup, a maintenance script. Observed in production on the forknet pool while a one-off maintenance script ran against the live DB — two batches, roughly 3-4 shares, silent apart from two ERROR lines. Set busy_timeout to 5s, and retry a failed batch up to three times with a linear backoff instead of dropping it. Counters advance only on the attempt that commits, so a retried batch is counted once. When every attempt fails the batch is still lost — nothing can put it back in the ring — but it now says so in those terms and lands in a dedicated events_lost counter rather than being folded into a generic error tally. events_lost must be 0. It means accepted work will never be credited, which is distinct from shares_dropped (enqueue-side overflow, already visible). The test holds the write lock on a second connection across several commit windows while shares are enqueued. It fails without the fix.
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.
The bug
writer_main()dequeues a batch and then opens the transaction:On a failed BEGIN the events are gone and are never retried. Shares the miner was already told were
acceptednever reach the ledger.Compounding it, the connection set no
busy_timeout, so SQLite returnedSQLITE_BUSYthe instant any other connection held the write lock — no waiting at all. One concurrent writer was enough to trigger it: a manualsqlite3session, a backup, a maintenance script.How it surfaced
Running a one-off maintenance script against the live forknet DB, mid-transaction:
Two batches at ~18 shares/sec over a 100 ms window — roughly 3–4 shares, ~250k sats of miner credit, gone. Small, but it was silent, it favoured the pool over the miners, and nothing in the ledger audits would ever have shown it: the shares were never written, so every consistency check still passes.
The fix
PRAGMA busy_timeout = 5000— wait for the lock instead of failing instantly. This alone would have made the incident a non-event.commit_batch(). Counters advance only on the attempt that commits, so a retried batch is counted once. After a failed COMMIT the transaction is rolled back, so replaying the batch is safe.events_lostcounter rather than a generic error tally.events_lostmust be 0. It means accepted work that will never be credited, which is a different failure fromshares_dropped(enqueue-side overflow, already counted and visible).Worst case is now a longer stall rather than lost data: 3 attempts × 5 s. That is the right trade — overflow during a stall is counted in
shares_dropped, whereas a dropped batch here is credited work vanishing.Test
test_commit_survives_a_locked_dbtakes the write lock on a second connection, holds it across several commit windows while 40 shares are enqueued, then releases. Asserts all 40 land andevents_lost == 0.Verified it catches the original bug — with
busy_timeoutremoved and attempts forced to 1:makeclean under-Werror; full C suite and all 47 dashboard tests pass.