Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

filer: opt-in inode->path index with shell nfs.enable / nfs.disable#10277

Draft
chrislusf wants to merge 4 commits into
masterseaweedfs/seaweedfs:masterfrom
nfs-enable-inode-indexseaweedfs/seaweedfs:nfs-enable-inode-indexCopy head branch name to clipboard
Draft

filer: opt-in inode->path index with shell nfs.enable / nfs.disable#10277
chrislusf wants to merge 4 commits into
masterseaweedfs/seaweedfs:masterfrom
nfs-enable-inode-indexseaweedfs/seaweedfs:nfs-enable-inode-indexCopy head branch name to clipboard

Conversation

@chrislusf

@chrislusf chrislusf commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Groundwork for bringing back the weed nfs gateway. The gateway resolves NFS filehandles by reversing an inode to its path, which the deterministic hash inode cannot do, so the filer needs a reverse index — but nobody should pay for it unless they export something.

  • filer.conf PathConf gains an inode_index option. The store wrapper writes inode->path rows only for entries under a marked location; with no rule (the default) no index work happens at all. Every filer picks the rule up through the normal filer.conf reload, so there is no per-process flag to keep in sync across a cluster.
  • nfs.enable -path=/exports -apply marks the path and backfills rows for the entries already under it, assigning inodes to entries that predate filer-assigned inodes, so pre-existing files resolve by filehandle immediately. Re-running is idempotent.
  • nfs.disable -path=/exports -apply removes the mark and deletes the rows, so turning NFS off leaves no index data behind.

Verified against a live server: enable backfills exactly the in-scope entries, files created afterwards get rows through the filer-side hook, out-of-scope paths get nothing, and disable clears every row.

The gateway restore itself comes as a follow-up on top of this.

https://claude.ai/code/session_01AQDAwEYZsGsmGZKo3myZxB

Summary by CodeRabbit

  • New Features

    • Added support for maintaining path-to-inode mappings for NFS-backed directories.
    • Introduced commands to enable or disable NFS export support for a path, with optional preview mode before applying changes.
  • Bug Fixes

    • Improved handling of renamed, deleted, and hard-linked files so path mappings stay accurate.
    • Added safer recursive cleanup for directory removals and better support for existing records during upgrades.

chrislusf added 4 commits July 8, 2026 16:41
…ions

The NFS gateway resolves filehandles by reversing an inode to its path,
which the deterministic hash inode cannot do. Bring back the reverse
index, but scoped: rows are written only for entries under a filer.conf
location with inode_index set, so with no such rule the filer does no
extra work. All filers pick the rule up through the usual filer.conf
reload, with no per-process flags to keep in sync.

Claude-Session: https://claude.ai/code/session_01AQDAwEYZsGsmGZKo3myZxB
One command to make a filer path exportable over NFS: nfs.enable marks
the path with inode_index in filer.conf and backfills index rows for
the entries already under it, assigning inodes where missing, so
existing files resolve by filehandle right away. nfs.disable clears
the mark and deletes the rows, leaving no index data behind.

Claude-Session: https://claude.ai/code/session_01AQDAwEYZsGsmGZKo3myZxB
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds an inode-to-path reverse index for NFS filehandle resolution. It extends proto schemas with an inode_index config flag, implements InodeIndexRecord with JSON encoding and legacy upgrade support, wires FilerStoreWrapper to maintain the index on writes/deletes, and adds nfs.enable/nfs.disable shell commands for backfill and cleanup.

Changes

Inode Index for NFS Filehandle Resolution

