Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions internal/fs/inode/base_dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ func NewBaseDirInode(
metricHandle: metricHandle,
isEnableTypeCacheDeprecation: isEnableTypeCacheDeprecation,
}
typed.lc.Init(id)
typed.mu = locker.NewRW("BaseDirInode"+name.GcsObjectName(), func() {})

d = typed
Expand Down Expand Up @@ -134,18 +133,18 @@ func (d *baseDirInode) Name() Name {

// LOCKS_REQUIRED(d)
func (d *baseDirInode) IncrementLookupCount() {
d.lc.Inc()
d.lc.Inc(d.id)
}

// LOCKS_REQUIRED(d)
func (d *baseDirInode) DecrementLookupCount(n uint64) (destroy bool) {
destroy = d.lc.Dec(n)
destroy = d.lc.Dec(d.id, n)
return
}

// LOCKS_REQUIRED(d)
func (d *baseDirInode) Destroy() (err error) {
// Nothing interesting to do.
d.lc.Destroy()
return
}

Expand Down
7 changes: 3 additions & 4 deletions internal/fs/inode/dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,8 +370,6 @@ func NewDirInode(
typed.cache = cache
}

typed.lc.Init(id)

// Set up invariant checking.
typed.mu = locker.NewRW(name.GcsObjectName(), typed.checkInvariants)

Expand Down Expand Up @@ -594,12 +592,12 @@ func (d *dirInode) Name() Name {

// LOCKS_REQUIRED(d)
func (d *dirInode) IncrementLookupCount() {
d.lc.Inc()
d.lc.Inc(d.id)
}

// LOCKS_REQUIRED(d)
func (d *dirInode) DecrementLookupCount(n uint64) (destroy bool) {
destroy = d.lc.Dec(n)
destroy = d.lc.Dec(d.id, n)
return
}

Expand All @@ -608,6 +606,7 @@ func (d *dirInode) Destroy() (err error) {
// When destroying the inode, we cancel its subdirectory prefetches.
// This cleans up any curr dir + child dir prefetchers.
d.CancelSubdirectoryPrefetches()
d.lc.Destroy()
return
}

Expand Down
6 changes: 3 additions & 3 deletions internal/fs/inode/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,6 @@ func NewFileInode(
f.kernelRangeReaderInstance = kernel_readers.NewKernelRangeReaderInstance(&minObj)
}

f.lc.Init(id)
f.mu = syncutil.NewInvariantMutex(f.checkInvariants)

return
Expand Down Expand Up @@ -527,12 +526,12 @@ func (f *FileInode) SourceGeneration() (g Generation) {

// LOCKS_REQUIRED(f.mu)
func (f *FileInode) IncrementLookupCount() {
f.lc.Inc()
f.lc.Inc(f.id)
}

// LOCKS_REQUIRED(f.mu)
func (f *FileInode) DecrementLookupCount(n uint64) (destroy bool) {
destroy = f.lc.Dec(n)
destroy = f.lc.Dec(f.id, n)
return
}

Expand Down Expand Up @@ -578,6 +577,7 @@ func (f *FileInode) UpdateSize(size uint64) {
// LOCKS_REQUIRED(f.mu)
func (f *FileInode) Destroy() (err error) {
f.destroyed = true
f.lc.Destroy()
if f.localFileCache {
cacheObjectKey := &contentcache.CacheObjectKey{BucketName: f.bucket.Name(), ObjectName: f.name.objectName}
f.contentCache.Remove(cacheObjectKey)
Expand Down
40 changes: 18 additions & 22 deletions internal/fs/inode/lookup_count.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,44 +20,40 @@ import (
"github.com/jacobsa/fuse/fuseops"
)

// A helper struct for implementing lookup counts. The only value added is some
// A helper type for implementing lookup counts. The only value added is some
// paranoid panics. External synchronization is required.
//
// May be embedded within a larger struct. Use Init to initialize.
type lookupCount struct {
id fuseops.InodeID
count uint64
destroyed bool
}

func (lc *lookupCount) Init(id fuseops.InodeID) {
lc.id = id
}
// May be embedded within a larger struct.
type lookupCount int64

func (lc *lookupCount) Inc() {
if lc.destroyed {
panic(fmt.Sprintf("Inode %v has already been destroyed", lc.id))
func (lc *lookupCount) Inc(id fuseops.InodeID) {
if *lc == -1 {
panic(fmt.Sprintf("Inode %v has already been destroyed", id))
}

lc.count++
(*lc)++
}

func (lc *lookupCount) Dec(n uint64) (destroy bool) {
if lc.destroyed {
panic(fmt.Sprintf("Inode %v has already been destroyed", lc.id))
func (lc *lookupCount) Dec(id fuseops.InodeID, n uint64) (destroy bool) {
if *lc == -1 {
panic(fmt.Sprintf("Inode %v has already been destroyed", id))
}

// Make sure n is in range.
if n > lc.count {
if n > uint64(*lc) {
panic(fmt.Sprintf(
"n is greater than lookup count: %v vs. %v",
n,
lc.count))
*lc))
}

// Decrement.
lc.count -= n
*lc -= lookupCount(n)

destroy = lc.count == 0
destroy = *lc == 0
return
}

func (lc *lookupCount) Destroy() {
*lc = -1
}
99 changes: 99 additions & 0 deletions internal/fs/inode/lookup_count_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package inode

import (
"math"
"testing"

"github.com/jacobsa/fuse/fuseops"
"github.com/stretchr/testify/assert"
)

func TestLookupCount_Inc_Normal(t *testing.T) {
var lc lookupCount
id := fuseops.InodeID(1)

lc.Inc(id)

assert.Equal(t, lookupCount(1), lc)
}

func TestLookupCount_Inc_PanicsWhenDestroyed(t *testing.T) {
var lc lookupCount = -1
id := fuseops.InodeID(1)

assert.Panics(t, func() {
lc.Inc(id)
})
}

func TestLookupCount_Dec_Normal(t *testing.T) {
var lc lookupCount = 2
id := fuseops.InodeID(1)

destroy := lc.Dec(id, 1)

assert.False(t, destroy)
assert.Equal(t, lookupCount(1), lc)
}

func TestLookupCount_Dec_ReachesZero(t *testing.T) {
var lc lookupCount = 1
id := fuseops.InodeID(1)

destroy := lc.Dec(id, 1)

assert.True(t, destroy)
assert.Equal(t, lookupCount(0), lc)
}

func TestLookupCount_Dec_PanicsWhenDestroyed(t *testing.T) {
var lc lookupCount = -1
id := fuseops.InodeID(1)

assert.Panics(t, func() {
lc.Dec(id, 1)
})
}

func TestLookupCount_Dec_PanicsOnUnderflow(t *testing.T) {
var lc lookupCount = 1
id := fuseops.InodeID(1)

assert.Panics(t, func() {
lc.Dec(id, 2)
})
}

func TestLookupCount_Dec_ProtectsAgainstOverflowWrap(t *testing.T) {
var lc lookupCount = 1
id := fuseops.InodeID(1)

// Providing math.MaxUint64 would wrap to -1 if we casted it directly to int64.
// This ensures our uint64 comparison works safely.
assert.Panics(t, func() {
lc.Dec(id, math.MaxUint64)
})
}

func TestLookupCount_Destroy_IsIdempotent(t *testing.T) {
var lc lookupCount = 5

lc.Destroy()
lc.Destroy() // Second call should not panic

assert.Equal(t, lookupCount(-1), lc)
}
9 changes: 3 additions & 6 deletions internal/fs/inode/symlink.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,6 @@ func NewSymlinkInode(
metadata: m.Metadata,
}

// Set up lookup counting.
s.lc.Init(id)

s.target, err = s.resolveSymlinkTarget(ctx)
if err != nil {
return nil, err
Expand Down Expand Up @@ -215,18 +212,18 @@ func (s *SymlinkInode) UpdateSize(size uint64) {

// LOCKS_REQUIRED(s.mu)
func (s *SymlinkInode) IncrementLookupCount() {
s.lc.Inc()
s.lc.Inc(s.id)
}

// LOCKS_REQUIRED(s.mu)
func (s *SymlinkInode) DecrementLookupCount(n uint64) (destroy bool) {
destroy = s.lc.Dec(n)
destroy = s.lc.Dec(s.id, n)
return
}

// LOCKS_REQUIRED(s.mu)
func (s *SymlinkInode) Destroy() (err error) {
// Nothing to do.
s.lc.Destroy()
return
}

Expand Down
Loading