mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-29 19:25:35 +00:00
* cluster: never broadcast an empty lock ring An empty member list is never a usable ring state, but a delayed RemoveServer on a former leader can fire after the new leader already broadcast the recovered ring. That late broadcast carries a newer wall-clock version, so clients accept the empty ring and permanently reject the good one. Skip the broadcast entirely when the member list is empty, keeping the last non-empty snapshot for reconnecting clients. * cluster: periodically rebroadcast the lock ring Ring updates are purely event-driven, so one lost or poisoned update is permanent until the next membership change — with a single filer that may never come. Re-arm a per-group timer after every broadcast so the current leader keeps re-sending the ring; clients reject nothing newer than their last accepted version, so a re-sent snapshot always heals a stale view. * filer,s3api: reset the lock ring on master change Ring versions are per-master monotonic — each master stamps wall-clock nanoseconds — so a late high-version update accepted from a former leader makes the new leader's snapshot look stale forever. Detect a leader change across the reconnect gap (currentMaster is cleared between attempts, so remember the last served master) and reset the ring to bootstrap state so the new leader's view always applies. * cluster: fail lock acquisition when no lock server exists retryUntilLocked loops forever, so a filer reporting an empty lock ring wedges every append write indefinitely. Bound only the "no lock server found" case — ordinary contention is still waited out since the holder releases eventually. The constructors now return nil on failure: the filer append path and S3 object writes fail fast, while mounts degrade to their existing lockless mode. * cluster: reset only the ring version on master change Ring versions are per-master monotonic, so a version gate reset is all a leader change needs. Clearing the whole ring made every filer its own write owner until the next update and dropped the prior-owner window for keys the new leader remaps; the last ring now keeps routing until the new leader's snapshot transitions off it. * cluster: skip redundant ring installs and defer rebroadcasts An unchanged member list now only bumps the accepted version instead of installing a snapshot: periodic rebroadcasts no longer fire the topology-change callback or restart the prior-owner window. And a rebroadcast that lands inside a membership stabilization window yields to the pending timer rather than publishing an intermediate ring. * cluster,mount: bound lock unavailability, fail ops that cannot lock Only 'lock already owned' contention retries without bound now; every other failure — no lock server, or a dead ring member refusing connections — shares the same unavailability budget, so a ring naming departed filers can no longer hang a lock forever. Mount open-write, create, and rename fail with EAGAIN when the required lock cannot be acquired instead of proceeding without cross-mount serialization. * cluster: check pending stabilization inside the broadcast critical section rebroadcast released the mutex between the pending-timer check and nextBroadcastUpdate, so a membership change arriving in the gap could arm a stabilization timer while the rebroadcast emitted an intermediate ring. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: acquire path locks before mutating create/rename state Create took the DLM lock only after the filer create, so a lock failure returned EAGAIN with an eagerly persisted file left behind. Rename marked source handles renamed before acquiring locks, so a failed acquisition left them suppressing old-path flushes for a rename that never happened. Both now take the locks first; the create's lock is released again if the entry race loses to another creator and AcquireHandle takes over. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: keep the old-path lock when rename lock migration fails The migration stopped the handle's lock before acquiring the replacement, so a nil result left the handle writing with no lock at all. Acquiring the new-path lock first means failure keeps the existing lock instead of reporting success with serialization dropped. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: skip new-path rename lock when a handle already holds it A target file open for write on this mount already carries a lock on newPath; the lock manager does not grant a second lock to the same owner, so the rename would wait on itself until the handle closed. Also avoid locking twice when old and new paths coincide. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: hand the rename's target lock to the migrating handle The rename holds a lock on newPath for its duration, so the response migration's fresh acquisition waited on that same lock until the handle released — under fhLockTable, blocking the handle's own close. Adopt the rename's lock directly; nested move responses still acquire their own. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: move the replaced target's lock to the renamed handle When the target path was already locked by an open handle on this mount, the migrated source handle kept only its stale old-path lock — the target's close would then release the last lock on the new path while the renamed handle was still open. Adopt the replaced handle's lock instead. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: stop the handle lock inside the fh lock on release ReleaseHandle stopped fh.dlmLock before taking the fhLockTable slot, so a rename migration holding that slot could still observe and adopt a lock that was already stopping. Stopping under the fh lock makes the transfer serialize against the release. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: claim the replaced target's lock for the renamed handle When the target path is already locked by an open handle on this mount, adopting it at migration time keeps the renamed path protected after that handle closes, without waiting on a lock this mount already holds. If the handle was released mid-migration the claimed lock is stopped, and a fresh acquire covers the case where it was already gone. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: claim the target handle's lock before the rename runs Skipping the new-path lock when a handle already holds it let that handle's close release the lock mid-rename, leaving the path unguarded until the response migrated it. Take over the lock at check time and hold it for the rename's duration: the response adopts it for the migrating handle, or it returns to the target handle / is released on failure. The target handle lookup also falls back to the entry's stored inode for a forgotten path mapping. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: read handle locks only under the fh lock during rename The loose dlmLock reads raced ReleaseHandle, which now mutates the lock inside the handle lock; check and claim it under the same hold. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
233 lines
7.6 KiB
Go
233 lines
7.6 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
|
|
// 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,
|
|
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", 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.
|
|
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)
|
|
|
|
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()
|
|
}
|
|
}
|