Layer / File(s) Summary
Proto schema for inode_index
weed/pb/filer.proto, other/java/client/src/main/proto/filer.proto
Adds a boolean inode_index field (17) to FilerConf.PathConf; also adds replicas field to AssignVolumeResponse.
Config merge/clone and activation check
weed/filer/filer_conf.go, weed/filer/filer_conf_test.go
ClonePathConf/mergePathConf handle InodeIndex; new InodeIndexActiveUnder determines if indexing applies to a directory path.
InodeIndexRecord type and encoding
weed/filer/filer_inode_index.go
Defines InodeIndexRecord with JSON encode/decode, legacy-value upgrade, and path add/remove/canonical/all-paths helpers.
FilerStoreWrapper maintenance hooks
weed/filer/filerstore_wrapper.go, weed/filer/filer.go
Wraps insert/update/delete/delete-folder-children to record inode index writes and removals; SetStore wires a filerConfFn closure for dynamic config lookup.
Inode index lifecycle tests
weed/filer/filer_inode_index_test.go
Covers insert/update/delete, hard links, legacy upgrade, rename/delete ordering, disabled indexing, prefix scoping, recursive delete, and activation checks.
nfs.enable command with backfill
weed/shell/command_nfs_enable.go
Enables inode_index for a path, backfills missing inodes, and upserts inode index KV rows during tree traversal.
nfs.disable command with cleanup
weed/shell/command_nfs_disable.go
Disables inode_index for a path and removes corresponding inode index KV rows via BFS traversal.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ShellUser
  participant NfsEnableCommand
  participant Filer
  participant FilerStoreWrapper
  participant KVStore

  ShellUser->>NfsEnableCommand: nfs.enable -path /exports -apply
  NfsEnableCommand->>Filer: read filer.conf
  NfsEnableCommand->>Filer: save updated filer.conf (inode_index=true)
  NfsEnableCommand->>Filer: TraverseBfs export path
  loop each entry
    NfsEnableCommand->>Filer: LookupDirectoryEntry / UpdateEntry (assign inode)
    NfsEnableCommand->>KVStore: KvGet inode index key
    NfsEnableCommand->>KVStore: KvPut updated InodeIndexRecord
  end
  NfsEnableCommand-->>ShellUser: totals (indexed rows, assigned inodes)

  ShellUser->>FilerStoreWrapper: InsertEntry / UpdateEntry / DeleteEntry
  FilerStoreWrapper->>Filer: check FilerConf.InodeIndexActiveUnder(path)
  FilerStoreWrapper->>KVStore: recordInodeIndexWrite / recordInodeIndexRemoval
  KVStore-->>FilerStoreWrapper: ack
  FilerStoreWrapper-->>ShellUser: operation result
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: an opt-in inode-to-path index plus the NFS enable/disable commands.
Description check ✅ Passed The description covers the problem, approach, and validation, though it omits the template's checklist sections and formal headings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch nfs-enable-inode-index

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an opt-in secondary inode_index mechanism to maintain reverse inode-to-path lookups, supporting the NFS gateway. It updates the protobuf definitions, modifies FilerStoreWrapper to handle index updates during entry mutations, and adds nfs.enable and nfs.disable shell commands for configuration and index backfilling/cleanup. Feedback highlights two key issues: a prefix-matching bug in InodeIndexActiveUnder that could incorrectly match sibling directories sharing a common prefix, and a missing context cancellation check in the recursive directory walk loop of collectInodeIndexEntriesRecursive.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread weed/filer/filer_conf.go
Comment on lines +246 to +252
fc.rules.Walk(func(key []byte, value *filer_pb.FilerConf_PathConf) bool {
if value.InodeIndex && strings.HasPrefix(string(key), dirPath) {
active = true
return false
}
return true
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current prefix check strings.HasPrefix(string(key), dirPath) can lead to incorrect matches for sibling directories that share a common prefix (e.g., /exports-other would match when checking for /exports). Since all paths are absolute, we should ensure we only match exact paths or true subdirectories by checking for a trailing slash / boundary.

Suggested change
fc.rules.Walk(func(key []byte, value *filer_pb.FilerConf_PathConf) bool {
if value.InodeIndex && strings.HasPrefix(string(key), dirPath) {
active = true
return false
}
return true
})
fc.rules.Walk(func(key []byte, value *filer_pb.FilerConf_PathConf) bool {
keyStr := string(key)
if value.InodeIndex && (dirPath == "/" || keyStr == dirPath || strings.HasPrefix(keyStr, dirPath+"/")) {
active = true
return false
}
return true
})

Comment on lines +283 to +284
for {
page := make([]*Entry, 0, PaginationSize)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The comment in collectInodeIndexEntries states that caller cancellation is honored during the walk to prevent DoS vectors on pathological directories. However, the recursive helper collectInodeIndexEntriesRecursive does not check ctx.Err() or ctx.Done() in its loop. We should check ctx.Err() at the start of each iteration to actually honor the cancellation.

	for {
		if err := ctx.Err(); err != nil {
			return err
		}
		page := make([]*Entry, 0, PaginationSize)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
weed/filer/filer_inode_index.go (1)

262-276: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Consider streaming instead of accumulating the whole subtree in memory.

collectInodeIndexEntries buffers every in-scope entry of the subtree into a single []inodeIndexEntry before the caller processes it. For a large indexed directory tree (a DeleteFolderChildren on a big -path=/exports), this is an unbounded allocation proportional to the number of descendants. A visitor/callback that removes index rows as entries are discovered would bound memory. Deferrable, but worth considering before this backs the NFS gateway at scale.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@weed/filer/filer_inode_index.go` around lines 262 - 276, The inode index
cleanup path currently buffers the entire subtree in collectInodeIndexEntries,
which can grow without bound for large directories. Refactor the traversal in
collectInodeIndexEntries and collectInodeIndexEntriesRecursive to stream entries
to a visitor/callback that processes and deletes each inodeIndexEntry as it is
discovered, instead of appending everything into a single slice. Keep the
existing context-cancellation behavior, but make the caller consume entries
incrementally so memory use stays bounded.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@weed/filer/filer_conf.go`:
- Around line 241-254: `FilerConf.InodeIndexActiveUnder` is using a raw prefix
match that can treat sibling paths as descendants. Update the walk logic in
`InodeIndexActiveUnder` to only consider paths that are actually under `dirPath`
by checking a path boundary after the prefix (for example, exact match or
separator-delimited descendant), while keeping the existing `MatchStorageRule`
fast path intact. Reference the `InodeIndexActiveUnder` method and its
`fc.rules.Walk` callback when making the fix.

In `@weed/filer/filer_inode_index.go`:
- Around line 278-310: The recursive inode-index walk in
collectInodeIndexEntriesRecursive should stop promptly when the caller cancels,
since this path bypasses context.WithoutCancel and may keep paging in backends
that do not poll ctx. Add a ctx.Err() check in the paging loop (before listing
and/or before recursing) and return that error immediately so the walk exits
cleanly on cancellation. Keep the fix localized to
collectInodeIndexEntriesRecursive and preserve the existing pagination and
recursion behavior otherwise.

In `@weed/shell/command_nfs_enable.go`:
- Around line 183-219: The inode row update path in upsertInodeIndexRow is racy
because TraverseBfs can invoke it concurrently for the same inode, allowing
overlapping KvGet/AddPath/KvPut cycles to overwrite one another. Serialize the
read-modify-write per inode by adding a per-inode lock around the
WithFilerClient block in upsertInodeIndexRow, or otherwise make the traversal
single-threaded before calling it. Also ensure the shared writer used by the
progress output is protected with synchronization if it remains enabled during
concurrent traversal.

---

Nitpick comments:
In `@weed/filer/filer_inode_index.go`:
- Around line 262-276: The inode index cleanup path currently buffers the entire
subtree in collectInodeIndexEntries, which can grow without bound for large
directories. Refactor the traversal in collectInodeIndexEntries and
collectInodeIndexEntriesRecursive to stream entries to a visitor/callback that
processes and deletes each inodeIndexEntry as it is discovered, instead of
appending everything into a single slice. Keep the existing context-cancellation
behavior, but make the caller consume entries incrementally so memory use stays
bounded.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dd19cde7-81af-4fe2-b434-5ee32bac12e2

📥 Commits

Reviewing files that changed from the base of the PR and between 11fd20e and 6d876df.

⛔ Files ignored due to path filters (1)
  • weed/pb/filer_pb/filer.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (10)
  • other/java/client/src/main/proto/filer.proto
  • weed/filer/filer.go
  • weed/filer/filer_conf.go
  • weed/filer/filer_conf_test.go
  • weed/filer/filer_inode_index.go
  • weed/filer/filer_inode_index_test.go
  • weed/filer/filerstore_wrapper.go
  • weed/pb/filer.proto
  • weed/shell/command_nfs_disable.go
  • weed/shell/command_nfs_enable.go

Comment thread weed/filer/filer_conf.go
Comment on lines +241 to +254
func (fc *FilerConf) InodeIndexActiveUnder(dirPath string) bool {
if fc.MatchStorageRule(dirPath).InodeIndex {
return true
}
active := false
fc.rules.Walk(func(key []byte, value *filer_pb.FilerConf_PathConf) bool {
if value.InodeIndex && strings.HasPrefix(string(key), dirPath) {
active = true
return false
}
return true
})
return active
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

InodeIndexActiveUnder prefix check doesn't respect path boundaries.

strings.HasPrefix(string(key), dirPath) will produce false positives for sibling paths. For example, if a rule exists for /exports_old with InodeIndex=true, calling InodeIndexActiveUnder("/exports") would incorrectly return true because strings.HasPrefix("/exports_old", "/exports") is true.

This won't cause data corruption (the per-entry MatchStorageRule check would still return false for entries under /exports), but it causes unnecessary index maintenance code paths to be entered for out-of-scope directories.

🛡️ Proposed fix for path boundary check
 func (fc *FilerConf) InodeIndexActiveUnder(dirPath string) bool {
 	if fc.MatchStorageRule(dirPath).InodeIndex {
 		return true
 	}
 	active := false
 	fc.rules.Walk(func(key []byte, value *filer_pb.FilerConf_PathConf) bool {
-		if value.InodeIndex && strings.HasPrefix(string(key), dirPath) {
+		if value.InodeIndex && (dirPath == "/" || strings.HasPrefix(string(key), dirPath+"/")) {
 			active = true
 			return false
 		}
 		return true
 	})
 	return active
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (fc *FilerConf) InodeIndexActiveUnder(dirPath string) bool {
if fc.MatchStorageRule(dirPath).InodeIndex {
return true
}
active := false
fc.rules.Walk(func(key []byte, value *filer_pb.FilerConf_PathConf) bool {
if value.InodeIndex && strings.HasPrefix(string(key), dirPath) {
active = true
return false
}
return true
})
return active
}
func (fc *FilerConf) InodeIndexActiveUnder(dirPath string) bool {
if fc.MatchStorageRule(dirPath).InodeIndex {
return true
}
active := false
fc.rules.Walk(func(key []byte, value *filer_pb.FilerConf_PathConf) bool {
if value.InodeIndex && (dirPath == "/" || strings.HasPrefix(string(key), dirPath+"/")) {
active = true
return false
}
return true
})
return active
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@weed/filer/filer_conf.go` around lines 241 - 254,
`FilerConf.InodeIndexActiveUnder` is using a raw prefix match that can treat
sibling paths as descendants. Update the walk logic in `InodeIndexActiveUnder`
to only consider paths that are actually under `dirPath` by checking a path
boundary after the prefix (for example, exact match or separator-delimited
descendant), while keeping the existing `MatchStorageRule` fast path intact.
Reference the `InodeIndexActiveUnder` method and its `fc.rules.Walk` callback
when making the fix.

Comment on lines +278 to +310
func (fsw *FilerStoreWrapper) collectInodeIndexEntriesRecursive(ctx context.Context, dirPath util.FullPath, collected *[]inodeIndexEntry) error {
actualStore := fsw.getActualStore(dirPath + "/")

lastFileName := ""
includeStartFile := false
for {
page := make([]*Entry, 0, PaginationSize)
nextLastFileName, err := actualStore.ListDirectoryEntries(ctx, dirPath, lastFileName, includeStartFile, PaginationSize, func(entry *Entry) (bool, error) {
page = append(page, entry)
return true, nil
})
if err != nil {
return err
}

for _, entry := range page {
if entry.Attr.Inode != 0 {
*collected = append(*collected, inodeIndexEntry{path: entry.FullPath, inode: entry.Attr.Inode})
}
if entry.IsDirectory() {
if err := fsw.collectInodeIndexEntriesRecursive(ctx, entry.FullPath, collected); err != nil {
return err
}
}
}

if len(page) < PaginationSize {
return nil
}
lastFileName = nextLastFileName
includeStartFile = false
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether ListDirectoryEntries implementations honor ctx cancellation.
rg -nP '\bfunc\b.*\bListDirectoryEntries\b' --type=go -A15

Repository: seaweedfs/seaweedfs

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file context =="
sed -n '240,340p' weed/filer/filer_inode_index.go

echo
echo "== all ListDirectoryEntries references =="
rg -n --hidden --glob '!**/*vendor*' 'ListDirectoryEntries' .

echo
echo "== all ctx.Err or cancellation checks in filer inode index file =="
rg -n 'ctx\.Err|context\.Canceled|context\.DeadlineExceeded' weed/filer/filer_inode_index.go

echo
echo "== definitions of directory listing methods/interfaces =="
rg -n --hidden --glob '!**/*vendor*' 'type .*ListDirectoryEntries|func .*ListDirectoryEntries|ListDirectoryEntries\(' weed

Repository: seaweedfs/seaweedfs

Length of output: 23445


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== interface and wrapper =="
sed -n '1,120p' weed/filer/filerstore.go
echo
sed -n '340,460p' weed/filer/filerstore_wrapper.go

echo
echo "== filer search/listing =="
sed -n '1,180p' weed/filer/filer_search.go

echo
echo "== leveldb3 implementation cancellation =="
sed -n '300,420p' weed/filer/leveldb3/leveldb3_store.go

echo
echo "== foundationdb implementation cancellation =="
sed -n '560,700p' weed/filer/foundationdb/foundationdb_store.go

echo
echo "== cassandra implementation cancellation =="
sed -n '220,320p' weed/filer/cassandra2/cassandra_store.go

Repository: seaweedfs/seaweedfs

Length of output: 21537


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== getActualStore and related helpers =="
rg -n 'func \(fsw \*FilerStoreWrapper\) getActualStore|func \(fsw \*FilerStoreWrapper\) inodeIndexUnder|func \(fsw \*FilerStoreWrapper\) inodeIndexInScope|collectInodeIndexEntries\(' weed/filer/filer_inode_index.go weed/filer/filerstore_wrapper.go weed/filer -A40

echo
echo "== DeleteFolderChildren call path =="
rg -n 'collectInodeIndexEntries\(|DeleteFolderChildren\(' weed/filer -A20

echo
echo "== any explicit ctx.Err in directory listing implementations =="
rg -n 'ctx\.Err\(\)' weed/filer/{leveldb,leveldb2,leveldb3,rocksdb,cassandra,cassandra2,foundationdb,elastic,v7,redis,redis2,redis3,tikv,arangodb,mongodb,hbase,abstract_sql,etcd,ydb} -g '*.go'

Repository: seaweedfs/seaweedfs

Length of output: 50375


Add a ctx.Err() guard to the inode-index walk. This path bypasses the wrapper’s context.WithoutCancel, so cancellation depends on the backend polling ctx; some listing backends don’t, and the loop can keep paging after the caller has canceled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@weed/filer/filer_inode_index.go` around lines 278 - 310, The recursive
inode-index walk in collectInodeIndexEntriesRecursive should stop promptly when
the caller cancels, since this path bypasses context.WithoutCancel and may keep
paging in backends that do not poll ctx. Add a ctx.Err() check in the paging
loop (before listing and/or before recursing) and return that error immediately
so the walk exits cleanly on cancellation. Keep the fix localized to
collectInodeIndexEntriesRecursive and preserve the existing pagination and
recursion behavior otherwise.

Comment on lines +183 to +219
func upsertInodeIndexRow(commandEnv *CommandEnv, fullPath util.FullPath, inode uint64) (changed bool, err error) {
key := filer.InodeIndexKey(inode)
err = commandEnv.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
resp, kvErr := client.KvGet(context.Background(), &filer_pb.KvGetRequest{Key: key})
if kvErr != nil {
return kvErr
}
if resp.GetError() != "" {
return errors.New(resp.GetError())
}
record, decodeErr := filer.DecodeInodeIndexRecord(resp.GetValue())
if decodeErr != nil {
// Unreadable row: rebuild it from this path.
record = &filer.InodeIndexRecord{}
}
if !record.AddPath(fullPath) {
return nil
}
value, encodeErr := record.Encode()
if encodeErr != nil {
return encodeErr
}
putResp, putErr := client.KvPut(context.Background(), &filer_pb.KvPutRequest{Key: key, Value: value})
if putErr != nil {
return putErr
}
if putResp.GetError() != "" {
return errors.New(putResp.GetError())
}
changed = true
return nil
})
if err != nil {
return false, fmt.Errorf("index inode %d for %s: %v", inode, fullPath, err)
}
return changed, nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f filer_client_bfs.go
rg -nP 'func TraverseBfs' -A30 weed/pb/filer_pb/

Repository: seaweedfs/seaweedfs

Length of output: 2174


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- filer_client_bfs.go outline ---'
ast-grep outline weed/pb/filer_pb/filer_client_bfs.go --view expanded || true

echo
echo '--- relevant TraverseBfs body ---'
sed -n '1,220p' weed/pb/filer_pb/filer_client_bfs.go

echo
echo '--- command_nfs_enable.go around the reviewed lines ---'
sed -n '100,250p' weed/shell/command_nfs_enable.go

echo
echo '--- search for writer synchronization in command_nfs_enable.go ---'
rg -n 'Fprintf|writer|mutex|lock|sync\.|atomic' weed/shell/command_nfs_enable.go

Repository: seaweedfs/seaweedfs

Length of output: 8405


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- inode index record definitions ---'
rg -n 'type InodeIndexRecord|func \(.*AddPath|func \(.*Encode|DecodeInodeIndexRecord|InodeIndexKey' weed -g '!**/*_test.go'

echo
echo '--- likely implementations ---'
files=$(rg -l 'type InodeIndexRecord|DecodeInodeIndexRecord|InodeIndexKey' weed -g '!**/*_test.go' | tr '\n' ' ')
for f in $files; do
  echo "### $f"
  sed -n '1,260p' "$f"
  echo
done

Repository: seaweedfs/seaweedfs

Length of output: 29452


Serialize inode row updates before writing them back

TraverseBfs runs this callback on multiple workers, so two hard-linked entries can race on the same inode row: both KvGet, both AddPath, and the later KvPut can drop the other path. Guard the read-modify-write per inode (or make this traversal single-threaded). The shared writer also needs synchronization if concurrent progress output stays enabled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@weed/shell/command_nfs_enable.go` around lines 183 - 219, The inode row
update path in upsertInodeIndexRow is racy because TraverseBfs can invoke it
concurrently for the same inode, allowing overlapping KvGet/AddPath/KvPut cycles
to overwrite one another. Serialize the read-modify-write per inode by adding a
per-inode lock around the WithFilerClient block in upsertInodeIndexRow, or
otherwise make the traversal single-threaded before calling it. Also ensure the
shared writer used by the progress output is protected with synchronization if
it remains enabled during concurrent traversal.

@chrislusf

Copy link
Copy Markdown
Collaborator Author

This may work, but the additional inode->path index overhead is still unnecessary. Not planning to support this in the long term.

The enterprise version uses kernel module VFS, which can avoid this persistent inode problem from NFS.

@chrislusf
chrislusf marked this pull request as draft July 13, 2026 18:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Morty Proxy This is a proxified and sanitized view of the page, visit original site.