Skip to content

Commit b6eba91

Browse files
authored
fix: Resolve deadlock in directory operations using generation-based invalidation (#39)
* fix: Resolve deadlock in directory operations using generation-based invalidation This fixes issue #37 where tigrisfs would deadlock during concurrent directory operations, particularly when listing directories. The deadlock occurred because: 1. listObjectsFlat() held dh.mu while calling sealDir() 2. sealDir() -> removeExpired() -> removeChildUnlocked() tried to lock ALL directory handles' mutexes 3. Other threads could hold different directory handle mutexes while waiting for dh.mu, creating a circular lock dependency The solution uses a generation counter approach: - Each directory maintains an atomic generation counter - The counter increments on structural changes (add/remove children) - Directory handles check the generation before operations - If generation changed, handles reset their position without locking * fix: Address race condition in generation handling after sealDir * fix: Initialize DirHandle generation correctly to prevent spurious invalidations
1 parent 4e48555 commit b6eba91

File tree

3 files changed

+44
-17
lines changed

3 files changed

+44
-17
lines changed

core/cluster_fs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ func (fs *ClusterFs) readDir(handleId fuseops.HandleID, offset fuseops.DirOffset
430430
dh.lastExternalOffset = 0
431431
dh.lastInternalOffset = 0
432432
dh.lastName = ""
433+
dh.generation = atomic.LoadUint64(&dh.inode.dir.generation)
433434
}
434435

435436
for {

core/cluster_fs_fuse.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,10 @@ func (fs *ClusterFsFuse) OpenDir(ctx context.Context, op *fuseops.OpenDirOp) (er
550550

551551
// 2nd phase
552552
fs.Goofys.mu.Lock()
553-
dh := &DirHandle{inode: inode}
553+
dh := &DirHandle{
554+
inode: inode,
555+
generation: atomic.LoadUint64(&inode.dir.generation),
556+
}
554557
fs.Goofys.dirHandles[fuseops.HandleID(resp.HandleId)] = dh
555558
fs.Goofys.mu.Unlock()
556559

core/dir.go

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ type DirInodeData struct {
6060
DeletedChildren map[string]*Inode
6161
Gaps []*SlurpGap
6262
handles []*DirHandle
63+
generation uint64 // incremented on structural changes
6364
}
6465

6566
// Returns the position of first char < '/' in `inp` after prefixLen + any continued '/' characters.
@@ -87,11 +88,15 @@ type DirHandle struct {
8788
// or from the previous offset
8889
lastExternalOffset fuseops.DirOffset
8990
lastInternalOffset int
91+
generation uint64 // tracks directory structure changes
9092
lastName string
9193
}
9294

9395
func NewDirHandle(inode *Inode) (dh *DirHandle) {
94-
dh = &DirHandle{inode: inode}
96+
dh = &DirHandle{
97+
inode: inode,
98+
generation: atomic.LoadUint64(&inode.dir.generation),
99+
}
95100
return
96101
}
97102

@@ -385,7 +390,9 @@ func (dir *DirInodeData) checkGapLoaded(key string, newerThan time.Time) bool {
385390
return false
386391
}
387392

393+
// sealDir completes directory listing and cleans up expired entries
388394
// LOCKS_REQUIRED(inode.mu)
395+
// LOCKS_EXCLUDED(dh.mu for all directory handles)
389396
func (inode *Inode) sealDir() {
390397
inode.dir.listMarker = ""
391398
inode.dir.listDone = true
@@ -397,6 +404,10 @@ func (inode *Inode) sealDir() {
397404
} else {
398405
inode.Attributes.Mtime, inode.Attributes.Ctime = inode.findChildMaxTime()
399406
}
407+
408+
// Increment generation to signal all handles need revalidation
409+
atomic.AddUint64(&inode.dir.generation, 1)
410+
400411
inode.removeExpired("")
401412
}
402413

@@ -622,7 +633,14 @@ func (dh *DirHandle) listObjectsFlat() (start string, err error) {
622633
dh.inode.dir.listMarker = lastName
623634
}
624635
} else {
636+
// We must release dh.mu before calling sealDir to avoid deadlock
637+
dh.mu.Unlock()
625638
dh.inode.sealDir()
639+
// Reload generation immediately after sealDir completes to get accurate state
640+
currentGen := atomic.LoadUint64(&dh.inode.dir.generation)
641+
dh.mu.Lock()
642+
// Update our generation to match the new state
643+
dh.generation = currentGen
626644
}
627645

628646
dh.inode.mu.Unlock()
@@ -633,6 +651,16 @@ func (dh *DirHandle) listObjectsFlat() (start string, err error) {
633651
// LOCKS_REQUIRED(dh.mu)
634652
// LOCKS_REQUIRED(dh.inode.mu)
635653
func (dh *DirHandle) checkDirPosition() {
654+
// Check if directory structure changed since we last checked
655+
// Note: There's a benign race here where generation could change between
656+
// the load and assignment. This is acceptable as we'll catch it on the
657+
// next operation. The worst case is an unnecessary position reset.
658+
currentGen := atomic.LoadUint64(&dh.inode.dir.generation)
659+
if dh.generation != currentGen {
660+
dh.lastInternalOffset = -1
661+
dh.generation = currentGen
662+
}
663+
636664
if dh.lastInternalOffset < 0 {
637665
parent := dh.inode
638666
// Directory position invalidated, try to find it again using lastName
@@ -752,6 +780,7 @@ func (dh *DirHandle) Seek(newOffset fuseops.DirOffset) {
752780
dh.lastExternalOffset = 0
753781
dh.lastInternalOffset = 0
754782
dh.lastName = ""
783+
dh.generation = atomic.LoadUint64(&dh.inode.dir.generation)
755784
}
756785
}
757786

@@ -996,6 +1025,10 @@ func (parent *Inode) removeChildUnlocked(inode *Inode) {
9961025
if l == 0 {
9971026
return
9981027
}
1028+
1029+
// Increment generation to invalidate all directory handles
1030+
atomic.AddUint64(&parent.dir.generation, 1)
1031+
9991032
i := sort.Search(l, parent.findInodeFunc(inode.Name))
10001033
if i >= l || parent.dir.Children[i].Name != inode.Name {
10011034
panic(fmt.Sprintf("%v.removeName(%v) but child not found: %v",
@@ -1004,11 +1037,7 @@ func (parent *Inode) removeChildUnlocked(inode *Inode) {
10041037

10051038
// POSIX allows parallel readdir() and modifications,
10061039
// so preserve position of all directory handles
1007-
for _, dh := range parent.dir.handles {
1008-
dh.mu.Lock()
1009-
dh.lastInternalOffset = -1
1010-
dh.mu.Unlock()
1011-
}
1040+
// Handles will detect the generation change and reset themselves
10121041
// >= because we use the "last open dir" as the "next" one
10131042
if parent.dir.lastOpenDirIdx >= i {
10141043
parent.dir.lastOpenDirIdx--
@@ -1038,11 +1067,8 @@ func (parent *Inode) removeAllChildrenUnlocked() {
10381067
child.DeRef(1)
10391068
child.mu.Unlock()
10401069
}
1041-
// POSIX allows parallel readdir() and modifications,
1042-
// so reset position of all directory handles
1043-
for _, dh := range parent.dir.handles {
1044-
dh.lastInternalOffset = -1
1045-
}
1070+
// Increment generation to invalidate all directory handles
1071+
atomic.AddUint64(&parent.dir.generation, 1)
10461072
parent.dir.Children = nil
10471073
}
10481074

@@ -1091,11 +1117,8 @@ func (parent *Inode) insertChildUnlocked(inode *Inode) {
10911117
panic(fmt.Sprintf("double insert of %v", parent.getChildName(inode.Name)))
10921118
}
10931119

1094-
// POSIX allows parallel readdir() and modifications,
1095-
// so preserve position of all directory handles
1096-
for _, dh := range parent.dir.handles {
1097-
dh.lastInternalOffset = -1
1098-
}
1120+
// Increment generation to invalidate all directory handles
1121+
atomic.AddUint64(&parent.dir.generation, 1)
10991122
if parent.dir.lastOpenDirIdx >= i {
11001123
parent.dir.lastOpenDirIdx++
11011124
}

0 commit comments

Comments
 (0)