fix(kbfs): back off in waitForCompleteFlush instead of spinning - #29500
Open
chrisnojima wants to merge 1 commit into
Open
fix(kbfs): back off in waitForCompleteFlush instead of spinning#29500chrisnojima wants to merge 1 commit into
chrisnojima wants to merge 1 commit into
Conversation
waitForCompleteFlush looped until the journal drained, returning lastFlushErr only when it was a ServerErrorLockConflict and discarding every other error. A flush that keeps failing - an unreachable MD or block server returning EOF - therefore never drained the journal, returned as fast as it could fail, and was re-signalled immediately. That signalWork also cancels the background flusher's retry timer, so its exponential backoff was computed and logged on every pass and never once waited on. Observed in the field as 160,586 flush attempts in 31 seconds on one git TLF, which filled the 128MB kbfs log by itself. The log reads like a conflict- resolution loop, but CR ran exactly once; the loop is here. Keep looping on a nil error - a conflict or squash pauses the journal and flushes nothing, which is why the loop exists - and give the loop its own exponential backoff for anything else, so a transient failure still recovers without spinning. Read lastFlushErr under journalLock while here; flush's defer writes it under that lock, so the old read was a data race. Test reproduces the spin at 14,093 attempts in 2s without the fix. Note that testBWDelegate.OnNewState sends to an unbuffered channel, so the background loop stalls unless the test keeps draining stateCh.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a KBFS single-op flush hot-loop where waitForCompleteFlush could repeatedly re-signal background work and effectively cancel the flusher’s retry timer, causing extremely high flush-attempt rates under persistent failures (e.g., EOF/unreachable servers). It also eliminates a data race by reading lastFlushErr under journalLock.
Changes:
- Add a local exponential backoff inside
waitForCompleteFlushfor persistent flush errors, while preserving retry-forever behavior. - Read
lastFlushErrvia a newgetLastFlushErr()helper guarded byjournalLockto avoid a race with the flusher. - Add a regression test that forces persistent MD
Putfailures and asserts attempts stay bounded over a time window (including drainingdelegate.stateChto avoid stalling the background loop).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| go/kbfs/libkbfs/tlf_journal.go | Adds locked access to lastFlushErr and introduces a dedicated backoff in waitForCompleteFlush to prevent spinning on persistent flush failures. |
| go/kbfs/libkbfs/tlf_journal_test.go | Extends shimMDServer to simulate persistent Put errors and adds a regression test to ensure finishSingleOp doesn’t trigger a hot retry loop. |
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
waitForCompleteFlush(go/kbfs/libkbfs/tlf_journal.go) loops until the journal drains. It returnslastFlushErronly when it is aServerErrorLockConflictand discards every other error:So a flush that keeps failing — an unreachable MD or block server returning EOF — never drains the journal, returns as fast as it can fail, and gets re-signalled immediately.
That
signalWork()is the second half of it: it cancels the background flusher's retry timer (tlf_journal.go:629-632), so the exponential backoff is computed and logged on every pass and never once waited on. TheRetrying in 59slines describe a wait that does not happen.Impact
Seen in the wild: 160,586 flush attempts in 31 seconds (5,180/s) on one git TLF, which filled the 128MB kbfs log by itself — 39% of the file was these two lines:
The log reads like a conflict-resolution loop, but CR ran exactly once in that window, and the backoff values grow and cap correctly (
251ms → … → 59.9s). The backoff object is fine; nothing waits on it. git is what hits this becausewaitForCompleteFlushis the single-op path (libgit/rpc.go:81).The fix
Keep looping on a nil error — a conflict or squash pauses the journal and flushes nothing, which is the reason the loop exists — and give the loop its own exponential backoff for anything else. Retry-forever semantics are preserved, so a transient failure still recovers; it just no longer spins.
Also reads
lastFlushErrunderjournalLock.flush's defer writes it under that lock, so the old read was a data race.Testing
New
testTLFJournalSingleOpPersistentFlushErrmakes every MD put fail and counts attempts over a 2s window:TestTLFJournalpasses under-racewith no data races.TestJournal*in libkbfs and all ofkbfs/libgitpass.One trap worth knowing for future tests in this file:
testBWDelegate.OnNewStatesends to an unbuffered channel, so the background loop stalls the moment a test stops callingrequireNextState. An earlier version of this test reported 0 attempts both with and without the fix because of it — a false green in the dangerous direction. The test now drainsdelegate.stateChin a goroutine.🤖 Generated with Claude Code