mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-15 08:18:42 +01:00
* 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>
82 lines
2.4 KiB
Go
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
|
|
}
|