mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-13 07:19:18 +01:00
* fix(mount): condition content flushes on the observed entry * mount: stop retrying a flush the filer refused on its condition The filer reports a failed CreateEntry condition as FilerError_PRECONDITION_FAILED in the response, which surfaces as filer_pb.ErrPreconditionFailed rather than a FailedPrecondition status. The flush retry loop treated it as transient and replayed the refused flush three more times over 7 s before returning EIO. The test filer now answers like the real one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: take the handle base from the entry the filer stored After an acknowledged flush the handle base was the requested entry, but the filer stores it with a fresh commit_ts_ns (and fills crtime, inode, atime), so the next IF_ENTRY_EQUAL flush on the same handle never matched: write, fsync, write, fsync returned EIO, as did close after a periodic metadata flush and the first flush after an eager create. Use the stored entry from the acknowledgement's metadata event. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: condition the periodic metadata flush like the close flush The periodic metadata flush of an open, dirty handle sent the same whole entry through an unconditional CreateEntry, so a file held open past the flush interval still overwrote a peer's acknowledged append, and the close flush then matched the overwritten state. Both paths now publish through one helper that carries the handle's write condition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: republish a refused flush once when the stored entry is unchanged A handle's base can differ from the stored entry without any peer write: a rename while the file is open restamps the entry under its new name, and with -map.uid a filer id that is also a mapped local id does not map back to itself. Each made every flush of the handle fail with EIO. On a miss, read the stored entry and, if it still holds the observed state apart from name, commit stamp, id encoding and derived size, retry once with that entry as the condition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: take the filer's entry once a write open holds the DLM lock The lock orders this writer after the previous holder, but the holder's flush may not have reached the local cache yet, so the handle's base named the entry from before the wait and the conditional flush refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: share the open-existing path of Create Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: open the file a previous DLM holder created during a deferred create A deferred create never asks the filer, so a create that waited for the lock went on to publish over a file the previous holder had just made, and its flush expecting no entry was refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * master: send the lock ring snapshot to mounts A mount running -dlm is a lock client like the S3 gateway, and needs the ring on connect to reach a file's owner filer before the next membership change is broadcast. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: follow the masters' lock ring under -dlm The lock client only knew the seed filer, so every lock took the forward hop and the mount had no way to tell which filer owns a file. Subscribe to the masters named by the filer, like the S3 gateway, and wait briefly for the first ring before serving. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: read the DLM-ordered entry from the file's owner filer The filer checks a flush's condition on the owner of the path's write route key, and another filer may not have applied the previous holder's flush yet. Look the entry up on that owner once the ring is known. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
210 lines
7.4 KiB
Go
210 lines
7.4 KiB
Go
package mount
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"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"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// loopFlushDirtyMetadata periodically flushes dirty file metadata to the filer.
|
|
// This protects newly uploaded chunks from being purged by volume.fsck orphan cleanup
|
|
// for files that remain open for extended periods without being closed.
|
|
//
|
|
// The problem: When a file is opened and written to continuously, chunks are uploaded
|
|
// to volume servers but the file metadata (containing chunk references) is only saved
|
|
// to the filer on file close or fsync. If volume.fsck runs during this window, it may
|
|
// identify these chunks as orphans (since they're not referenced in filer metadata)
|
|
// and purge them.
|
|
//
|
|
// This background task periodically flushes metadata for open files, ensuring chunk
|
|
// references are visible to volume.fsck even before files are closed.
|
|
func (wfs *WFS) loopFlushDirtyMetadata() {
|
|
if wfs.option.MetadataFlushSeconds <= 0 {
|
|
glog.V(0).Infof("periodic metadata flush disabled")
|
|
return
|
|
}
|
|
|
|
flushInterval := time.Duration(wfs.option.MetadataFlushSeconds) * time.Second
|
|
glog.V(0).Infof("periodic metadata flush enabled, interval: %v", flushInterval)
|
|
|
|
ticker := time.NewTicker(flushInterval)
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
|
wfs.flushAllDirtyMetadata()
|
|
}
|
|
}
|
|
|
|
// flushAllDirtyMetadata iterates through all open file handles and flushes
|
|
// metadata for files that have dirty metadata (chunks uploaded but not yet persisted).
|
|
func (wfs *WFS) flushAllDirtyMetadata() {
|
|
// Collect file handles with dirty metadata under a read lock
|
|
var dirtyHandles []*FileHandle
|
|
wfs.fhMap.RLock()
|
|
for _, fh := range wfs.fhMap.inode2fh {
|
|
if fh.dirtyMetadata {
|
|
dirtyHandles = append(dirtyHandles, fh)
|
|
}
|
|
}
|
|
wfs.fhMap.RUnlock()
|
|
|
|
if len(dirtyHandles) == 0 {
|
|
return
|
|
}
|
|
|
|
glog.V(3).Infof("flushing metadata for %d open files", len(dirtyHandles))
|
|
|
|
// Process dirty handles in parallel with limited concurrency
|
|
var wg sync.WaitGroup
|
|
concurrency := wfs.option.ConcurrentWriters
|
|
if concurrency <= 0 {
|
|
concurrency = 16
|
|
}
|
|
sem := make(chan struct{}, concurrency)
|
|
|
|
for _, fh := range dirtyHandles {
|
|
wg.Add(1)
|
|
sem <- struct{}{}
|
|
go func(handle *FileHandle) {
|
|
defer wg.Done()
|
|
defer func() { <-sem }()
|
|
if err := wfs.flushFileMetadata(handle); err != nil {
|
|
glog.Warningf("failed to flush metadata for %s: %v", handle.FullPath(), err)
|
|
}
|
|
}(fh)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
// flushFileMetadata flushes the current file metadata to the filer without
|
|
// flushing dirty pages from memory. This updates chunk references in the filer
|
|
// so volume.fsck can see them, while keeping data in the write buffer.
|
|
//
|
|
// When -dlm is enabled, the distributed lock is already held by the FileHandle
|
|
// from open-for-write through close, so no additional distributed lock is
|
|
// needed here. The local fhLockTable lock below serializes within this mount.
|
|
func (wfs *WFS) flushFileMetadata(fh *FileHandle) error {
|
|
// Acquire exclusive lock on the file handle
|
|
fhActiveLock := fh.wfs.fhLockTable.AcquireLock("flushMetadata", fh.fh, util.ExclusiveLock)
|
|
defer fh.wfs.fhLockTable.ReleaseLock(fh.fh, fhActiveLock)
|
|
|
|
// Double-check dirty flag under lock
|
|
if !fh.dirtyMetadata {
|
|
return nil
|
|
}
|
|
|
|
fileFullPath := fh.FullPath()
|
|
dir, name := fileFullPath.DirAndName()
|
|
|
|
glog.V(4).Infof("flushFileMetadata %s fh %d", fileFullPath, fh.fh)
|
|
|
|
if fh.GetEntry() == nil {
|
|
return nil
|
|
}
|
|
|
|
// Snapshot the current chunk list. Async uploader goroutines call
|
|
// entry.AppendChunks while we run CompactFileChunks / MaybeManifestize
|
|
// below — those steps can take seconds (manifest upload is a round trip).
|
|
// We must remember the snapshot length so we can splice any chunks that
|
|
// land after the snapshot back in once we reassign entry.Chunks; without
|
|
// that, the naked overwrite below clobbers them and the file ends up
|
|
// missing chunk references for data the volumes already store.
|
|
var snapshotChunks []*filer_pb.FileChunk
|
|
var snapshotLen int
|
|
fh.UpdateEntry(func(e *filer_pb.Entry) {
|
|
// Do not stamp mtime/ctime here. Write/SetAttr already maintain
|
|
// them on the entry; overwriting at periodic-flush time clobbered
|
|
// user-set mtime (utimes/touch -m -d) once the timer fired.
|
|
e.Name = name
|
|
snapshotLen = len(e.Chunks)
|
|
if snapshotLen > 0 {
|
|
snapshotChunks = append([]*filer_pb.FileChunk(nil), e.Chunks...)
|
|
}
|
|
})
|
|
|
|
if snapshotLen == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Separate manifest and non-manifest chunks
|
|
manifestChunks, nonManifestChunks := filer.SeparateManifestChunks(snapshotChunks)
|
|
|
|
// Compact chunks to remove fully overlapped ones
|
|
compactedChunks, _, err := filer.CompactFileChunks(context.Background(), wfs.LookupFn(), nonManifestChunks)
|
|
if err != nil {
|
|
return fmt.Errorf("compact chunks of %s: %w", fileFullPath, err)
|
|
}
|
|
|
|
// Try to create manifest chunks for large files
|
|
// no chunk deleter here: a failed fold reports the blobs it saved
|
|
compactedChunks, manifestErr := filer.MaybeManifestize(wfs.saveDataAsChunk(fileFullPath), nil, compactedChunks)
|
|
if manifestErr != nil {
|
|
glog.V(0).Infof("flushFileMetadata MaybeManifestize: %v", manifestErr)
|
|
}
|
|
|
|
processedPrefix := append(compactedChunks, manifestChunks...)
|
|
|
|
// Splice the processed snapshot back in, preserving any chunks that
|
|
// async uploaders appended after our snapshot, and clone the resulting
|
|
// entry for the filer request while still holding the lock so the
|
|
// request can't observe a half-merged state.
|
|
var requestEntry *filer_pb.Entry
|
|
fh.UpdateEntry(func(e *filer_pb.Entry) {
|
|
// e.Chunks[snapshotLen:] is whatever async uploaders appended while
|
|
// we processed the snapshot. processedPrefix is freshly built and not
|
|
// referenced elsewhere, so we can append straight onto it.
|
|
var tail []*filer_pb.FileChunk
|
|
if len(e.Chunks) > snapshotLen {
|
|
tail = e.Chunks[snapshotLen:]
|
|
}
|
|
e.Chunks = append(processedPrefix, tail...)
|
|
requestEntry = proto.Clone(e).(*filer_pb.Entry)
|
|
})
|
|
request := &filer_pb.CreateEntryRequest{
|
|
Directory: string(dir),
|
|
Entry: requestEntry,
|
|
Signatures: []int32{wfs.signature},
|
|
SkipCheckParentDirectory: true,
|
|
}
|
|
|
|
// Snapshot with local ids before the request mapping mutates the clone:
|
|
// on ack this becomes the handle's base, judged against future events.
|
|
baseSnapshot := proto.Clone(requestEntry).(*filer_pb.Entry)
|
|
wfs.mapPbIdFromLocalToFiler(request.Entry)
|
|
|
|
resp, err := wfs.createContentEntry(context.Background(), fh, request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
event := resp.GetMetadataEvent()
|
|
if event == nil {
|
|
event = metadataUpdateEvent(string(dir), request.Entry)
|
|
if event != nil {
|
|
event.TsNs = ackVersionTsNs(resp)
|
|
}
|
|
}
|
|
fh.setAuthoritativeBase(wfs.ackedBase(resp, baseSnapshot))
|
|
fh.advanceEntryVersion(ackVersionTsNs(resp), resp.GetLogSignature())
|
|
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil {
|
|
glog.Warningf("flushFileMetadata %s: best-effort metadata apply failed: %v", fileFullPath, applyErr)
|
|
wfs.inodeToPath.InvalidateChildrenCache(util.FullPath(dir))
|
|
}
|
|
|
|
glog.V(3).Infof("flushed metadata for %s with %d chunks", fileFullPath, len(requestEntry.GetChunks()))
|
|
|
|
// Note: We do NOT clear dirtyMetadata here because:
|
|
// 1. There may still be dirty pages in the write buffer
|
|
// 2. The file may receive more writes before close
|
|
// 3. dirtyMetadata will be cleared on the final flush when the file is closed
|
|
|
|
return nil
|
|
}
|