or run

tessl search
Log in

Version

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
pypipkg:pypi/pyfuse3@3.4.x

docs

index.md
tile.json

tessl/pypi-pyfuse3

tessl install tessl/pypi-pyfuse3@3.4.0

Python 3 bindings for libfuse 3 with async I/O support

lookup-counts.mddocs/guides/

Lookup Count Management Guide

Understanding and correctly implementing lookup count tracking is critical for filesystem stability.

What Are Lookup Counts?

The kernel maintains lookup counts to track how many references it has to each inode. This prevents the filesystem from deleting inodes that the kernel still knows about.

Key Concepts

  1. Increasing counts: Operations that return EntryAttributes (except getattr/setattr) increase the count by 1
  2. Decreasing counts: Kernel calls forget() to decrease counts
  3. Zero count: When count reaches zero, kernel no longer knows about the inode
  4. Safe deletion: Only delete inode when lookup count is 0 AND nlink is 0

Operations That Increase Lookup Counts

OperationIncrementNotes
lookup()+1Per successful lookup
mknod()+1New file node
mkdir()+1New directory
symlink()+1New symlink
link()+1New hard link
create()+1Create and open
readdir()+1 per entryVia readdir_reply() (except . and ..)

Implementation Pattern

Basic Tracking

class MyFS(pyfuse3.Operations):
    def __init__(self):
        super().__init__()
        self.lookup_counts = {}  # inode -> count
        self.inodes = {}  # inode -> data
    
    async def lookup(self, parent_inode, name, ctx):
        """Lookup with count tracking."""
        inode = self._find_entry(parent_inode, name)
        
        # CRITICAL: Increase lookup count
        self.lookup_counts[inode] = self.lookup_counts.get(inode, 0) + 1
        
        return await self.getattr(inode, ctx)
    
    async def forget(self, inode_list):
        """Decrease lookup counts - MUST NOT raise exceptions."""
        for inode, nlookup in inode_list:
            try:
                if inode in self.lookup_counts:
                    self.lookup_counts[inode] -= nlookup
                    
                    # When count reaches zero
                    if self.lookup_counts[inode] <= 0:
                        del self.lookup_counts[inode]
                        
                        # Only delete if also unlinked
                        if inode in self.inodes and self.inodes[inode]['nlink'] == 0:
                            self._delete_inode(inode)
            except Exception as e:
                logger.error(f"Error in forget: {e}", exc_info=True)

Complete Example

import pyfuse3
import errno
import stat
import time

class LookupCountFS(pyfuse3.Operations):
    def __init__(self):
        super().__init__()
        self.next_inode = pyfuse3.ROOT_INODE + 1
        self.inodes = {}
        self.lookup_counts = {}
    
    def init(self):
        """Initialize with root inode."""
        self.inodes[pyfuse3.ROOT_INODE] = {
            'mode': stat.S_IFDIR | 0o755,
            'nlink': 2,
            'entries': {},
        }
        self.lookup_counts[pyfuse3.ROOT_INODE] = 1
    
    async def create(self, parent_inode, name, mode, flags, ctx):
        """Create file with lookup count tracking."""
        # Allocate inode
        inode = self.next_inode
        self.next_inode += 1
        
        # Create inode data
        self.inodes[inode] = {
            'mode': stat.S_IFREG | (mode & ~ctx.umask),
            'nlink': 1,
            'data': b'',
        }
        
        # Add to directory
        self.inodes[parent_inode]['entries'][name] = inode
        
        # CRITICAL: Set initial lookup count to 1
        self.lookup_counts[inode] = 1
        
        fi = pyfuse3.FileInfo(fh=inode)
        entry = await self.getattr(inode, ctx)
        return (fi, entry)
    
    async def unlink(self, parent_inode, name, ctx):
        """Remove file - don't delete inode yet."""
        inode = self.inodes[parent_inode]['entries'][name]
        
        # Remove from directory
        del self.inodes[parent_inode]['entries'][name]
        
        # Decrement link count
        self.inodes[inode]['nlink'] -= 1
        
        # DON'T delete inode here - kernel may still reference it!
        # It will be deleted in forget() when lookup count reaches 0
    
    async def forget(self, inode_list):
        """Decrease lookup counts."""
        for inode, nlookup in inode_list:
            try:
                if inode in self.lookup_counts:
                    self.lookup_counts[inode] -= nlookup
                    
                    if self.lookup_counts[inode] <= 0:
                        del self.lookup_counts[inode]
                        
                        # Safe to delete when both conditions met:
                        # 1. lookup_count == 0 (kernel doesn't know about it)
                        # 2. nlink == 0 (no directory entries reference it)
                        if inode in self.inodes and self.inodes[inode]['nlink'] == 0:
                            del self.inodes[inode]
            except Exception as e:
                logger.error(f"Error in forget: {e}", exc_info=True)

Common Mistakes

Mistake 1: Not Tracking Counts

# WRONG - No lookup count tracking
async def lookup(self, parent_inode, name, ctx):
    inode = self._find_entry(parent_inode, name)
    return await self.getattr(inode, ctx)  # Missing count increment!

