From ff4b43e0f18f0fc87a20f2aea89a86c99de3bf6e Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Thu, 22 Oct 2026 02:33:32 -0700 Subject: [PATCH] filer: serialize inode index row updates in the filer storeInodeIndex and removePathFromInodeIndex read-modify-write a shared KV row; concurrent writes on one inode (hard links share an inode) could overwrite each other and drop a path. Stripe-lock the row like the shell backfill already does. --- weed/filer/filer_inode_index.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/weed/filer/filer_inode_index.go b/weed/filer/filer_inode_index.go index 04e487ffbd..83dd6f9bdb 100644 --- a/weed/filer/filer_inode_index.go +++ b/weed/filer/filer_inode_index.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "encoding/json" "sort" + "sync" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/util" @@ -13,6 +14,14 @@ import ( const inodeIndexKeyPrefix = "filer.inode.path." const InodeIndexInitialGeneration uint64 = 1 +// inodeRowLocks serializes the read-modify-write of one inode's index row so +// concurrent writers (hard links share an inode) cannot overwrite each other. +var inodeRowLocks [256]sync.Mutex + +func inodeRowLock(inode uint64) *sync.Mutex { + return &inodeRowLocks[inode%uint64(len(inodeRowLocks))] +} + type inodeIndexEntry struct { path util.FullPath inode uint64 @@ -190,6 +199,10 @@ func (fsw *FilerStoreWrapper) storeInodeIndex(ctx context.Context, path util.Ful return nil } + lock := inodeRowLock(inode) + lock.Lock() + defer lock.Unlock() + record, err := fsw.lookupInodeIndex(ctx, inode) if err != nil { if err != ErrKvNotFound { @@ -237,6 +250,10 @@ func (fsw *FilerStoreWrapper) removePathFromInodeIndex(ctx context.Context, path return nil } + lock := inodeRowLock(inode) + lock.Lock() + defer lock.Unlock() + record, err := fsw.lookupInodeIndex(ctx, inode) if err != nil { if err == ErrKvNotFound {