Skip to content

fix(fs): defensive cancellation guards in FUSE synth path#40

Merged
mfreed merged 2 commits into
mainfrom
fix/cancellation-followups
May 24, 2026
Merged

fix(fs): defensive cancellation guards in FUSE synth path#40
mfreed merged 2 commits into
mainfrom
fix/cancellation-followups

Conversation

@mfreed
Copy link
Copy Markdown
Member

@mfreed mfreed commented May 24, 2026

  • Skip setNegative on transient cancellation (1178aa4): new isCancellationError(ctx, fsErr) helper in
    fs/errors.go; guards the only statCache.setNegative call site in synth_ops.go. Backstop for the
    FUSE_INTERRUPT bug class even after the three prior decoupling commits (4c7b4c1, b5cf146, df7616c).

  • Propagate ResolvePath errors (6cd9e0b): resolveSynthPath signature changed from (string, bool) to
    (string, bool, error). DB errors now surface as ErrIO with Cause preserved instead of being swallowed into
    ErrNotExist. 7 callers in synth_ops.go, 1 in history.go, 12 test callers updated.

  • Validated by 22,000 clean stress iterations across four configurations (mix of docker-FUSE and native NFS,
    large+many files).

mfreed added 2 commits May 22, 2026 18:04
When statSynthFile sees an ErrNotExist from the underlying lookup, it
caches a negative entry in statCache for the full 2s TTL. If the
ErrNotExist actually came from a cancelled DB query (rather than a
genuine "row does not exist"), this poisons the cache and blocks
subsequent reads with false ENOENT.

The three FUSE_INTERRUPT decoupling commits (4c7b4c1, b5cf146, df7616c)
prevent kernel-side cancellation from reaching here on any FUSE entry
path today. This commit is defense in depth: detect cancellation at the
moment of the cache write and skip it, so the bug class is self-
correcting if a future code path forgets to wrap (e.g. a new internal
Operations caller, the NFS adapter's 30s timeout firing mid-query).

Adds isCancellationError(ctx, fsErr) in errors.go. Inspects both
fsErr.Cause (errors.Is against context.Canceled/DeadlineExceeded) AND
ctx.Err() -- the latter catches the case where an intermediate layer
(e.g. resolveSynthPath today) discarded the underlying cause before
wrapping as ErrNotExist.

Adds 7-sub-test table in errors_test.go covering nil/non-nil errors,
direct and wrapped causes, both cancellation flavors, and the ctx-only
fallback path.
Previously, resolveSynthPath swallowed any error from db.ResolvePath
and returned ("", false), making a real DB failure indistinguishable
from a genuine "path doesn't exist". Callers wrapped the false return
as ErrNotExist, dropping the underlying cause -- which meant context
cancellation, connection failures, and other transient errors
surfaced as ENOENT and (until the defensive setNegative guard in
1178aa4) poisoned the cache for the full statCacheTTL.

Changes resolveSynthPath's signature from (string, bool) to
(string, bool, error) and propagates the wrapped error. Each caller
now distinguishes:
* (id, true, nil)   -- path resolved
* ("", false, nil)  -- path genuinely doesn't exist (clean ENOENT)
* ("", false, err)  -- DB call failed (returned as ErrIO with Cause)

Updated 8 production call sites (synth_ops.go x7, history.go x1) and
12 unit-test call sites. Fixed TestSynth_ResolveSynthRow_DBError --
it had been asserting the old swallowing behavior (ErrNotExist on
"connection refused"); now correctly asserts ErrIO with the original
error preserved in Cause.
@mfreed mfreed changed the title fix(fs): defensive cancellation guards in synth path fix(fs): defensive cancellation guards in FUSE synth path May 24, 2026
@mfreed mfreed merged commit c485d9d into main May 24, 2026
2 checks passed
@mfreed mfreed deleted the fix/cancellation-followups branch May 24, 2026 23:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant