Files
seaweedfs/weed/mount/inode_to_path.go
T
Chris LuandDevin 9e41500a3a mount: rebuild dropped directory listings and stale sections on lookup (#12195)
* mount: rebuild dropped directory listings on lookup

Invalidation, idle eviction, and a kernel Forget all clear a directory's
cached-listing flag, but only an expired listing was rebuilt on lookup — a
dropped one read through per entry until a ReadDir happened to run. Track
directories that once held a listing in wasCachedDirs, which survives those
drops, and let ShouldRebuildExpiredDir rebuild them on the next lookup.
Never-cached and read-through directories keep reading through.

* mount: re-validate a stale directory section on lookup

A burst of remote changes invalidates only the directory section it lands
in, but nothing re-validates that section until a ReadDir runs — so a
lookup-heavy workload reads through per entry indefinitely. EnsureNameFresh
re-lists just the section covering the looked-up name, after which lookups
in it are served from the cache again.

* mount: wait for pending flush before a stale-section refresh on lookup

A section refresh lists the range from the filer; without the wait a
pending async flush of the looked-up entry could land after the listing
and leave the refreshed section holding pre-flush metadata — the same
hazard the rebuild path already waits out.

* mount: bound the dropped-listing rebuild history

wasCachedDirs grew one full path per directory ever cached, removed only
by the read-through mark, so deleted and forgotten directories
accumulated forever. Remove the record when its name is deleted, move it
on rename so rebuild eligibility travels with the directory, and cap the
history with FIFO eviction — past the bound an evicted directory reads
through per entry, the same end state as a never-cached one.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: drop oversized stale sections and back off failed refreshes

A section that outgrew one refresh failed EnsureNameFresh with
ErrRefreshRangeTooLarge on every lookup, repeating the multi-page
listing before each per-entry read, and other refresh failures retried
with no cooldown. Drop the cached listing on ErrRefreshRangeTooLarge so
a later lookup rebuilds it re-tiled, as ReadDir does, and stamp the
rebuild attempt on other failures so retries back off under the same
cooldown the rebuild gate uses.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: version hard-link sibling syncs at the committed log position

* mount: tag dropped-listing queue slots so stale ones cannot evict

RemovePath, MovePath, and MarkDirectoryReadThrough delete the rebuild
record but leave the FIFO slot behind, so a path removed and re-cached
holds two queue entries. When the stale slot reaches the front it evicts
the newer record, and the directory reads through per lookup until the
next listing. Tag each record with an insertion generation and only
evict on a match, keeping the newer record for its own slot's turn.

* mount: refresh hard-link siblings from the filer, not the cached copy

Stamping a cached sibling with the mutation's version claimed that
position for content that may predate it — an in-flight event with
newer chunks or attributes then hits the version gate and the sibling
serves stale data until a refresh. Fetch each cached sibling's entry
from the filer after the commit and record the higher of the fetched
and committed positions, falling back to the in-place counter patch
only when the fetch itself fails.

* mount: back off failed section refreshes per section, not per directory

A failed refresh of one stale section marked a directory-wide attempt,
so every other stale section in that directory waited out the cooldown
with per-entry filer reads. Key the attempt timestamp by the section's
lower bound so unrelated ranges still refresh on their own schedule.

* mount: fence hard-linked entries by the group's newest write position

Every path of a hard-link group reads one shared content blob, but the
version gate only tracks per-path records — a sibling absent from
inodeToPath got no version from the sibling sync, so a stale section
refresh or event could roll the shared blob back for all of them.
Record a monotonic version floor per hard-link id wherever versioned
content lands, and check it in the section-refresh and event gates.

* mount: keep hard-link names landing when the group floor fences content

The per-group version floor rejected a delayed event's newEntry while
its oldPath still applied — a rename could drop the source without
adding the destination, and a section refresh could skip then mark a
live link fresh. Rebase the group's current cached content onto the
new path so the name lands without rolling the shared blob back, only
skip refresh entries already cached, and gate the sibling refresh
write under the same lock so a racing event or delete wins.

* mount: rebase hard-link entries from the shared blob when no sibling is cached

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: park group content on uncached hard-link names a stale refresh installs

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: stale the destination section when a fenced hard-link name cannot land

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: bound the sibling hard-link refresh under one request deadline

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: replace a reused-name occupant the hard-link floor would skip

* mount: claim no version for a sibling counter patched without a fetch

* mount: vacate a reused-name occupant without decrementing its group

A sibling write can fold the occupant's filer-side unlink into the shared
blob counter while the stale entry is still cached, so replacing it through
the plain insert may decrement a group that no longer counts the name and
drop a blob live siblings still read. Vacate the path like a rename first;
the counter then only ever drifts high, which leaks a blob rather than
losing shared content.

* mount: keep a fetch-failed sibling's version when patching its counter

The fallback marked the entry unversioned, which fences nothing — a
replayed delete older than the cached content could remove a live link.
Claim the version the entry already holds instead: newer in-flight events
still land and correct it, older replays stay fenced.

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-29 00:55:47 -07:00

728 lines
20 KiB
Go

package mount
import (
"sync"
"time"
"github.com/seaweedfs/go-fuse/v2/fuse"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/util"
)
type InodeToPath struct {
sync.RWMutex
nextInodeId uint64
cacheMetaTtlSec time.Duration
inode2path map[uint64]*InodeEntry
path2inode map[util.FullPath]uint64
// dirStates holds directory-only readdir-cache state, keyed by inode. An
// inode is a directory iff it has an entry here, registered at creation.
dirStates map[uint64]*dirState
// dirPaths indexes the same states by path, so a directory lookup never
// goes through the map that holds one full path per inode.
dirPaths map[util.FullPath]*dirState
// wasCachedDirs remembers directories that once held a fully cached
// listing. Unlike the flags on dirState it survives invalidation, idle
// eviction, and the state drop on kernel Forget, so a lookup can rebuild
// a dropped listing instead of reading through per entry forever.
// MarkDirectoryReadThrough removes oversized directories from it,
// RemovePath drops deleted ones, and wasCachedOrder bounds the retained
// history with FIFO eviction. Records are tagged with an insertion
// generation so a stale queue slot can never evict a path's newer record.
wasCachedDirs map[util.FullPath]uint64
wasCachedOrder []wasCachedEntry
wasCachedGen uint64
}
// wasCachedEntry pairs a queued path with the insertion generation that put
// it there; removals leave the slot behind, and only a matching generation
// may evict the live record.
type wasCachedEntry struct {
path util.FullPath
gen uint64
}
// InodeEntry exists per inode the kernel references. Directory cache state is
// kept out in dirStates so a file entry stays in the 32-byte size class — the
// dominant cost on a mount with millions of files. A hard link's extra paths
// hang off a pointer for the same reason.
type InodeEntry struct {
path util.FullPath
nlookup uint64
extraPaths *[]util.FullPath
}
type dirState struct {
path util.FullPath
isChildrenCached bool
readDirDirect bool
cachedExpiresTime time.Time
lastAccess time.Time
lastRefresh time.Time
lastRebuildAttempt time.Time
// lastSectionAttempts backs off section refreshes per section (keyed by
// the section's lower bound) so one failing range does not stall the
// rest of the directory.
lastSectionAttempts map[string]time.Time
subdirCount int32 // tracked in-memory for POSIX directory nlink
}
func (d *dirState) resetCacheState() {
d.isChildrenCached = false
d.readDirDirect = false
d.cachedExpiresTime = time.Time{}
d.lastRebuildAttempt = time.Time{}
d.lastSectionAttempts = nil
}
// appendPaths appends every path the inode is reachable by, primary first.
func (ie *InodeEntry) appendPaths(dst []util.FullPath) []util.FullPath {
if ie.path == "" {
return dst
}
dst = append(dst, ie.path)
if ie.extraPaths != nil {
dst = append(dst, *ie.extraPaths...)
}
return dst
}
func (ie *InodeEntry) addPath(p util.FullPath) {
if ie.path == "" {
ie.path = p
return
}
if ie.extraPaths == nil {
ie.extraPaths = &[]util.FullPath{p}
return
}
*ie.extraPaths = append(*ie.extraPaths, p)
}
func (ie *InodeEntry) replacePath(from, to util.FullPath) {
if ie.path == from {
ie.path = to
}
if ie.extraPaths == nil {
return
}
for i, p := range *ie.extraPaths {
if p == from {
(*ie.extraPaths)[i] = to
}
}
}
func (ie *InodeEntry) setExtraPaths(extra []util.FullPath) {
if len(extra) == 0 {
ie.extraPaths = nil
return
}
ie.extraPaths = &extra
}
// removeOnePath promotes an extra path when the primary is the one going away,
// so an entry that still has a path always has a primary one.
func (ie *InodeEntry) removeOnePath(p util.FullPath) bool {
if ie.path == "" {
return false
}
if ie.path == p {
if ie.extraPaths == nil {
ie.path = ""
return true
}
extra := *ie.extraPaths
ie.path = extra[0]
ie.setExtraPaths(extra[1:])
return true
}
if ie.extraPaths == nil {
return false
}
extra := *ie.extraPaths
for i, x := range extra {
if x != p {
continue
}
ie.setExtraPaths(append(extra[:i], extra[i+1:]...))
return true
}
return false
}
func NewInodeToPath(root util.FullPath, ttlSec int) *InodeToPath {
t := &InodeToPath{
inode2path: make(map[uint64]*InodeEntry),
path2inode: make(map[util.FullPath]uint64),
dirStates: make(map[uint64]*dirState),
dirPaths: make(map[util.FullPath]*dirState),
wasCachedDirs: make(map[util.FullPath]uint64),
cacheMetaTtlSec: time.Second * time.Duration(ttlSec),
}
t.inode2path[1] = &InodeEntry{
path: root,
nlookup: 1,
}
t.setDirState(1, &dirState{path: root, lastAccess: time.Now()})
t.path2inode[root] = 1
return t
}
// EnsurePath make sure the full path is tracked, used by symlink.
func (i *InodeToPath) EnsurePath(path util.FullPath, isDirectory bool) bool {
dir, _ := path.DirAndName()
if dir == "/" {
return true
}
if i.EnsurePath(util.FullPath(dir), true) {
i.Lookup(path, time.Now().Unix(), isDirectory, false, 0, false)
return true
}
return false
}
func (i *InodeToPath) Lookup(path util.FullPath, unixTime int64, isDirectory bool, isHardlink bool, possibleInode uint64, isLookup bool) uint64 {
i.Lock()
defer i.Unlock()
inode, found := i.path2inode[path]
if !found {
if possibleInode == 0 {
inode = path.AsInode(unixTime)
} else {
inode = possibleInode
}
if !isHardlink {
for _, found := i.inode2path[inode]; found; inode++ {
_, found = i.inode2path[inode+1]
}
}
}
i.path2inode[path] = inode
if _, found := i.inode2path[inode]; found {
if isLookup {
i.inode2path[inode].nlookup++
}
} else {
nlookup := uint64(0)
if isLookup {
nlookup = 1
}
i.inode2path[inode] = &InodeEntry{
path: path,
nlookup: nlookup,
}
if isDirectory {
i.setDirState(inode, &dirState{path: path})
}
}
return inode
}
// IncrementNlookup takes one more reference on an inode already in the table,
// reporting false if it is not there.
func (i *InodeToPath) IncrementNlookup(inode uint64) bool {
i.Lock()
defer i.Unlock()
entry, found := i.inode2path[inode]
if !found {
return false
}
entry.nlookup++
return true
}
// InodeForListing returns the inode number a readdir should report for path
// without entering it in the table. Nothing is reserved, so the collision probe
// Lookup does is skipped: the worst case is a repeated st_ino in one listing.
func (i *InodeToPath) InodeForListing(path util.FullPath, unixTime int64, possibleInode uint64) uint64 {
i.RLock()
inode, found := i.path2inode[path]
i.RUnlock()
if found {
return inode
}
if possibleInode != 0 {
return possibleInode
}
return path.AsInode(unixTime)
}
func (i *InodeToPath) AllocateInode(path util.FullPath, unixTime int64) uint64 {
if path == "/" {
return 1
}
i.Lock()
defer i.Unlock()
inode := path.AsInode(unixTime)
for _, found := i.inode2path[inode]; found; inode++ {
_, found = i.inode2path[inode]
}
return inode
}
func (i *InodeToPath) GetInode(path util.FullPath) (uint64, bool) {
if path == "/" {
return 1, true
}
i.Lock()
defer i.Unlock()
inode, found := i.path2inode[path]
if !found {
// glog.Fatalf("GetInode unknown inode for %s", path)
// this could be the parent for mount point
}
return inode, found
}
func (i *InodeToPath) GetPath(inode uint64) (util.FullPath, fuse.Status) {
i.RLock()
defer i.RUnlock()
path, found := i.inode2path[inode]
if !found || path.path == "" {
return "", fuse.ENOENT
}
return path.path, fuse.OK
}
// GetAllPaths returns a copy of all paths associated with an inode. For a
// hard-linked file, this includes every link that the mount currently knows
// about. Returns nil if the inode is unknown.
func (i *InodeToPath) GetAllPaths(inode uint64) []util.FullPath {
i.RLock()
defer i.RUnlock()
ie, found := i.inode2path[inode]
if !found {
return nil
}
return ie.appendPaths(nil)
}
func (i *InodeToPath) setDirState(inode uint64, d *dirState) {
i.dirStates[inode] = d
i.dirPaths[d.path] = d
}
// dropDirPath drops the path index only; Forget releases the state itself. A
// released directory's state keeps the path it had, so drop the index only
// while it is still the one that path resolves to: a new directory may have
// taken the name in the meantime.
func (i *InodeToPath) dropDirPath(inode uint64) {
if d := i.dirStates[inode]; d != nil && i.dirPaths[d.path] == d {
delete(i.dirPaths, d.path)
}
}
func (i *InodeToPath) dirStateOf(fullpath util.FullPath) *dirState {
return i.dirPaths[fullpath]
}
func (i *InodeToPath) HasPath(path util.FullPath) bool {
i.RLock()
defer i.RUnlock()
_, found := i.path2inode[path]
return found
}
func (i *InodeToPath) MarkChildrenCached(fullpath util.FullPath) {
i.Lock()
defer i.Unlock()
d := i.dirStateOf(fullpath)
if d == nil {
// https://github.com/seaweedfs/seaweedfs/issues/4968
// glog.Fatalf("MarkChildrenCached not found inode %v", fullpath)
glog.Warningf("MarkChildrenCached not a tracked directory: %v", fullpath)
return
}
d.isChildrenCached = true
d.readDirDirect = false
now := time.Now()
d.lastAccess = now
d.lastRefresh = now
d.lastRebuildAttempt = time.Time{}
d.lastSectionAttempts = nil
i.markWasCached(fullpath)
if i.cacheMetaTtlSec > 0 {
d.cachedExpiresTime = now.Add(i.cacheMetaTtlSec)
}
}
// maxWasCachedDirs bounds how many dropped listings a lookup may rebuild:
// past it the oldest record is evicted and that directory reads through per
// entry, the same end state as a never-cached one.
const maxWasCachedDirs = 16384
// markWasCached records a directory as once-cached, evicting the oldest
// record past maxWasCachedDirs. Callers hold the write lock.
func (i *InodeToPath) markWasCached(fullpath util.FullPath) {
if _, ok := i.wasCachedDirs[fullpath]; ok {
return
}
i.wasCachedGen++
i.wasCachedDirs[fullpath] = i.wasCachedGen
i.wasCachedOrder = append(i.wasCachedOrder, wasCachedEntry{path: fullpath, gen: i.wasCachedGen})
for len(i.wasCachedOrder) > maxWasCachedDirs {
oldest := i.wasCachedOrder[0]
i.wasCachedOrder = i.wasCachedOrder[1:]
if gen, ok := i.wasCachedDirs[oldest.path]; ok && gen == oldest.gen {
delete(i.wasCachedDirs, oldest.path)
}
}
}
func (i *InodeToPath) IsChildrenCached(fullpath util.FullPath) bool {
i.RLock()
defer i.RUnlock()
d := i.dirStateOf(fullpath)
if d == nil {
return false
}
if d.isChildrenCached {
return d.cachedExpiresTime.IsZero() || time.Now().Before(d.cachedExpiresTime)
}
return false
}
// ShouldRebuildExpiredDir reports whether a lookup should re-list the
// directory into the cache, backing off for cooldown after a failed attempt.
// It covers a fully cached directory whose TTL elapsed — a plain expiry keeps
// isChildrenCached set, unlike invalidation or eviction — and a directory
// whose previously cached listing was dropped by invalidation, idle eviction,
// or a kernel Forget. A never-cached or read-through directory keeps reading
// through: caching one on a single lookup is how oversized listings get
// pinned, so only a directory that proved cacheable earns the rebuild.
func (i *InodeToPath) ShouldRebuildExpiredDir(fullpath util.FullPath, cooldown time.Duration) bool {
i.RLock()
defer i.RUnlock()
d := i.dirStateOf(fullpath)
if d == nil || d.readDirDirect {
return false
}
if d.isChildrenCached {
if d.cachedExpiresTime.IsZero() || time.Now().Before(d.cachedExpiresTime) {
return false
}
} else if _, was := i.wasCachedDirs[fullpath]; !was {
return false
}
return d.lastRebuildAttempt.IsZero() || time.Since(d.lastRebuildAttempt) >= cooldown
}
// MarkRebuildAttempt records that a rebuild was attempted at now, so
// ShouldRebuildExpiredDir backs off for the cooldown before retrying.
func (i *InodeToPath) MarkRebuildAttempt(fullpath util.FullPath, now time.Time) {
i.Lock()
defer i.Unlock()
if d := i.dirStateOf(fullpath); d != nil {
d.lastRebuildAttempt = now
}
}
// ShouldRefreshStaleSection reports whether a lookup should re-list the
// stale section of a still-cached directory, backing off for cooldown after
// a failed attempt on that same section — a neighbouring range's failure
// does not stall this one. sectionKey is the section's lower bound.
func (i *InodeToPath) ShouldRefreshStaleSection(fullpath util.FullPath, sectionKey string, cooldown time.Duration) bool {
i.RLock()
defer i.RUnlock()
d := i.dirStateOf(fullpath)
if d == nil || d.readDirDirect || !d.isChildrenCached {
return false
}
last, ok := d.lastSectionAttempts[sectionKey]
return !ok || time.Since(last) >= cooldown
}
// MarkSectionRefreshAttempt records that a refresh of the section keyed by
// sectionKey was attempted at now — the section-level analog of
// MarkRebuildAttempt.
func (i *InodeToPath) MarkSectionRefreshAttempt(fullpath util.FullPath, sectionKey string, now time.Time) {
i.Lock()
defer i.Unlock()
d := i.dirStateOf(fullpath)
if d == nil {
return
}
if d.lastSectionAttempts == nil {
d.lastSectionAttempts = make(map[string]time.Time)
}
d.lastSectionAttempts[sectionKey] = now
}
func (i *InodeToPath) HasInode(inode uint64) bool {
if inode == 1 {
return true
}
i.RLock()
defer i.RUnlock()
_, found := i.inode2path[inode]
return found
}
func (i *InodeToPath) InvalidateAllChildrenCache() {
i.Lock()
defer i.Unlock()
for _, d := range i.dirStates {
if d.isChildrenCached {
d.resetCacheState()
}
}
}
func (i *InodeToPath) InvalidateChildrenCache(fullpath util.FullPath) {
i.Lock()
defer i.Unlock()
if d := i.dirStateOf(fullpath); d != nil {
d.resetCacheState()
}
}
// AdjustSubdirCount adjusts the subdirectory count for a directory inode.
// delta is typically +1 (mkdir) or -1 (rmdir).
func (i *InodeToPath) AdjustSubdirCount(dirPath util.FullPath, delta int32) {
i.Lock()
defer i.Unlock()
d := i.dirStateOf(dirPath)
if d == nil {
return
}
d.subdirCount += delta
if d.subdirCount < 0 {
d.subdirCount = 0
}
}
// GetSubdirCount returns the tracked subdirectory count for a directory.
func (i *InodeToPath) GetSubdirCount(dirPath util.FullPath) int32 {
i.RLock()
defer i.RUnlock()
d := i.dirStateOf(dirPath)
if d == nil {
return 0
}
return d.subdirCount
}
// SetSubdirCount sets the subdirectory count for a directory (used after readdir).
func (i *InodeToPath) SetSubdirCount(dirPath util.FullPath, count int32) {
i.Lock()
defer i.Unlock()
if d := i.dirStateOf(dirPath); d != nil {
d.subdirCount = count
}
}
func (i *InodeToPath) TouchDirectory(fullpath util.FullPath) {
i.Lock()
defer i.Unlock()
if d := i.dirStateOf(fullpath); d != nil {
d.lastAccess = time.Now()
}
}
func (i *InodeToPath) MarkDirectoryReadThrough(fullpath util.FullPath, now time.Time) bool {
i.Lock()
defer i.Unlock()
d := i.dirStateOf(fullpath)
if d == nil {
return false
}
d.isChildrenCached = false
d.readDirDirect = true
d.cachedExpiresTime = time.Time{}
d.lastAccess = now
d.lastRefresh = time.Time{}
delete(i.wasCachedDirs, fullpath)
return true
}
func (i *InodeToPath) ShouldReadDirectoryDirect(fullpath util.FullPath) bool {
i.RLock()
defer i.RUnlock()
d := i.dirStateOf(fullpath)
if d == nil {
return false
}
return d.readDirDirect
}
func (i *InodeToPath) MarkDirectoryRefreshed(fullpath util.FullPath, now time.Time) {
i.Lock()
defer i.Unlock()
d := i.dirStateOf(fullpath)
if d == nil {
return
}
d.lastRefresh = now
d.lastAccess = now
d.readDirDirect = false
if i.cacheMetaTtlSec > 0 {
d.cachedExpiresTime = now.Add(i.cacheMetaTtlSec)
}
}
func (i *InodeToPath) CollectEvictableDirs(now time.Time, idle time.Duration) []util.FullPath {
if idle <= 0 {
return nil
}
i.Lock()
defer i.Unlock()
var dirs []util.FullPath
for inode, d := range i.dirStates {
if !d.isChildrenCached {
continue
}
if d.lastAccess.IsZero() || now.Sub(d.lastAccess) < idle {
continue
}
d.resetCacheState()
if entry, ok := i.inode2path[inode]; ok {
dirs = entry.appendPaths(dirs)
}
}
return dirs
}
func (i *InodeToPath) AddPath(inode uint64, path util.FullPath) {
i.Lock()
defer i.Unlock()
i.path2inode[path] = inode
ie, found := i.inode2path[inode]
if found {
ie.addPath(path)
ie.nlookup++
} else {
i.inode2path[inode] = &InodeEntry{
path: path,
nlookup: 1,
}
}
}
// RemovePath drops the name. onStillReferenced, if given, runs under the
// table's lock when the kernel still holds lookup references to the inode:
// such an inode keeps receiving requests until the final forget, and holding
// the lock is what keeps that forget from racing whatever per-inode state the
// callback installs — Forget releases under the same lock.
func (i *InodeToPath) RemovePath(path util.FullPath, onStillReferenced func(inode uint64)) {
i.Lock()
defer i.Unlock()
inode, found := i.path2inode[path]
if found {
delete(i.path2inode, path)
i.dropDirPath(inode)
i.removePathFromInode2Path(inode, path)
if ie := i.inode2path[inode]; ie != nil && ie.nlookup > 0 && onStillReferenced != nil {
onStillReferenced(inode)
}
}
// A deleted name must never justify a listing rebuild; the record is
// keyed by path, so drop it even when the inode was already forgotten.
delete(i.wasCachedDirs, path)
}
func (i *InodeToPath) removePathFromInode2Path(inode uint64, path util.FullPath) {
ie, found := i.inode2path[inode]
if !found {
return
}
if !ie.removeOnePath(path) {
return
}
}
func (i *InodeToPath) MovePath(sourcePath, targetPath util.FullPath) (sourceInode, targetInode uint64) {
i.Lock()
defer i.Unlock()
sourceInode, sourceFound := i.path2inode[sourcePath]
if !sourceFound {
// Nothing of ours to move: the source was never visited here, or a
// redelivery already moved it. Whatever sits at the target is not ours
// to take apart on the strength of an absent source, and deciding that
// outside this lock would race a concurrent move to the same target.
return 0, 0
}
targetInode, targetFound := i.path2inode[targetPath]
if targetFound {
i.removePathFromInode2Path(targetInode, targetPath)
delete(i.path2inode, targetPath)
i.dropDirPath(targetInode)
}
// The rebuild eligibility travels with the directory: the target keeps it
// when the source earned it, and an overwritten target loses its own.
_, sourceWasCached := i.wasCachedDirs[sourcePath]
delete(i.wasCachedDirs, sourcePath)
delete(i.wasCachedDirs, targetPath)
if sourceWasCached {
i.markWasCached(targetPath)
}
delete(i.path2inode, sourcePath)
i.path2inode[targetPath] = sourceInode
if entry, entryFound := i.inode2path[sourceInode]; entryFound {
entry.replacePath(sourcePath, targetPath)
if d := i.dirStates[sourceInode]; d != nil {
i.dropDirPath(sourceInode)
d.path = targetPath
i.dirPaths[targetPath] = d
d.resetCacheState()
}
} else {
glog.Errorf("MovePath %s to %s: sourceInode %d not found", sourcePath, targetPath, sourceInode)
}
return
}
// Forget drops nlookup references. onRelease, if given, runs at the moment the
// inode is released and while the table is still locked: state keyed by the
// inode number has to be dropped there, because the number is derived from the
// path and a lookup arriving after the unlock would be handed the same one.
func (i *InodeToPath) Forget(inode, nlookup uint64, onRelease func(inode uint64), onForgetDir func(dir util.FullPath)) {
var dirPaths []util.FullPath
callOnForgetDir := false
i.Lock()
path, found := i.inode2path[inode]
if found {
if nlookup > path.nlookup {
glog.Errorf("kernel forget over-decrement: inode %d path %v current %d forget %d", inode, path.path, path.nlookup, nlookup)
path.nlookup = 0
} else {
path.nlookup -= nlookup
}
glog.V(4).Infof("kernel forget: inode %d path %v nlookup %d", inode, path.path, path.nlookup)
if path.nlookup == 0 {
if onRelease != nil {
onRelease(inode)
}
if _, isDir := i.dirStates[inode]; isDir && onForgetDir != nil {
dirPaths = path.appendPaths(nil)
callOnForgetDir = true
}
delete(i.path2inode, path.path)
if path.extraPaths != nil {
for _, p := range *path.extraPaths {
delete(i.path2inode, p)
}
}
delete(i.inode2path, inode)
i.dropDirPath(inode)
delete(i.dirStates, inode)
} else {
glog.V(4).Infof("kernel forget but nlookup not zero: inode %d path %v nlookup %d", inode, path.path, path.nlookup)
}
} else {
glog.Warningf("kernel forget but inode not found: inode %d", inode)
}
i.Unlock()
if callOnForgetDir {
for _, p := range dirPaths {
onForgetDir(p)
}
}
}