mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-15 00:09:00 +01:00
A flush uploads the dirty pages without the handle lock, then commits the entry under it. A write in between grows the entry's size, but its pages are not in the committed chunks. Clearing dirtyMetadata then made the next flush skip the commit, leaving a file with the new size and no chunk for the write, which reads back as zeros. Count page writes on the handle and clear dirtyMetadata only when none landed since the flush started uploading. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
2.6 KiB
Go
85 lines
2.6 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
|
|
// writes is the handle's pageWrites before its pages were flushed. A
|
|
// later write is not in the published chunks, so it keeps the handle dirty.
|
|
writes uint64
|
|
}
|
|
|
|
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
|
|
}
|