mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-18 12:30:46 +02:00
* mount: re-resolve volume locations after a failed chunk read NewChunkGroup passed nil as the ReaderCache's CacheInvalidator, so retryFetchAfterCacheInvalidation was dead code on the FUSE read path. A mount that cached a volume's locations while one server was down kept retrying that server after it died, then returned EIO, even though the master and filer both resolved the live replica. The S3 gateway already passes its filerClient; do the same for the mount. * test: FUSE integration tests for volume server failover One mount appends while a second tails, and a volume server is killed, started or restarted mid-stream against a 001-replicated cluster of three volume servers. Automates the scenario matrix reported for Docker Swarm mounts, including the large-file variant and a no-chaos control. * test: report the filer's own view when append content mismatches A mismatch between what the writer wrote and what the reader sees can come from either side's cache. Read the file back through the filer's HTTP handler as well, and let the mount verbosity be raised from the environment, so a failing run says which layer lost the data. * test: wait for the reader mount to converge before comparing A mount caches metadata for about a second, so reading the file the instant the writer's last close returned can legitimately come back short. Poll the reader until it matches or the timeout expires; content that is wrong rather than merely late never converges and still fails, now with the writer's mount and the filer's own view alongside it. * test: detect a failover cluster child that exited at startup Signal(0) succeeds for a zombie and nothing reaped these children until shutdown, so a process that died on startup looked alive until the readiness timeout expired. Reap each child as it is started and consult the result. * test: read a file the killed volume server actually holds Placement decides which two of three servers back each volume, so killing volume N and reading readfile-N could pass without the victim ever holding a replica of it. Resolve each file's volumes through the filer and the master, and pick one the victim backs, preferring a file the reader has not cached. * ci: stop persisting checkout credentials in the failover workflow The job does not use the token after cloning. Also tag the README's command block as bash and match the timeout the workflow actually uses. * test: discard the ignored errors errcheck flags in the failover harness * test: resolve manifests when mapping a file to its volumes A manifest chunk's own fid names the volume holding the manifest, not the volumes holding the data, so a large enough file would point the failover victim at the wrong server. * test: pin the stale-location recovery path with a primed reader Reading a file for the first time after a server dies proves nothing: the lookup is fresh and returns the survivor. Kill one holder and wait for the master to drop it, read a file on that volume so the reader caches the lone survivor, restart the first server, then kill the survivor. The reader's only cached location is now dead while the data is live elsewhere, which is the case the invalidator exists for: EIO without it, recovery with it.
282 lines
9.1 KiB
Go
282 lines
9.1 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
|
|
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
|
|
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
|
|
|
|
// RDMA chunk offset cache for performance optimization
|
|
chunkOffsetCache []int64
|
|
chunkCacheValid bool
|
|
chunkCacheLock sync.RWMutex
|
|
|
|
// for debugging
|
|
mirrorFile *os.File
|
|
}
|
|
|
|
func newFileHandle(wfs *WFS, handleId FileHandleId, inode uint64, entry *filer_pb.Entry) *FileHandle {
|
|
fh := &FileHandle{
|
|
fh: handleId,
|
|
counter: 1,
|
|
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
|
|
var resolveManifestErr error
|
|
fh.entryChunkGroup, resolveManifestErr = filer.NewChunkGroup(fh.wfs.LookupFn(), fh.wfs.chunkCache, entry.Chunks, fh.wfs.option.ConcurrentReaders, fh.wfs.CacheInvalidator())
|
|
if resolveManifestErr != nil {
|
|
glog.Warningf("failed to resolve manifest chunks in %+v", entry)
|
|
}
|
|
} else {
|
|
glog.Fatalf("setting file handle entry to nil")
|
|
}
|
|
fh.entry.SetEntry(entry)
|
|
|
|
// Invalidate chunk offset cache since chunks may have changed
|
|
fh.invalidateChunkCache()
|
|
}
|
|
|
|
// 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.
|
|
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 || 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)
|
|
|
|
// Invalidate chunk offset cache since entry may have been modified
|
|
fh.invalidateChunkCache()
|
|
|
|
return result
|
|
}
|
|
|
|
func (fh *FileHandle) AddChunks(chunks []*filer_pb.FileChunk) {
|
|
fh.entry.AppendChunks(chunks)
|
|
|
|
// Invalidate chunk offset cache since new chunks were added
|
|
fh.invalidateChunkCache()
|
|
}
|
|
|
|
func (fh *FileHandle) ReleaseHandle() {
|
|
// 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)
|
|
}
|
|
|
|
fhActiveLock := fh.wfs.fhLockTable.AcquireLock("ReleaseHandle", fh.fh, util.ExclusiveLock)
|
|
defer fh.wfs.fhLockTable.ReleaseLock(fh.fh, fhActiveLock)
|
|
|
|
fh.dirtyPages.Destroy()
|
|
if IsDebugFileReadWrite {
|
|
fh.mirrorFile.Close()
|
|
}
|
|
}
|
|
|
|
// getCumulativeOffsets returns cached cumulative offsets for chunks, computing them if necessary
|
|
func (fh *FileHandle) getCumulativeOffsets(chunks []*filer_pb.FileChunk) []int64 {
|
|
fh.chunkCacheLock.RLock()
|
|
if fh.chunkCacheValid && len(fh.chunkOffsetCache) == len(chunks)+1 {
|
|
// Cache is valid and matches current chunk count
|
|
result := make([]int64, len(fh.chunkOffsetCache))
|
|
copy(result, fh.chunkOffsetCache)
|
|
fh.chunkCacheLock.RUnlock()
|
|
return result
|
|
}
|
|
fh.chunkCacheLock.RUnlock()
|
|
|
|
// Need to compute/recompute cache
|
|
fh.chunkCacheLock.Lock()
|
|
defer fh.chunkCacheLock.Unlock()
|
|
|
|
// Double-check in case another goroutine computed it while we waited for the lock
|
|
if fh.chunkCacheValid && len(fh.chunkOffsetCache) == len(chunks)+1 {
|
|
result := make([]int64, len(fh.chunkOffsetCache))
|
|
copy(result, fh.chunkOffsetCache)
|
|
return result
|
|
}
|
|
|
|
// Compute cumulative offsets
|
|
cumulativeOffsets := make([]int64, len(chunks)+1)
|
|
for i, chunk := range chunks {
|
|
cumulativeOffsets[i+1] = cumulativeOffsets[i] + int64(chunk.Size)
|
|
}
|
|
|
|
// Cache the result
|
|
fh.chunkOffsetCache = make([]int64, len(cumulativeOffsets))
|
|
copy(fh.chunkOffsetCache, cumulativeOffsets)
|
|
fh.chunkCacheValid = true
|
|
|
|
return cumulativeOffsets
|
|
}
|
|
|
|
// invalidateChunkCache invalidates the chunk offset cache when chunks are modified
|
|
func (fh *FileHandle) invalidateChunkCache() {
|
|
fh.chunkCacheLock.Lock()
|
|
fh.chunkCacheValid = false
|
|
fh.chunkOffsetCache = nil
|
|
fh.chunkCacheLock.Unlock()
|
|
}
|