Commit Graph
3 Commits
Author SHA1 Message Date
Chris LuandDevin 9e41500a3a mount: rebuild dropped directory listings and stale sections on lookup (#12195)
* mount: rebuild dropped directory listings on lookup

Invalidation, idle eviction, and a kernel Forget all clear a directory's
cached-listing flag, but only an expired listing was rebuilt on lookup — a
dropped one read through per entry until a ReadDir happened to run. Track
directories that once held a listing in wasCachedDirs, which survives those
drops, and let ShouldRebuildExpiredDir rebuild them on the next lookup.
Never-cached and read-through directories keep reading through.

* mount: re-validate a stale directory section on lookup

A burst of remote changes invalidates only the directory section it lands
in, but nothing re-validates that section until a ReadDir runs — so a
lookup-heavy workload reads through per entry indefinitely. EnsureNameFresh
re-lists just the section covering the looked-up name, after which lookups
in it are served from the cache again.

* mount: wait for pending flush before a stale-section refresh on lookup

A section refresh lists the range from the filer; without the wait a
pending async flush of the looked-up entry could land after the listing
and leave the refreshed section holding pre-flush metadata — the same
hazard the rebuild path already waits out.

* mount: bound the dropped-listing rebuild history

wasCachedDirs grew one full path per directory ever cached, removed only
by the read-through mark, so deleted and forgotten directories
accumulated forever. Remove the record when its name is deleted, move it
on rename so rebuild eligibility travels with the directory, and cap the
history with FIFO eviction — past the bound an evicted directory reads
through per entry, the same end state as a never-cached one.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: drop oversized stale sections and back off failed refreshes

A section that outgrew one refresh failed EnsureNameFresh with
ErrRefreshRangeTooLarge on every lookup, repeating the multi-page
listing before each per-entry read, and other refresh failures retried
with no cooldown. Drop the cached listing on ErrRefreshRangeTooLarge so
a later lookup rebuilds it re-tiled, as ReadDir does, and stamp the
rebuild attempt on other failures so retries back off under the same
cooldown the rebuild gate uses.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: version hard-link sibling syncs at the committed log position

* mount: tag dropped-listing queue slots so stale ones cannot evict

RemovePath, MovePath, and MarkDirectoryReadThrough delete the rebuild
record but leave the FIFO slot behind, so a path removed and re-cached
holds two queue entries. When the stale slot reaches the front it evicts
the newer record, and the directory reads through per lookup until the
next listing. Tag each record with an insertion generation and only
evict on a match, keeping the newer record for its own slot's turn.

* mount: refresh hard-link siblings from the filer, not the cached copy

Stamping a cached sibling with the mutation's version claimed that
position for content that may predate it — an in-flight event with
newer chunks or attributes then hits the version gate and the sibling
serves stale data until a refresh. Fetch each cached sibling's entry
from the filer after the commit and record the higher of the fetched
and committed positions, falling back to the in-place counter patch
only when the fetch itself fails.

* mount: back off failed section refreshes per section, not per directory

A failed refresh of one stale section marked a directory-wide attempt,
so every other stale section in that directory waited out the cooldown
with per-entry filer reads. Key the attempt timestamp by the section's
lower bound so unrelated ranges still refresh on their own schedule.

* mount: fence hard-linked entries by the group's newest write position

Every path of a hard-link group reads one shared content blob, but the
version gate only tracks per-path records — a sibling absent from
inodeToPath got no version from the sibling sync, so a stale section
refresh or event could roll the shared blob back for all of them.
Record a monotonic version floor per hard-link id wherever versioned
content lands, and check it in the section-refresh and event gates.

* mount: keep hard-link names landing when the group floor fences content

The per-group version floor rejected a delayed event's newEntry while
its oldPath still applied — a rename could drop the source without
adding the destination, and a section refresh could skip then mark a
live link fresh. Rebase the group's current cached content onto the
new path so the name lands without rolling the shared blob back, only
skip refresh entries already cached, and gate the sibling refresh
write under the same lock so a racing event or delete wins.

* mount: rebase hard-link entries from the shared blob when no sibling is cached

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: park group content on uncached hard-link names a stale refresh installs

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: stale the destination section when a fenced hard-link name cannot land

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: bound the sibling hard-link refresh under one request deadline

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: replace a reused-name occupant the hard-link floor would skip

* mount: claim no version for a sibling counter patched without a fetch

* mount: vacate a reused-name occupant without decrementing its group

A sibling write can fold the occupant's filer-side unlink into the shared
blob counter while the stale entry is still cached, so replacing it through
the plain insert may decrement a group that no longer counts the name and
drop a blob live siblings still read. Vacate the path like a rename first;
the counter then only ever drifts high, which leaks a blob rather than
losing shared content.

* mount: keep a fetch-failed sibling's version when patching its counter

The fallback marked the entry unversioned, which fences nothing — a
replayed delete older than the cached content could remove a live link.
Claim the version the entry already holds instead: newer in-flight events
still land and correct it, older replays stay fenced.

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-29 00:55:47 -07:00
Chris LuandDevin bd0bc06df7 mount: unwind metadata lookups on FUSE interrupt (#11717)
* mount: propagate request context through metadata lookups

The FUSE interrupt channel never reached the filer RPC: lookupEntry ran
its LookupEntry under context.Background(), so a stalled filer parked
ops like ListXAttr forever even after the kernel gave up on them.

Thread a context from each op's cancel channel through
maybeReadEntry, maybeLoadEntry, lookupEntry, ensureDirectoryVisited and
waitForPendingAsyncFlush; the existing withFilerClient attempt bound
turns it into both an interrupt and a deadline. A caller cancellation
now surfaces as EINTR instead of waiting out the RPC.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: reuse ctxFromCancel for the read path's interrupt bridge

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: regression tests for canceled and stalled filer lookups

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: thread ctx through AcquireHandle

Open, Create and Mknod all carry the request's cancel channel; let it
bound the pending-flush wait and metadata load inside AcquireHandle too.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: keep flush waits and write sequences uninterruptible

The cancel-derived context must only cover metadata reads: an
interrupted Unlink or Rename that skips the pending-flush wait can
race the async write back into existence, and Link/Rename mutations
that run under a canceled context abort mid-sequence, strand the
rollback, and leave wrong hard-link counts. Restore the blocking flush
wait and run each op's write phase on context.Background(). Rmdir and
Unlink also propagate EINTR from the parent lookup instead of
proceeding past a permission check the interrupt prevented.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: interrupt shared directory builds and directory reads

Waiters behind another request's singleflight directory build blocked
until it finished even when interrupted; switch the wait to DoChan so
the request context can abandon it. ReadDir paths now carry the
request context through EnsureListingFresh, cache listings and direct
reads, and map its cancellation to EINTR rather than EIO.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: assert exact statuses in filer-stall regression tests

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: let read-side flush waits release on interrupt

waitForPendingAsyncFlush reports whether the flush actually finished:
lookupEntry and AcquireHandle return EINTR when the request is
abandoned mid-wait, while Unlink and Rename keep waiting on
context.Background() so the delete/rename still sees the flushed
state.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: propagate interrupt through remaining lookup checks

Rename's source and destination permission lookups now return EINTR
on interrupt instead of skipping checkStickyBit or masking an
existing target, and the readdir preload visit maps cancellation to
EINTR like the other branches.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* mount: fail closed on errored rename prerequisite lookups

* mount: stop rmdir on interrupted target lookup, fail closed on dir lookup errors

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-12 09:42:52 +08:00
Junker der Provinz fa3bd5b5a7 mount: use the kernel-resolved node id in Link, not the persisted attribute (#10885)
* fix(mount): reply to LINK with the kernel node id, not the stored inode

Link() answered the kernel with out.NodeId = oldEntry.Attributes.Inode.
That attribute is a mount-runtime number and only entries created through a
mount carry one. An entry written by the S3 API, WebDAV or a direct filer
call persists inode 0, so the LINK reply named node id 0, which the kernel
rejects as invalid_nodeid and reports as EIO. The hard link itself had
already been written to the filer, which is why it looked correct again
after a mount restart.

The same stale number was also used as an inodeToPath key. AddPath(0, path)
filed the new link under inode 0, so a later Lookup on that name handed the
kernel node id 0 as well, and a LOOKUP reply carrying node id 0 means no
such entry.

in.Oldnodeid is the node id the kernel already holds for the source, and it
is the key inodeToPath is indexed by, so use it for the reply, for AddPath
and for the sibling sync.

Fixes #8404

* test(mount): cover the sibling sync in Link with a third hard link

The two existing cases never reach the body of syncHardLinkSiblings: with
two links the source alias and the name just created are both in skipPaths,
so the loop iterates over nothing and a change to that site goes unnoticed.
A third link leaves one name that no other part of Link() writes.

The new case drives three links off one source. It guards against covering
nothing (it fails if every path turns out to be a skipPath), checks that
every name of the file reports nlink 3, and then drives the sync with both
candidate keys to pin down which one it has to be: keyed by the source's
persisted Attributes.Inode, which is 0 for an entry written outside a mount,
GetAllPaths has no path to walk, while the kernel node id reaches the
sibling.

That second half is driven directly because Link() alone cannot tell the two
keys apart. The meta cache keeps one blob per hard link id (FilerStoreWrapper
setHardLink/maybeReadHardLink), so a read of any sibling returns the
attributes of the last write to any of them whether or not the sync ran.
2026-08-23 10:43:22 -07:00