From 2264941a17a8f6641840c4f1e520cb1a529a16c1 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Fri, 10 Apr 2026 13:05:48 -0700 Subject: [PATCH] fix(mount): update parent directory mtime/ctime on deferred file create (#9021) * fix(mount): update parent directory mtime/ctime on deferred file create * style: run go fmt on mount package --- test/pjdfstest/known_failures.txt | 6 ------ weed/mount/meta_cache/meta_cache.go | 19 +++++++++++++++++++ weed/mount/weedfs.go | 6 +++--- weed/mount/weedfs_attr.go | 12 ++++++++++++ weed/mount/weedfs_dir_mkrm.go | 1 - weed/mount/weedfs_file_mkrm.go | 2 ++ weed/mount/weedfs_stream_mutate.go | 6 +++--- 7 files changed, 39 insertions(+), 13 deletions(-) diff --git a/test/pjdfstest/known_failures.txt b/test/pjdfstest/known_failures.txt index 41f59bab7c..b7ceb5be99 100644 --- a/test/pjdfstest/known_failures.txt +++ b/test/pjdfstest/known_failures.txt @@ -42,12 +42,6 @@ tests/unlink/03.t tests/rename/23.t tests/rename/24.t -# ── Parent directory mtime/ctime on deferred file create ─────────────── -# When file creation is deferred (not flushed to filer immediately), -# the parent directory mtime/ctime cannot be updated without invalidating -# the just-cached child entry in the metadata cache. -tests/open/00.t - # ── Directory rename permission edge case ────────────────────────────── # Cross-directory rename of a subdirectory with restricted permissions # causes cascading test failures within the test file. diff --git a/weed/mount/meta_cache/meta_cache.go b/weed/mount/meta_cache/meta_cache.go index ffa9ba3a63..ec28912381 100644 --- a/weed/mount/meta_cache/meta_cache.go +++ b/weed/mount/meta_cache/meta_cache.go @@ -278,6 +278,25 @@ func (mc *MetaCache) UpdateEntry(ctx context.Context, entry *filer.Entry) error return mc.localStore.UpdateEntry(ctx, entry) } +// TouchDirMtimeCtime updates the mtime and ctime of a directory entry +// directly in the local metadata cache store. This avoids a filer RPC +// round-trip and the associated metadata event that would invalidate +// recently cached child entries. +func (mc *MetaCache) TouchDirMtimeCtime(ctx context.Context, dirPath util.FullPath, now time.Time) error { + mc.Lock() + defer mc.Unlock() + entry, err := mc.localStore.FindEntry(ctx, dirPath) + if err != nil { + return err + } + if entry == nil { + return nil + } + entry.Attr.Mtime = now + entry.Attr.Ctime = now + return mc.localStore.UpdateEntry(ctx, entry) +} + func (mc *MetaCache) FindEntry(ctx context.Context, fp util.FullPath) (entry *filer.Entry, err error) { mc.RLock() defer mc.RUnlock() diff --git a/weed/mount/weedfs.go b/weed/mount/weedfs.go index 992e70f80e..ba6644f1f2 100644 --- a/weed/mount/weedfs.go +++ b/weed/mount/weedfs.go @@ -63,9 +63,9 @@ type Option struct { MountMtime time.Time MountParentInode uint64 - VolumeServerAccess string // how to access volume servers - Cipher bool // whether encrypt data on volume server - UidGidMapper *meta_cache.UidGidMapper + VolumeServerAccess string // how to access volume servers + Cipher bool // whether encrypt data on volume server + UidGidMapper *meta_cache.UidGidMapper IncludeSystemEntries bool // Periodic metadata flush interval in seconds (0 to disable) diff --git a/weed/mount/weedfs_attr.go b/weed/mount/weedfs_attr.go index bca4cb45be..7cd6cabea9 100644 --- a/weed/mount/weedfs_attr.go +++ b/weed/mount/weedfs_attr.go @@ -1,6 +1,7 @@ package mount import ( + "context" "os" "syscall" "time" @@ -279,6 +280,17 @@ func (wfs *WFS) touchDirMtimeCtime(dirPath util.FullPath) { wfs.saveEntry(dirPath, dirEntry) } +// touchDirMtimeCtimeLocal updates a directory's mtime and ctime directly +// in the local metadata cache, without a filer RPC. This is used for +// deferred file creates where a filer round-trip would invalidate the +// just-cached child entry. +func (wfs *WFS) touchDirMtimeCtimeLocal(dirPath util.FullPath) { + now := time.Now() + if err := wfs.metaCache.TouchDirMtimeCtime(context.Background(), dirPath, now); err != nil { + glog.V(3).Infof("touchDirMtimeCtimeLocal %s: %v", dirPath, err) + } +} + const atimeMapMaxSize = 8192 // setAtime stores an in-memory atime for an inode. The map is bounded; diff --git a/weed/mount/weedfs_dir_mkrm.go b/weed/mount/weedfs_dir_mkrm.go index d569e265da..b7bf4f747a 100644 --- a/weed/mount/weedfs_dir_mkrm.go +++ b/weed/mount/weedfs_dir_mkrm.go @@ -56,7 +56,6 @@ func (wfs *WFS) Mkdir(cancel <-chan struct{}, in *fuse.MkdirIn, name string, out // but BEFORE outputPbEntry writes attributes to the kernel. We restore // explicitly below instead of using defer so the kernel gets local values. - request := &filer_pb.CreateEntryRequest{ Directory: string(dirFullPath), Entry: newEntry, diff --git a/weed/mount/weedfs_file_mkrm.go b/weed/mount/weedfs_file_mkrm.go index 0dc69b617b..bb7c8b4775 100644 --- a/weed/mount/weedfs_file_mkrm.go +++ b/weed/mount/weedfs_file_mkrm.go @@ -312,6 +312,8 @@ func (wfs *WFS) createRegularFile(dirFullPath util.FullPath, name string, mode u if insertErr := wfs.metaCache.InsertEntry(context.Background(), filer.FromPbEntry(string(dirFullPath), newEntry)); insertErr != nil { glog.Warningf("createFile %s: insert local entry: %v", entryFullPath, insertErr) } + wfs.inodeToPath.TouchDirectory(dirFullPath) + wfs.touchDirMtimeCtimeLocal(dirFullPath) glog.V(3).Infof("createFile %s: deferred to flush", entryFullPath) return inode, newEntry, fuse.OK } diff --git a/weed/mount/weedfs_stream_mutate.go b/weed/mount/weedfs_stream_mutate.go index 2f297c771a..b4b3298fa2 100644 --- a/weed/mount/weedfs_stream_mutate.go +++ b/weed/mount/weedfs_stream_mutate.go @@ -46,9 +46,9 @@ type streamMutateMux struct { cancel context.CancelFunc grpcConn *grpc.ClientConn // dedicated connection, closed on stream teardown closed bool - disabled bool // permanently disabled if filer doesn't support the RPC + disabled bool // permanently disabled if filer doesn't support the RPC stopSend chan struct{} // closed to signal the current sendLoop to exit - generation uint64 // incremented each time a new stream is created + generation uint64 // incremented each time a new stream is created nextID atomic.Uint64 @@ -64,7 +64,7 @@ type streamMutateMux struct { type streamMutateReq struct { req *filer_pb.StreamMutateEntryRequest errCh chan error // send error feedback - gen uint64 // stream generation this request targets + gen uint64 // stream generation this request targets } func newStreamMutateMux(wfs *WFS) *streamMutateMux {