Files
seaweedfs/weed/mount/filehandle_map.go
T
Chris LuandClaude Opus 5.5 6ad4b07598 mount: scope and order the final-release writeback fence (#12651)
* mount: fence a dropped handle only when it holds unpublished content

dirtyMetadata is also set by chmod, xattr and fallocate, so a failed
metadata-only close refused every later write of the inode.

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

* mount: fence the inode before its last handle leaves the handle map

A reopen between the map removal and the fence could publish at the
stale kernel EOF.

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

* mount: fence a failed metadata flush only when the handle holds content

A refused synchronous flush or an abandoned writeback metadata flush of a
handle with only chmod, xattr or fallocate changes fenced the inode, so
later writes were refused until FORGET.

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

* mount: count a size change on an open handle as unpublished content

A truncate or extend through an open handle changes the entry's size and
chunks, but it set only dirtyMetadata. A failed close then skipped the
final-release fence, so a reopen before FORGET could publish against the
untruncated entry.

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

* mount: mark copied content dirty before each page of a streaming copy

A copy that failed or was cancelled after earlier pages were accepted left
dirtyContent false, so the final-release fence skipped that content.

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

* mount: mark copied content dirty only after a page is accepted

A copy whose first page is rejected leaves the destination unchanged and
the kernel EOF where it was, so fencing the inode on release only blocked
later writes until FORGET.

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

* mount: skip the server-side copy while the destination holds unpublished content

A whole-file server copy clears dirtyContent when it commits, so a page a
cancelled streaming copy left behind would lose its release fence. Fall
back to the streaming copy instead.

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

* mount: skip the server-side copy while the source holds unpublished content

The filer copies the stored source, so pages still pending on the source
handle would be missing from the destination. The streaming copy reads
them through the dirty pages.

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

* mount: check the publication fence before a server-side whole-file copy

The filer copy publishes the destination without going through the
flush-time guard, so a fenced writeback inode could still take new
content before FORGET. Fall back to the streaming copy, whose flush is
guarded.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 10:47:33 -08:00

189 lines
5.6 KiB
Go

package mount
import (
"sync"
"sync/atomic"
"github.com/seaweedfs/seaweedfs/weed/util"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
)
type FileHandleToInode struct {
sync.RWMutex
inode2fh map[uint64]*FileHandle
fh2inode map[FileHandleId]uint64
}
func NewFileHandleToInode() *FileHandleToInode {
return &FileHandleToInode{
inode2fh: make(map[uint64]*FileHandle),
fh2inode: make(map[FileHandleId]uint64),
}
}
func (i *FileHandleToInode) GetFileHandle(fh FileHandleId) *FileHandle {
i.RLock()
defer i.RUnlock()
inode, found := i.fh2inode[fh]
if found {
return i.inode2fh[inode]
}
return nil
}
func (i *FileHandleToInode) FindFileHandle(inode uint64) (fh *FileHandle, found bool) {
i.RLock()
defer i.RUnlock()
fh, found = i.inode2fh[inode]
return
}
// MarkInodeRenamed sets isRenamed on any file handle associated with the
// given inode. This prevents the async flush from recreating a renamed
// file's metadata under its old path.
func (i *FileHandleToInode) MarkInodeRenamed(inode uint64) {
i.RLock()
defer i.RUnlock()
if fh, ok := i.inode2fh[inode]; ok {
fh.isRenamed = true
}
}
// AcquireFileHandle fully initializes a handle — entry and the log position it
// reflects — before exposing it in the map, and reports whether one already
// existed. An existing handle only gets its counter bumped: its entry belongs
// to whoever holds the handle lock, so callers install there (AcquireHandle)
// rather than under this one.
// Every open gets its own fh value over the shared FileHandle so that
// RELEASE — which identifies nothing else about the dying description — can
// scope per-open state (e.g. OFD lock owners) to the open that is closing.
//
// A handle whose counter already reached zero is draining its deferred flush:
// it stays in the map only so rename/unlink can still find it, and
// RemoveFileHandle will drop it (and every fh id minted for it) when the drain
// completes. Attaching here would hand the kernel an fh id that vanishes mid-
// open — reads on it return ENOENT, which the kernel reports to read() as EIO.
// Callers must treat a draining report as "wait for the pending flush, then
// retry the whole acquire", not as success.
func (i *FileHandleToInode) AcquireFileHandle(wfs *WFS, inode uint64, entry *filer_pb.Entry, versionTsNs int64, signature int32, appendMode ...bool) (*FileHandle, FileHandleId, bool, bool) {
// Bind before publishing the handle or queuing any async close. Capture
// under the inode-table lock, but release it before taking the fhMap lock.
var writebackError *atomic.Bool
if wfs.option.WritebackCache {
wfs.inodeToPath.RLock()
state, found := wfs.writebackContentErrors.Load(inode)
if !found {
state, _ = wfs.writebackContentErrors.LoadOrStore(inode, new(atomic.Bool))
}
writebackError = state.(*atomic.Bool)
wfs.inodeToPath.RUnlock()
}
i.Lock()
defer i.Unlock()
fh, found := i.inode2fh[inode]
if !found {
fh = newFileHandle(wfs, FileHandleId(util.RandomUint64()), inode, entry)
fh.writebackContentError = writebackError
fh.fhs[fh.fh] = len(appendMode) > 0 && appendMode[0]
fh.entryVersionTsNs.Store(versionTsNs)
fh.entryVersionSignature.Store(signature)
i.inode2fh[inode] = fh
i.fh2inode[fh.fh] = inode
return fh, fh.fh, false, false
}
if fh.counter <= 0 {
return nil, 0, true, true
}
fh.counter++
openFh := FileHandleId(util.RandomUint64())
fh.fhs[openFh] = len(appendMode) > 0 && appendMode[0]
i.fh2inode[openFh] = inode
return fh, openFh, true, false
}
func (i *FileHandleToInode) isAppendHandle(openFh FileHandleId) bool {
i.RLock()
defer i.RUnlock()
if fh := i.inode2fh[i.fh2inode[openFh]]; fh != nil {
return fh.fhs[openFh]
}
return false
}
func (i *FileHandleToInode) ReleaseByHandle(fh FileHandleId) *FileHandle {
i.Lock()
defer i.Unlock()
inode, found := i.fh2inode[fh]
if !found {
return nil
}
// The released id never resolves again: a duplicate RELEASE on it must
// not decrement the counter a second time.
delete(i.fh2inode, fh)
fhHandle, fhFound := i.inode2fh[inode]
if !fhFound {
return nil
}
delete(fhHandle.fhs, fh)
// If the counter is already <= 0, a prior Release already started the
// drain. Return nil to prevent double-processing.
if fhHandle.counter <= 0 {
return nil
}
fhHandle.counter--
if fhHandle.counter <= 0 {
if fhHandle.asyncFlushPending {
// Handle stays in fhMap so rename/unlink can still find it
// via FindFileHandle during the background drain.
return fhHandle
}
// Fence before a reopen can see the inode without this handle.
if fhHandle.dirtyContent && !fhHandle.isDeleted {
(&contentPublication{failure: fhHandle.writebackContentError}).abandon()
}
delete(i.inode2fh, inode)
for openFh := range fhHandle.fhs {
delete(i.fh2inode, openFh)
}
fhHandle.fhs = nil
return fhHandle
}
return nil
}
// RemoveFileHandle removes a handle from both maps. Called after an async
// drain completes to clean up the handle that was intentionally kept in the
// maps during the flush.
func (i *FileHandleToInode) RemoveFileHandle(fh FileHandleId, inode uint64) {
i.Lock()
defer i.Unlock()
if fhHandle, ok := i.inode2fh[inode]; ok {
for openFh := range fhHandle.fhs {
delete(i.fh2inode, openFh)
}
}
delete(i.inode2fh, inode)
delete(i.fh2inode, fh)
}
// openFhIds returns the live fh values minted for an inode's shared handle —
// one per still-open description.
func (i *FileHandleToInode) openFhIds(inode uint64) []FileHandleId {
i.RLock()
defer i.RUnlock()
fh := i.inode2fh[inode]
if fh == nil {
return nil
}
ids := make([]FileHandleId, 0, len(fh.fhs))
for id := range fh.fhs {
ids = append(ids, id)
}
return ids
}