Files
seaweedfs/weed/mount/content_gap.go
T
10f8b2d5b1 mount: guard peer-truncate gaps, stale append EOF, and terminal writeback refusal (#12630)
* mount: guard append gaps over concurrent content merges

* mount: fence a writeback inode when its async publication is abandoned

A drain that gives up on a non-precondition error left the token clear, so a
reopened writeback handle could publish past the stored EOF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: fence a writeback inode when its async data flush fails

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: fence the inode when its last handle drops uncommitted content

A synchronous release whose final flush fails destroys the handle's dirty
pages, while under writeback the kernel EOF already counts them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 01:13:37 -08:00

82 lines
2.4 KiB
Go

package mount
import (
"fmt"
"sort"
"sync/atomic"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
)
// A publication owner carries provider refusal evidence through every retry.
// Helpers and the periodic flusher never retire content that is still owned.
type contentPublication struct {
refused bool
failure *atomic.Bool
}
func (p *contentPublication) retire(err error) {
if err != nil && p.refused {
p.abandon()
}
}
// An abandoned writeback drain leaves the kernel EOF past the stored one,
// whatever the filer answered.
func (p *contentPublication) abandon() {
if p.failure != nil {
p.failure.Store(true)
}
}
func (wfs *WFS) checkContentPublication(fh *FileHandle, base *filer_pb.Entry) error {
if wfs.option.WritebackCache && (fh.writebackContentError == nil || fh.writebackContentError.Load()) {
return fmt.Errorf("writeback inode awaits Forget after content refusal: %w", filer_pb.ErrPreconditionFailed)
}
// This guard shares the CAS version with CreateEntry. A rewritten or
// manifest bounding range cannot conceal the original kernel WRITE frontier.
var eof uint64
if base != nil {
eof = filer.FileSize(base)
}
if !wfs.option.WritebackCache && fh.pendingAppend && fh.pendingAppendOffset > eof {
return fmt.Errorf("append offset %d exceeds committed EOF %d: %w", fh.pendingAppendOffset, eof, filer_pb.ErrPreconditionFailed)
}
return nil
}
// Only used when a peer shrank the file: a merge must not acknowledge a gap
// between that stored EOF and local pending content. Ordinary sparse pwrite
// without a concurrent truncate is unaffected. Compare copies, never reorder
// a handle's chunks. A manifest's bounding range is not proof of coverage.
func contentGapAfter(entry *filer_pb.Entry, start uint64) bool {
end := filer.FileSize(entry)
if start >= end {
return false
}
chunks := append([]*filer_pb.FileChunk(nil), entry.Chunks...)
sort.SliceStable(chunks, func(i, j int) bool { return chunks[i].Offset < chunks[j].Offset })
covered := start
if uint64(len(entry.Content)) > covered {
covered = uint64(len(entry.Content))
}
for _, c := range chunks {
if covered >= end {
return false
}
if c.Offset < 0 || c.Size > ^uint64(0)-uint64(c.Offset) {
return true
}
left, right := uint64(c.Offset), uint64(c.Offset)+c.Size
if right <= covered {
continue
}
if left > covered || c.IsChunkManifest {
return true
}
covered = right
}
return covered < end
}