Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

FUSE Access Policy System

The FUSE composition layer includes a pluggable access policy system that controls how file operations behave during dependency updates. This allows developers to tune the trade-off between consistency and availability based on their workflow.

Overview

When the composition system is updating (rebuilding Nix derivations), file access to dependency cells may need to be controlled. The policy system determines whether to:

  • Allow the operation immediately
  • Block until the system becomes stable
  • Deny with an error (e.g., EAGAIN)
  • Allow with stale data and log a warning
┌─────────────────────────────────────────────────────────────┐
│                     FUSE Operation                          │
│   (lookup, getattr, read, readdir, write, create, ...)      │
└─────────────────────────────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────┐
│                   Classify Request                          │
│  path/inode → FileClass                                     │
│  state machine → SystemState                                │
│  operation → OperationType                                  │
└─────────────────────────────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────┐
│                   Policy.check()                            │
│  (FileClass, SystemState, OperationType) → PolicyDecision   │
└─────────────────────────────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────┐
│                   Execute Decision                          │
│  Allow → proceed                                            │
│  Block → wait then retry                                    │
│  Deny → return errno                                        │
│  AllowStale → proceed with warning                          │
└─────────────────────────────────────────────────────────────┘

Core Concepts

File Classes

Files in the composition view are classified by their behavioral characteristics:

ClassDescriptionExamples
SourcePassthroughRepository source filessrc/main.rs, docs/README.md
CellContentDependency cell contentexternal/godeps/vendor/...
VirtualGeneratedGenerated virtual files.buckconfig, .buckroot
VirtualDirectoryVirtual directory structureMount root, cell prefix
EditLayerUser modifications (future)Local patches to dependencies

Key insight: SourcePassthrough and virtual files are always accessible regardless of system state. Only CellContent access is subject to policy decisions.

System States

The composition system transitions through these states:

Settled ──manifest change──► Syncing ──nix build──► Building
   ▲                                                    │
   │                                               build done
   │                                                    │
   └───────────────────── Transitioning ◄───────────────┘
StateDescription
SettledSystem is stable, no pending changes
SyncingManifest changed, preparing for update
BuildingNix derivation is building
TransitioningAtomically switching to new view
ErrorSystem encountered an error

Operation Types

OperationDescription
LookupPath lookup (finding a file)
GetattrGet file/directory attributes
ReadRead file content
ReaddirRead directory entries
ReadlinkRead symbolic link target
Open / OpendirOpen file/directory
Write / Create / UnlinkWrite operations (future)

Built-in Policies

StrictPolicy

Best for: CI pipelines, production builds where correctness is critical

Blocks all cell access during any update phase. Reads will never return stale data, but may block for the duration of the Nix build.

#![allow(unused)]
fn main() {
StrictPolicy::new()                    // Default 5-minute timeout
StrictPolicy::with_timeout(Duration::from_secs(120))  // Custom timeout
}
StateCellContentSourcePassthrough
SettledAllowAllow
SyncingBlockAllow
BuildingBlockAllow
TransitioningBlockAllow

LenientPolicy

Best for: Interactive development where latency matters

Allows stale reads during syncing and building phases, only blocks during the brief transition phase.

#![allow(unused)]
fn main() {
LenientPolicy::new()
}
StateCellContentSourcePassthrough
SettledAllowAllow
SyncingAllowStaleAllow
BuildingAllowStaleAllow
TransitioningBlockAllow

CIPolicy

Best for: CI/CD environments where blocking is undesirable

Never blocks - immediately returns EAGAIN if the operation would need to wait. The caller can retry or handle the error.

#![allow(unused)]
fn main() {
CIPolicy::new()
}
StateCellContentSourcePassthrough
SettledAllowAllow
SyncingDeny (EAGAIN)Allow
BuildingDeny (EAGAIN)Allow
TransitioningDeny (EAGAIN)Allow

DevelopmentPolicy (Default)

Best for: Day-to-day development work

A balanced approach:

  • Syncing: Allow stale reads (quick phase)
  • Building: Block (wait for fresh data)
  • Error: Allow stale (degrade gracefully)
#![allow(unused)]
fn main() {
DevelopmentPolicy::new()
}
StateCellContentSourcePassthrough
SettledAllowAllow
SyncingAllowStaleAllow
BuildingBlockAllow
TransitioningBlockAllow
ErrorAllowStaleAllow

Creating Custom Policies

