mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-15 08:18:42 +01:00
* mount: record a hard link's looked-up names on its inode Lookup mapped a second name of a hard-linked file to the existing inode without adding it to the inode's names, so GetAllPaths missed it and the final forget left it mapped to a released inode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: keep an open file publishing when a peer unlinks one of its names A delete event from another client marked the inode's open handle deleted even when other names still held the hard-linked file, so every later fsync reported success without saving metadata. The delete event's entry carries the link count the filer decided on, so an alias delete now drops only that name, keeps the handle on a name this mount knows, and has the next flush read the survivor's stored entry before publishing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: test peer unlinks of a hard link name held open Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: leave an unlinked file out of the periodic metadata flush flushFileMetadata did not check the handle's delete mark. A file unlinked while open before its deferred create reached the filer was created again by the next periodic flush, which expects no entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: read a pending link change before a periodic metadata flush Only flushMetadataToFiler applied linkRefresh. After a peer unlinked one name, the periodic flush kept publishing over the old link count, so the filer refused every one until a regular flush ran. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: test the periodic flush after a peer unlinks a hard link name Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
732 lines
20 KiB
Go
732 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 ie, known := i.inode2path[inode]; known {
|
|
// Another name of a hard link the table already holds.
|
|
if !found {
|
|
ie.addPath(path)
|
|
}
|
|
if isLookup {
|
|
ie.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)
|
|
}
|
|
}
|
|
}
|