# CORRECT
async def lookup(self, parent_inode, name, ctx):
    inode = self._find_entry(parent_inode, name)
    self.lookup_counts[inode] = self.lookup_counts.get(inode, 0) + 1  # Track it!
    return await self.getattr(inode, ctx)

Mistake 2: Deleting Too Early

# WRONG - Deletes inode immediately
async def unlink(self, parent_inode, name, ctx):
    inode = self.inodes[parent_inode]['entries'][name]
    del self.inodes[parent_inode]['entries'][name]
    del self.inodes[inode]  # WRONG - kernel may still reference it!

# CORRECT - Only decrement nlink
async def unlink(self, parent_inode, name, ctx):
    inode = self.inodes[parent_inode]['entries'][name]
    del self.inodes[parent_inode]['entries'][name]
    self.inodes[inode]['nlink'] -= 1  # Decrement, don't delete
    # Deletion happens in forget() when safe

Mistake 3: Forgetting readdir Counts

# WRONG - Doesn't track readdir counts
async def readdir(self, fh, start_id, token):
    for name, inode in self.inodes[fh]['entries'].items():
        attr = await self.getattr(inode, None)
        pyfuse3.readdir_reply(token, name, attr, next_id)
        # Missing: readdir_reply increases lookup count!

# CORRECT - Counts tracked automatically by readdir_reply
async def readdir(self, fh, start_id, token):
    for name, inode in self.inodes[fh]['entries'].items():
        attr = await self.getattr(inode, None)
        if not pyfuse3.readdir_reply(token, name, attr, next_id):
            break  # readdir_reply handles count tracking

Edge Cases

Open File During Unlink

async def unlink(self, parent_inode, name, ctx):
    """
    Unlink may be called while file is open.
    Don't delete inode - it's still in use.
    """
    inode = self.inodes[parent_inode]['entries'][name]
    
    # Remove directory entry
    del self.inodes[parent_inode]['entries'][name]
    
    # Decrement nlink
    self.inodes[inode]['nlink'] -= 1
    
    # File may still be open (fh exists)
    # AND kernel may still have references (lookup_count > 0)
    # Don't delete - will be cleaned up in forget() and release()

Rename Operations

async def rename(self, parent_old, name_old, parent_new, name_new, flags, ctx):
    """
    Rename doesn't change lookup counts.
    Kernel handles count management.
    """
    inode = self.inodes[parent_old]['entries'][name_old]
    
    # If replacing existing entry
    if name_new in self.inodes[parent_new]['entries']:
        replaced_inode = self.inodes[parent_new]['entries'][name_new]
        self.inodes[replaced_inode]['nlink'] -= 1
        # Don't delete - kernel will call forget()
    
    # Move entry
    del self.inodes[parent_old]['entries'][name_old]
    self.inodes[parent_new]['entries'][name_new] = inode
    
    # No lookup count changes needed

Debugging Lookup Counts

Add Logging

async def lookup(self, parent_inode, name, ctx):
    """Lookup with debug logging."""
    inode = self._find_entry(parent_inode, name)
    
    old_count = self.lookup_counts.get(inode, 0)
    self.lookup_counts[inode] = old_count + 1
    
    logger.debug(f"lookup: inode={inode}, count {old_count} -> {old_count + 1}")
    
    return await self.getattr(inode, ctx)

async def forget(self, inode_list):
    """Forget with debug logging."""
    for inode, nlookup in inode_list:
        old_count = self.lookup_counts.get(inode, 0)
        new_count = old_count - nlookup
        
        logger.debug(f"forget: inode={inode}, count {old_count} -> {new_count}")
        
        # ... rest of forget logic ...

Track Statistics

class MonitoredFS(pyfuse3.Operations):
    def __init__(self):
        super().__init__()
        self.lookup_counts = {}
        self.stats = {
            'lookup_calls': 0,
            'forget_calls': 0,
            'max_lookup_count': 0,
        }
    
    async def lookup(self, parent_inode, name, ctx):
        self.stats['lookup_calls'] += 1
        inode = self._find_entry(parent_inode, name)
        self.lookup_counts[inode] = self.lookup_counts.get(inode, 0) + 1
        self.stats['max_lookup_count'] = max(
            self.stats['max_lookup_count'],
            self.lookup_counts[inode]
        )
        return await self.getattr(inode, ctx)
    
    async def forget(self, inode_list):
        self.stats['forget_calls'] += 1
        # ... forget logic ...

Best Practices

  1. Always track in lookup(): Never forget to increment
  2. Never delete in unlink(): Only decrement nlink
  3. Check both conditions: Delete only when lookup_count == 0 AND nlink == 0
  4. Never raise in forget(): Log errors, don't raise
  5. Initialize root inode: Set lookup_counts[ROOT_INODE] = 1 in init()
  6. Don't track . and ..: readdir_reply() handles these specially
  7. Test with concurrent operations: Verify counts under load
  8. Add debug logging: Track count changes during development
  9. Monitor for leaks: Check for inodes with non-zero counts at unmount
  10. Document assumptions: Note any special count handling

See Also

  • Quick Start Guide - Basic lookup count example
  • Operations Reference - Detailed operation documentation
  • Edge Cases - Complex lookup count scenarios