mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-30 17:00:48 +01:00
* mount: keep the shared directory build out of the requester's context The shared directory build behind EnsureVisited is a singleflight call: the first requester for a path owns the build, and every later requester for the same path waits on that same build. The build ran on the owner's request context, so a FUSE request that returned -- or that the kernel interrupted -- canceled the build for everybody waiting on it. A waiter whose own context was still alive then received somebody else's cancellation. weed/mount/weedfs_dir_read.go classified a canceled read as EINTR only when the local context was done, so such a waiter fell through to fuse.EIO: the kernel handed EIO to readdirent, kubelet's applyFSGroup failed, and the volume never mounted. Build the shared directory on a context that keeps the request's values but not its cancellation, and report a canceled read as EINTR even when the local context is still alive. Waiters still abandon on their own context, so a canceled request stops waiting immediately. This follows up #11717, which made the wait abandonable and mapped the local cancellation to EINTR, but left the shared build itself on the owner's context. The surrounding build lifecycle calls already use context.Background() for the same reason. Test: TestEnsureVisitedSharedBuildSurvivesOwnerRequestCancel reproduces the cancellation with a fake filer and fails without this change. * mount: classify gRPC-canceled directory listings as EINTR * mount: log only real directory read failures, not interruptions * mount: keep shared section refresh out of the requester's context * mount: let section-refresh waiters leave on their own context The shared refresh flight now outlives the requester that started it, but callers still blocked in singleflight.Do until it finished — an interrupted directory read could not stop waiting, and the caller's own deadline never applied to the wait. DoChan + select on the caller's context restores early abandon while the detached refresh still completes for the remaining waiters. * ci: re-run FUSE integration after a flaky writeback-cache failure * mount: refresh a stale entry once before reporting a read error A read resolves chunk views from the handle's entry. A concurrent overwrite commits a new entry whose replaced file ids the filer queues for deletion; the open handle only learns the new chunk list when the metadata invalidation lands. A fetch that lands between the volume delete and the invalidation names data that is already gone and the read reports EIO. On a chunk read error, wait out any in-flight async flush, re-read the entry from the filer, install it when it outranks the handle, and retry once. An unchanged version means the failure is genuine. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: fence the read-error entry refresh by file identity Two guards on refreshEntryForRead's install path: the unlocked dirtyMetadata read raced with Write under the handle lock — drop it, installAckedEntry already checks under that lock, and it now fences isDeleted handles the same way. And a saved path can name a different file after unlink/reuse, so refuse the refresh when the fetched inode disagrees with the one the handle holds. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: user.email <chrislusf@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
237 lines
8.0 KiB
Go
237 lines
8.0 KiB
Go
package mount
|
|
|
|
import (
|
|
"os"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"github.com/seaweedfs/go-fuse/v2/fuse"
|
|
"github.com/seaweedfs/seaweedfs/weed/cluster"
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
type FileHandleId uint64
|
|
|
|
var IsDebugFileReadWrite = false
|
|
|
|
type FileHandle struct {
|
|
fh FileHandleId
|
|
counter int64
|
|
fhs map[FileHandleId]struct{} // live fh values, one per open; guarded by fhMap's lock
|
|
entry *LockedEntry
|
|
entryLock sync.RWMutex
|
|
entryChunkGroup *filer.ChunkGroup
|
|
inode uint64
|
|
wfs *WFS
|
|
|
|
// cache file has been written to
|
|
dirtyMetadata bool
|
|
dirtyPages *PageWriter
|
|
reader *filer.ChunkReadAt
|
|
contentType string
|
|
asyncFlushPending bool // set in writebackCache mode to defer flush to Release
|
|
asyncFlushUid uint32 // saved uid for deferred metadata flush
|
|
asyncFlushGid uint32 // saved gid for deferred metadata flush
|
|
savedDir string // last known parent path if inode-to-path state is forgotten
|
|
savedName string // last known file name if inode-to-path state is forgotten
|
|
|
|
isDeleted bool
|
|
// deleteEpoch counts the times isDeleted was raised, all of them under the
|
|
// handle's flush lock. A caller that raised it and then found it had
|
|
// nothing to delete after all can tell its own mark from a later one.
|
|
deleteEpoch uint64
|
|
isRenamed bool // set by Rename before waiting for async flush; skips old-path metadata flush
|
|
|
|
// entryVersionTsNs is the filer log position the handle's entry reflects.
|
|
// State at or below it must not replace the entry — that rolls it back.
|
|
entryVersionTsNs atomic.Int64
|
|
|
|
// entryVersionSignature identifies the filer whose clock stamped
|
|
// entryVersionTsNs, when it came from an RPC fence. Positions from a
|
|
// different filer are not comparable, so an event that filer did not log
|
|
// is applied rather than fenced out. Zero when the version came from an
|
|
// event, whose ordering the subscription already provides.
|
|
entryVersionSignature atomic.Int32
|
|
|
|
// baseEntry snapshots the filer state last installed or acknowledged.
|
|
// Local writes move the live entry away from it, so "is this event new"
|
|
// must be judged here, not against the live entry. Always store a clone.
|
|
baseEntry atomic.Pointer[filer_pb.Entry]
|
|
|
|
// dlmLock holds the distributed lock for cross-mount write coordination.
|
|
// Non-nil only when -dlm is enabled and the file was opened for writing.
|
|
// Acquired in AcquireHandle, released in ReleaseHandle.
|
|
dlmLock *cluster.LiveLock
|
|
|
|
// remoteInstallMu serializes downloadRemoteEntry's install, which holds
|
|
// only the handle's shared lock and so races a second concurrent read.
|
|
remoteInstallMu sync.Mutex
|
|
|
|
// for debugging
|
|
mirrorFile *os.File
|
|
}
|
|
|
|
func newFileHandle(wfs *WFS, handleId FileHandleId, inode uint64, entry *filer_pb.Entry) *FileHandle {
|
|
fh := &FileHandle{
|
|
fh: handleId,
|
|
counter: 1,
|
|
fhs: map[FileHandleId]struct{}{handleId: {}},
|
|
inode: inode,
|
|
wfs: wfs,
|
|
}
|
|
// dirtyPages: newContinuousDirtyPages(file, writeOnly),
|
|
fh.dirtyPages = newPageWriter(fh, wfs.option.ChunkSizeLimit)
|
|
fh.entry = &LockedEntry{
|
|
Entry: entry,
|
|
}
|
|
if entry != nil {
|
|
fh.SetEntry(entry)
|
|
fh.baseEntry.Store(proto.Clone(entry).(*filer_pb.Entry))
|
|
}
|
|
|
|
if IsDebugFileReadWrite {
|
|
var err error
|
|
fh.mirrorFile, err = os.OpenFile("/tmp/sw/"+entry.Name, os.O_RDWR|os.O_CREATE, 0600)
|
|
if err != nil {
|
|
println("failed to create mirror:", err.Error())
|
|
}
|
|
}
|
|
|
|
return fh
|
|
}
|
|
|
|
func (fh *FileHandle) FullPath() util.FullPath {
|
|
if fp, status := fh.wfs.inodeToPath.GetPath(fh.inode); status == fuse.OK {
|
|
return fp
|
|
}
|
|
if fh.savedName != "" {
|
|
return util.FullPath(fh.savedDir).Child(fh.savedName)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (fh *FileHandle) RememberPath(fullPath util.FullPath) {
|
|
if fullPath == "" {
|
|
return
|
|
}
|
|
fh.savedDir, fh.savedName = fullPath.DirAndName()
|
|
}
|
|
|
|
func (fh *FileHandle) GetEntry() *LockedEntry {
|
|
return fh.entry
|
|
}
|
|
|
|
func (fh *FileHandle) SetEntry(entry *filer_pb.Entry) {
|
|
if entry != nil {
|
|
fileSize := filer.FileSize(entry)
|
|
entry.Attributes.FileSize = fileSize
|
|
if fh.entryChunkGroup != nil {
|
|
_ = fh.entryChunkGroup.Close()
|
|
}
|
|
var resolveManifestErr error
|
|
fh.entryChunkGroup, resolveManifestErr = filer.NewChunkGroup(fh.wfs.LookupFn(), fh.wfs.chunkCache, entry.Chunks, fh.wfs.option.ConcurrentReaders, fh.wfs.CacheInvalidator(), fh.wfs.manifestCache, fh.wfs.readerCacheBudget)
|
|
if resolveManifestErr != nil {
|
|
glog.Warningf("failed to resolve manifest chunks in %+v", filer_pb.ScrubCipherKeys(entry))
|
|
}
|
|
} else {
|
|
glog.Fatalf("setting file handle entry to nil")
|
|
}
|
|
fh.entry.SetEntry(entry)
|
|
}
|
|
|
|
// installAckedEntry installs filer-acknowledged state under the handle lock
|
|
// when it outranks the handle. A version never advances without its value:
|
|
// stamping alone would fence out the events carrying what the handle lacks.
|
|
// Dirty handles are skipped — local writes supersede the ack. Deleted ones
|
|
// too: their path may already name a different file, and no acknowledged
|
|
// state belongs to this handle anymore.
|
|
func (fh *FileHandle) installAckedEntry(entry *filer_pb.Entry, versionTsNs int64, signature int32) {
|
|
fhActiveLock := fh.wfs.fhLockTable.AcquireLock("installAckedEntry", fh.fh, util.ExclusiveLock)
|
|
defer fh.wfs.fhLockTable.ReleaseLock(fh.fh, fhActiveLock)
|
|
if versionTsNs == 0 || fh.dirtyMetadata || fh.isDeleted || entry == fh.GetEntry().GetEntry() {
|
|
return
|
|
}
|
|
// Refuse only what is provably older. Two known, differing filer
|
|
// signatures mean the positions come from unrelated clocks and say
|
|
// nothing about each other; dropping the acknowledgment there would leave
|
|
// the handle holding the very state this mutation replaced. Unknown
|
|
// signatures still compare, as they did before.
|
|
handleSignature := fh.entryVersionSignature.Load()
|
|
provablyOtherClock := signature != 0 && handleSignature != 0 && signature != handleSignature
|
|
if !provablyOtherClock && versionTsNs <= fh.entryVersionTsNs.Load() {
|
|
return
|
|
}
|
|
fh.SetEntry(entry)
|
|
fh.setAuthoritativeBase(proto.Clone(entry).(*filer_pb.Entry))
|
|
fh.advanceEntryVersion(versionTsNs, signature)
|
|
}
|
|
|
|
// setAuthoritativeBase installs the base snapshot a local ack acknowledged.
|
|
func (fh *FileHandle) setAuthoritativeBase(base *filer_pb.Entry) {
|
|
fh.baseEntry.Store(base)
|
|
}
|
|
|
|
// advanceEntryVersion raises the entry version, never regresses it, and
|
|
// records the clock domain the new position belongs to: a filer signature for
|
|
// an RPC fence, zero for an event. The signature travels with the timestamp so
|
|
// the two never disagree. A zero position (an unversioned old filer) is a
|
|
// no-op, leaving the handle open to refreshes.
|
|
func (fh *FileHandle) advanceEntryVersion(tsNs int64, signature int32) {
|
|
if tsNs == 0 {
|
|
return
|
|
}
|
|
for {
|
|
current := fh.entryVersionTsNs.Load()
|
|
if tsNs <= current {
|
|
return
|
|
}
|
|
if fh.entryVersionTsNs.CompareAndSwap(current, tsNs) {
|
|
fh.entryVersionSignature.Store(signature)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (fh *FileHandle) ResetDirtyPages() {
|
|
fh.dirtyPages.Destroy()
|
|
fh.dirtyPages = newPageWriter(fh, fh.wfs.option.ChunkSizeLimit)
|
|
fh.dirtyMetadata = false
|
|
fh.contentType = ""
|
|
}
|
|
|
|
func (fh *FileHandle) UpdateEntry(fn func(entry *filer_pb.Entry)) *filer_pb.Entry {
|
|
result := fh.entry.UpdateEntry(fn)
|
|
|
|
return result
|
|
}
|
|
|
|
func (fh *FileHandle) AddChunks(chunks []*filer_pb.FileChunk) {
|
|
fh.entry.AppendChunks(chunks)
|
|
}
|
|
|
|
func (fh *FileHandle) ReleaseHandle() {
|
|
fhActiveLock := fh.wfs.fhLockTable.AcquireLock("ReleaseHandle", fh.fh, util.ExclusiveLock)
|
|
defer fh.wfs.fhLockTable.ReleaseLock(fh.fh, fhActiveLock)
|
|
|
|
// Release distributed lock before cleaning up, so other mounts can
|
|
// proceed as soon as this handle is done flushing.
|
|
if fh.dlmLock != nil {
|
|
fh.dlmLock.Stop()
|
|
fh.dlmLock = nil
|
|
glog.V(1).Infof("DLM lock released for inode %d", fh.inode)
|
|
}
|
|
|
|
if fh.entryChunkGroup != nil {
|
|
_ = fh.entryChunkGroup.Close()
|
|
}
|
|
fh.dirtyPages.Destroy()
|
|
if IsDebugFileReadWrite {
|
|
fh.mirrorFile.Close()
|
|
}
|
|
}
|