Implement the AccessPolicy trait to create custom behavior:

#![allow(unused)]
fn main() {
use composition::policy::{
    AccessPolicy, FileClass, SystemState, OperationType, PolicyDecision,
};
use std::time::Duration;

pub struct MyPolicy {
    block_timeout: Duration,
}

impl AccessPolicy for MyPolicy {
    fn check(
        &self,
        class: &FileClass,
        state: SystemState,
        op: OperationType,
    ) -> PolicyDecision {
        // Source files always accessible
        if class.is_always_accessible() {
            return PolicyDecision::Allow;
        }

        // Custom logic based on state and operation
        match (state, op) {
            // Allow reads during syncing
            (SystemState::Syncing, OperationType::Read) => {
                PolicyDecision::AllowStale
            }
            // Block lookups during building
            (SystemState::Building, OperationType::Lookup) => {
                PolicyDecision::Block {
                    timeout: self.block_timeout,
                }
            }
            // Fail fast for directory listing during updates
            (_, OperationType::Readdir) if state.is_updating() => {
                PolicyDecision::eagain()
            }
            // Default: allow
            _ => PolicyDecision::Allow,
        }
    }

    fn name(&self) -> &'static str {
        "my-policy"
    }

    fn description(&self) -> &'static str {
        "Custom policy with special handling for readdir"
    }
}
}

Policy Decision Types

DecisionBehaviorUse Case
AllowProceed immediatelyStable state, always-accessible files
Block { timeout }Wait up to timeout for stable stateEnsuring consistency during builds
Deny { errno }Return error immediatelyCI environments, fail-fast scenarios
AllowStaleProceed with warning logInteractive development, quick feedback

Convenience Constructors

#![allow(unused)]
fn main() {
PolicyDecision::block()                    // Block with 5-minute timeout
PolicyDecision::block_with_timeout(dur)    // Block with custom timeout
PolicyDecision::eagain()                   // Deny with EAGAIN (11)
PolicyDecision::ebusy()                    // Deny with EBUSY (16)
}

Configuring the Policy

In Rust Code

When creating a CompositionFs, use the with_policy constructor:

#![allow(unused)]
fn main() {
use composition::{CompositionConfig, CompositionFs};
use composition::policy::{CIPolicy, StrictPolicy};

// With CI policy
let fs = CompositionFs::with_policy(
    config,
    repo_root,
    state_machine,
    Box::new(CIPolicy::new()),
);

// With strict policy and custom timeout
let fs = CompositionFs::with_policy(
    config,
    repo_root,
    state_machine,
    Box::new(StrictPolicy::with_timeout(Duration::from_secs(60))),
);
}

Via Nix Configuration (Future)

turnkey.fuse = {
  enable = true;

  # Policy selection
  accessPolicy = "development";  # "strict" | "lenient" | "ci" | "development"

  # Custom timeout for blocking policies
  blockTimeout = 300;  # seconds
};

Environment Variables (Future)

# Override policy at runtime
TURNKEY_ACCESS_POLICY=ci tk build //...

# Custom timeout
TURNKEY_BLOCK_TIMEOUT=60 tk build //...

Debugging Policies

Policy decisions are logged at debug level. Enable debug logging to see:

DEBUG Policy 'development': blocking for up to 300s until stable
DEBUG Policy 'ci': denying Readdir on CellContent { cell: "godeps" } in state Building
WARN  Policy 'lenient': returning potentially stale data for cell 'godeps' during Building

Guidelines for Choosing a Policy

ScenarioRecommended Policy
CI/CD pipelinesCIPolicy - fail fast, let retry logic handle it
Production buildsStrictPolicy - correctness over speed
Interactive developmentDevelopmentPolicy - balanced default
Quick iterationLenientPolicy - maximum availability
Custom requirementsImplement AccessPolicy trait

API Reference

Module: composition::policy

Types:

  • FileClass - File classification enum
  • SystemState - System state enum
  • OperationType - Operation type enum
  • PolicyDecision - Decision enum
  • AccessPolicy - Policy trait
  • BoxedPolicy - Type alias for Box<dyn AccessPolicy>

Built-in Policies:

  • StrictPolicy
  • LenientPolicy
  • CIPolicy
  • DevelopmentPolicy

Functions:

  • default_policy() - Returns a boxed DevelopmentPolicy

Constants:

  • EAGAIN - Resource temporarily unavailable (11)
  • EBUSY - Device or resource busy (16)