filer: bound metadata log flush retries during shutdown (#11596)

* filer: bound metadata log flush retries during shutdown

On SIGTERM the filer could hang forever in Shutdown: the final
LocalMetaLogBuffer flush retries appendToFile indefinitely, and with
the master already down each AssignVolume attempt just kept failing.
WaitForShutdown never returned, the interrupt hook never reached
os.Exit, and the half-dead filer kept its ports bound.

Thread a context through appendToFile/assignAndUpload and switch to
a 15s-bounded context once the filer is stopping: the flush abandons
with a log line instead of retrying forever. Normal operation keeps
the unbounded retry so no metadata is dropped while running.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* filer: share one shutdown deadline across all pending meta log flushes

Review feedback on the per-flush timeout: a deadline armed at flush start
could already be expired when shutdown arrived, the first append of a flush
still ran unbounded, each queued window got a fresh budget (16 windows *
15s), and an abandoned window still advanced the flushed watermark as if it
had landed.

Rework to a single shared flush context on the Filer, cancelled once by
Shutdown via AfterFunc. Every append - the in-flight one and every queued
window - observes the same deadline, so the whole drain is bounded at 15s.
A flush that gives up reports its dropped bytes through the new
LogBuffer.NoteFlushDropped, and loopFlush then skips the offset/timestamp
advance and subscriber notifications for that window.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* filer: bound append attempts and commit uploaded pieces detached

Store operations check then drop request cancellation, so a stalled
backend could still hold flushFn past the shutdown deadline; run each
append attempt on its own goroutine and give up on it at the deadline.
Once a piece is uploaded, commit its entry on a detached context so the
expired deadline cannot strand the chunk.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* filer: keep shutdown flushes synchronous

Detaching the append attempt let flushFn return while the goroutine
still held the pooled flush buffer and could commit after the metadata
store closed; abandonment is only safe for the cancelable assign/upload
phase, which the shared flush context already bounds.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Chris LuandDevin authored and GitHub committed 2026-10-06 09:51:02 +08:00
1 parent 90eb4ec091
commit 076fd24186
5 files changed
+115 -16

No files matched your search

+27 -7
View File
@@ -195,13 +195,17 @@ type LogBuffer struct {
// Notified only when a flush lands, for readers that cannot act on an append
flushSubscribers map[string]*subscription
isStopping *atomic.Bool
shutdownCh chan struct{} // closed by ShutdownLogBuffer to wake blocked subscribers
loopsDone sync.WaitGroup // loopFlush and loopInterval signal exit
isAllFlushed bool
flushChan chan *dataToFlush
flushBudget *flushBudget
flushSeq uint64 // seal counter, assigned under the write lock
writeMu sync.Mutex // serializes sealing and enqueueing, including shutdown
shutdownCh chan struct{} // closed by ShutdownLogBuffer to wake blocked subscribers
// flushDroppedBytes counts bytes the flushFn gave up on during shutdown.
// loopFlush reads it after each window so an abandoned flush does not
// advance the flushed watermark or wake subscribers as if it had landed.
flushDroppedBytes atomic.Int64
loopsDone sync.WaitGroup // loopFlush and loopInterval signal exit
isAllFlushed bool
flushChan chan *dataToFlush
flushBudget *flushBudget
flushSeq uint64 // seal counter, assigned under the write lock
writeMu sync.Mutex // serializes sealing and enqueueing, including shutdown
// Offset range tracking for Kafka integration
hasOffsets bool
// Disk chunk cache for historical data reads
@@ -755,6 +759,14 @@ func (logBuffer *LogBuffer) WaitForShutdown() {
logBuffer.loopsDone.Wait()
}
// NoteFlushDropped records that the flush function abandoned n bytes instead
// of writing them (a shutdown-bounded retry giving up). loopFlush then skips
// the watermark advance and subscriber notifications for that window, so no
// reader is told the bytes were flushed when they were not.
func (logBuffer *LogBuffer) NoteFlushDropped(n int) {
logBuffer.flushDroppedBytes.Add(int64(n))
}
// IsAllFlushed returns true if all data in the buffer has been flushed, after calling ShutdownLogBuffer().
func (logBuffer *LogBuffer) IsAllFlushed() bool {
return logBuffer.isAllFlushed
@@ -775,8 +787,16 @@ func (logBuffer *LogBuffer) loopFlush() {
break // shutdown sentinel
}
logBuffer.flushFn(logBuffer, d.startTime, d.stopTime, d.data, d.minOffset, d.maxOffset)
dropped := logBuffer.flushDroppedBytes.Swap(0)
d.releaseMemory()
logBuffer.flushBudget.release(d.budget)
if dropped > 0 {
glog.Warningf("log buffer %s: shutdown flush dropped %d bytes", logBuffer.name, dropped)
if d.done != nil {
close(d.done)
}
continue
}
// local logbuffer is different from aggregate logbuffer here
if d.maxOffset >= 0 {
logBuffer.lastFlushedOffset.Store(d.maxOffset)