Commit Graph
30 Commits
Author SHA1 Message Date
Chris LuandGitHub 65114575eb mount: invalidate hot directory listings by section (#10712)
* mount: invalidate hot directory listings by section

A cached directory used to be dropped whole when it saw 64 changes in
2s: with a continuous writer the listing cycled through wipe, direct
listing and full rebuild for as long as the writer kept going, and
every sibling lookup fell through to the filer in between.

Split each cached listing into name-range sections of 1024 entries. A
burst of foreign changes invalidates just the section it lands in;
entries stay served and events keep applying, and the next readdir
re-lists only that range from the filer, reconciled through the version
gate so it cannot roll back newer applied events. Lookups in an
invalidated section read through until then. The mount's own writes no
longer invalidate anything: they are ground truth for its cache.

* meta_cache: drop the version floor with a deleted or moved directory

The other teardown paths already clear both maps; a floor left behind
here would fence the listing of a directory re-created at the same
path.

* mount: harden section refresh

An unversioned listing (pre-upgrade filer) now only fills gaps instead
of reconciling: without a snapshot to order against, an overwrite or
the deletion sweep could roll back an event applied after the listing.

The section table can be rebuilt or re-split between the listing and
its apply, so the refresh only marks fresh or splits when the section
still covers the range it read. Splicing bounds from a stale range
into a rebuilt table could leave them unsorted.

Bound the wait: a readdir gives a refresh five seconds before serving
the maintained-but-unverified cache. Bound the size: a range grown
past four sections aborts the refresh and drops the directory cache,
re-tiling it with a full rebuild, with that request served direct.

Cover the filer-facing path with a listing server: paging with the
snapshot pinned across pages, the section cutoff, no calls for a
fresh section, and the overgrown-range abort.

* meta_cache: make the section table a self-contained state machine

Churn counting, freshness, stale-range scanning and the refresh
completion with its guard and split now live on dirSections itself,
free of the lock, the store and the apply loop, so they test directly
with synthetic clocks and tables. MetaCache keeps thin wrappers that
hold its mutex and find the directory's table.

* meta_cache: keep section internals out of the apply request

The request now carries the completed build's table and one refresh as
opaque values built by section code, and the boundary-derivation rule
moves out of the build loop into a collector next to the rest of the
section logic.

* mount: fence refreshed sections with a snapshot floor

A refresh versioned the entries it fetched and tombstoned the ones it
swept, but a name absent from both cache and listing kept the old
directory floor, so a delayed event between the two snapshots could
resurrect it into a section already marked fresh. The section now
carries its own floor, consulted next to the directory floor, covering
every name in the range, present or absent — which also retires the
refresh's per-entry version stamps and sweep tombstones.

An unversioned listing sets no floor and vouches for nothing: it may
still fill gaps, but the section stays stale and reads through until a
filer that stamps snapshots re-validates it.

A listing's reach is unknowable up front — a resumed handle can skip
far ahead, and shrunken sections let one batch span many — so a
readdir now re-validates every stale section from its start name to
the end of the directory instead of the next two.

* mount: fence tombstoned names with floors and gate the reconcile

A tombstone answered for its name before the floors were consulted, so
one at an old position let through events the newer listing floor
should have fenced; a build never hit this because it prunes
superseded tombstones, which a section refresh does not. The version
gate now raises a tombstone to the floors like any other record.

With no per-entry versions, only the section floor fences a
reconcile's work, so a range the rebuilt or re-split table no longer
has must not touch the store either: the range check moves ahead of
the mutations, under the same lock the floor install holds.

An unversioned refresh no longer retries: the section is remembered as
unverifiable and skipped by the stale scan, or every batch of every
readdir would re-list the same ranges against a filer that cannot
vouch for them.

* mount: clear beaten unversioned markers and skip refresh mid-build

An unversioned marker outliving the snapshot write that replaced its
content bypassed the section floor the same way an old tombstone did,
letting a delayed pre-snapshot event roll the entry back. The refresh
now clears the marker when its write wins; pinned local-only entries
are not replaced at all, keeping their content and marker.

A rebuild wipes and repopulates the store off the apply loop, so a
refresh reconciling meanwhile could sweep children the build had
already inserted and let it publish the directory incomplete. The
refresh now skips a building directory, as events (buffered) and
purges (skipped) already do; its staleness dies with the build's
fresh table.

* mount: clear the unversioned marker only after its replacement lands

Clearing before the insert meant a failed write left the old local
content claiming the listing floors, fencing the very events that were
still entitled to correct it.

* meta_cache: rename the section state machine to sectionList

dirSections named both the type and the map of them.

* mount: raise the default cacheDirMaxEntries to 100000

The low ceiling guarded against whole-listing rebuild churn: a big
cached directory under writes kept re-streaming everything. Sectioned
invalidation ended that — a burst now costs one range listing — so the
remaining cost of caching a large directory is its one-time build,
comparable to the single direct listing that read-through mode pays on
every enumeration instead.

* meta_cache: cover section border and edge cases

A bound-named entry belongs to the section starting at the bound: the
neighboring refresh's sweep stops before it, its own section's covers
it. Churn past everything the build saw lands in the tail section, a
rename spanning two sections invalidates both, and a listed entry at
the section's end name is cut off with the ones beyond it.
2026-08-11 21:11:18 -07:00
Chris LuandGitHub dd73fee077 mount: read oversized directories through instead of caching them (#10631)
* mount: read oversized directories through instead of caching them

Visiting a directory pulls every child from the filer into the local
LevelDB before the first listing returns. For a directory of a few
million entries that is minutes of streaming, gigabytes of local store,
and gigabytes of decoded entries in flight -- paid by a mount that may
only walk the directory once.

A build that crosses -cacheDirMaxEntries (default ten thousand) now
stops, cleans up, and marks the directory read-through: listings stream
from the filer with pagination, the way update-hot directories already
do, and lookups in it consult the filer per entry as any uncached
directory does. The refusal is remembered, so the next visit fails fast
instead of streaming to the limit again, and an oversized ancestor is
stepped over when caching its subdirectories rather than wedging every
listing beneath it.

The direct path keeps the same pagination state on the handle, so a walk
that crosses the limit mid-flight carries on from where the cached walk
reached.

* mount: an ancestor found oversized must not fail its descendants

Visiting a directory builds its whole uncached ancestor chain in one
group, so the first discovery that an ancestor is oversized cancelled the
group and surfaced as the listed directory's own refusal: the descendant
build was aborted and the caller marked the descendant read-through,
leaving a perfectly cacheable directory streaming from the filer until
its inode was forgotten. The earlier test missed this by pre-marking the
ancestor, which exercises only the fast path.

The refusal of any directory other than the one being listed is now kept
out of the group's result; it is already remembered for the next visit.
2026-08-07 17:48:40 -07:00
Chris LuandGitHub 12627d376d mount: fix four readdir pagination bugs (#10624)
* mount: size the direct listing slice from the batch, not the offset

The limit passed here is skipCount+batchSize when a client resumes a
fresh handle partway through a directory, so preallocating for it turns
the client's cookie into an allocation: a readdir at offset 3,000,000
reserves 24MB before the first entry arrives, and an offset near the
uint32 ceiling asks makeslice for ~4.29e9 elements.

* mount: stop replaying a directory that shrank past the resume offset

A client that opens a fresh handle and resumes at a cookie from an
earlier, larger listing gets a preload that cannot reach the entry before
that offset. The resume name was then left empty and the follow-up batch
listed from the directory's first child again, so the client was handed
every name a second time.

The stream always runs from the first child, so failing to reach that
entry means the directory is simply shorter than the offset. That is the
end of it.

* mount: page a directory from where the store reached

The batch loader treated a short batch as the end of the directory, but
the meta cache drops an expired child after the store has already spent
it against the limit, so a batch that filled up could still deliver fewer
entries than asked for. A directory with a handful of expired children
would stop listing early and hide every child behind them; a whole batch
of expired ones truncated the listing to nothing.

ListDirectoryEntries now reports the name the store itself reached. That
is both the sound end-of-directory signal -- the store returning nothing
-- and the right cursor, since resuming from the last visible name would
re-read the dropped children every round and never get past a batch that
was entirely expired.

* mount: drop the entries a directory walk has already passed

entryStreamOffset was only ever written by reset, so the stream a handle
holds grew for the life of the walk and a directory was retained whole
even though nothing could read the entries behind the client's position
again. A 10M-entry walk parked millions of entries per handle, and
NFS-Ganesha opens several on the same directory.

Offsets index into the stream from entryStreamOffset, so advancing the
two together keeps them lined up. One entry is held back because the next
batch resumes from the name immediately before the offset. Seeking back
behind what is still held now restarts the directory, which is what the
offset scheme can honestly support -- it previously returned nothing.
2026-08-07 12:11:27 -07:00
Chris LuandGitHub b8cba2982c mount: tell windows about changes made elsewhere (#10553)
* mount: tell windows about changes made elsewhere

Nothing invalidates a Windows client's cache from this side, so a file
created or removed by another mount, the S3 gateway or the filer API
stayed invisible in Explorer until the user refreshed by hand. The mount
already receives those events; they just had nowhere to go.

WFS gains a listener for every applied metadata event, and on Windows
that turns into the WinFsp notification for the path. A rename reports
both ends, since the destination's own event may never arrive when it
falls outside this mount.

* mount: report a removed directory as a directory

Entry is nil once a path is vacated, so asking it whether the thing that
went away was a directory always answered no and every removal was
reported as a file. Windows watches the two through different filters, so
a folder removed elsewhere never refreshed.

The invalidation now carries what used to be there, which the event
already knew and simply was not passing on.

* mount: report a rename destination once

The event stream already carries a second invalidation describing the new
path, so reporting RenamedTo here sent the destination twice — and always
as a create, so a moved directory arrived as a create followed by a
mkdir.
2026-08-03 22:17:09 -07:00
Chris LuandGitHub 47b491b53c mount: version open file handles by filer log position (#10403)
* filer: stamp a log position on lookup and remote-cache responses

Metadata events are logged after their store write and stamped with the
filer clock. Reading that clock before serving an entry therefore gives
a timestamp with a causal guarantee: every event at or below it is
reflected in the returned entry. Clients caching filer state can use it
as the entry's version to order the response against subscription
events, including events committed before the call but delivered after
it.

* mount: version open file handles by filer log position

A subscription event refreshing an open handle did a second lookup; a
transient failure left the handle pinned to its old entry with no
retry, since the subscription cursor had already advanced. The deeper
problem is ordering: the handle is a cache written by three unordered
channels — the async invalidation worker, local mutation acks, and
open-time lookups — and overwriting cached state safely requires
knowing which write is newer.

The filer log timestamp is that order, and it now travels with every
value instead of being derived out of band. Events carry it natively;
lookup and remote-cache responses carry the log position stamped before
the serving read; mutation acks carry it in their returned event; and
the local store pairs each read with a version cursor advanced under
the same lock as the store write. Each handle records the version its
entry reflects, and one rule replaces the per-site reasoning: state at
or below the handle's version is old news and must not be installed.

The invalidation itself applies the event's own entry — no lookup, so
no transient-failure window — except under a cached parent, where the
store entry is the ordered merge of the event and anything applied
since, and its version outranks the event's. An uncached parent
receives no store writes, so a hit there would be a stale leftover
masking the event. A vacated path (delete, rename away) keeps the last
entry so unlinked-but-open reads still work. Directory builds version
the completed directory at the listing snapshot and re-invalidate
buffered events at that version, since their mid-build refresh ran
against an incomplete store.

The tests replay every race this replaces machinery for: rollback of a
newer local flush (queued, cached, and read-through), stale leftovers
under uncached parents, the build window including abort, handles
opened after an event was queued, events landing mid-lookup, and
undelivered events at remote-cache time across a filer failover.

* filer: serialize the log position fence with mutations, stamp mutation acks

The fence stamped before an unlocked entry read could precede state the
read returned: a mutation writes storage first and assigns its event
timestamp only at notify time, so a lookup racing that window handed
the mount an entry newer than its fence, and the event's later delivery
looked like fresh news — destroying dirty pages for a change the handle
already had. The mutation handlers already hold an exclusive per-path
lock across read, write, and notify; the lookup and remote-cache reads
now take it shared around the stamp and the read, making the fence
exact: everything at or below it is in the entry, nothing above it is.

A no-change update returns success without an event, leaving the mount
nothing to fence with even though the response confirms current state.
Create and update acks now carry a log position stamped under the same
lock, and the mount falls back to it whenever the ack has no event.

Also regenerate the VT marshalers, which the earlier generation missed:
without them a VT round-trip silently zeroed every log position.

* java: sync filer.proto

* mount: scope store versions to what they vouch for; atomic handle install

The store's version cursor claimed too much. Advanced by local mutation
acks and directory listing snapshots, it inflated the version of store
reads for unrelated paths whose events the subscription still owed, and
those events were then fenced out permanently. The cursor now tracks
subscription progress only — events arrive in log order, so everything
at or below it has been delivered for every path — and a completed
listing records its snapshot as a per-directory floor instead of a
global claim. Local acks never touch it: they version their own handle
directly. Buffered build events advance the cursor at delivery, since
their store write may never happen (abort) while their invalidation is
already queued; their read-through directory pairs no store read with
it, and rename fragments are applied first.

Concurrent first opens raced: a slower opener's older lookup could
overwrite the newer entry a faster opener had installed, while the
monotonic version kept the newer timestamp — an old entry fenced at a
new version, immune to every correcting event. Entry and version are
now installed as one decision under the handle map lock, and an install
that does not outrank the handle's version is dropped.

The remote-cache commit also escaped the fence: it wrote storage and
notified without the path lock, so a lookup's shared-locked fence and
read could land between the two and hand out the cached state
under-versioned. The commit now re-reads and writes under the exclusive
path lock, and backs off entirely when the entry changed during the
download — the concurrent writer supersedes the cached content.

* mount: floors gate store applies; installs respect handle users; renames join the fence

A directory floor certifies the listing state as of its snapshot, but a
delayed event at or below the floor was still applied to the store —
rolling the content back to pre-snapshot state while the floor kept
claiming the snapshot version, so the correcting events were fenced out
of every future read. Events are now gated against the affected
directory's floor, each half of a rename independently.

Fences are lower bounds: a listing or lookup can include a mutation
whose event has not been delivered yet, and that event later passes
every gate carrying state the handle already holds. Such a re-delivery
now advances the version without destroying dirty pages or reinstalling
the entry — invalidating local writes over a no-op was the real damage
in every remaining under-fence window, including the unlocked listing
snapshot, which no per-path lock can serialize.

The concurrent-open install moved from the map lock to the handle lock
every reader, writer, and invalidation synchronizes on, and rejects
what cannot improve the handle: dirty state (local writes would be
lost), unversioned lookup responses (they cannot outrank anything, and
two zero-version opens must not overwrite each other), and anything not
strictly newer. New handles are still fully initialized before the map
exposes them.

Renames committed metadata and emitted events with no path lock, so a
lookup could read the renamed state under a fence preceding its events.
Both rename handlers now hold the source and destination locks, ordered
by path, across commit and notification; descendants of a renamed
directory are not individually locked and rely on the no-op re-delivery
handling above.

* mount: per-entry store versions replace the cursor and directory floors

The store's aggregate versions — a global subscription cursor and
per-directory listing floors — were versions at coarser granularity
than the values they described, and every over-claiming bug in this
series traced to that gap: an aggregate vouching for state its source
never saw. Each store entry now carries the filer log position of the
write that produced it — the event that applied it, or the listing
snapshot that inserted it, recorded in the store's key-value space
under the same lock as the entry write. The store becomes what the
handle already is: a last-writer-wins register with one rule, install
only what outranks the current claim.

The cursor, the floors, their advancement rules, the pairing ordering
constraint, and the floor gating all collapse into that rule. Applies
are gated per entry, each half of a rename independently; an
unversioned local write clears the claim its content no longer proves;
version records lingering after a bulk folder wipe cannot fence a
recreate, since a claim only blocks while its entry exists. Listing
inserts are stamped at build completion, before the buffered replay so
newer replayed events override the stamp.

Filer side, the fence dance every versioned read must perform is now a
single choke point, fencedFindEntry, so a future read RPC gets the
lock-serialized stamp by construction rather than by convention.

* mount: judge no-op re-deliveries against an immutable base, not the live entry

The equal-state skip compared the incoming event to the live handle
entry, but local writes mutate the live entry — size, timestamps,
chunks — so a delayed event re-delivering the base the handle was
opened with no longer matched, and the installer destroyed the dirty
pages and rolled the entry back over nothing new. The handle now keeps
an immutable snapshot of the filer state it last installed or
acknowledged, refreshed at every install and mutation ack (flush acks
snapshot the request entry before the id mapping mutates it), and the
no-op judgment runs against that base: an event carrying the base
brings nothing, whatever the live entry has diverged to since.

* mount: tombstones for versioned deletes, absence floors, copy enrollment

Four gaps in the per-entry version protocol, all the same shape: a
versioned fact with nothing carrying its version.

A deletion is a fact about a path with no entry left to hold it —
clearing the record let a delayed older event resurrect the deleted
path, permanently, since the deletion's own redelivery is
dedup-suppressed. Versioned deletes now leave a tombstone record that
fences without an entry; renames tombstone their source the same way.
Plain records still only block while their entry exists, so records
lingering after a bulk folder wipe cannot fence a recreate.

A completed listing proves absences as well as presences: a name it
omitted was deleted as of the snapshot, and a delayed create below the
snapshot re-creates it. The snapshot is kept per directory strictly as
an absence fence, consulted only when a path has neither an entry nor
a version record — present entries carry their own versions and never
touch it, which is what separates this from the over-claiming floor it
replaces.

A rebuild against a pre-upgrade filer returns no snapshot; stamping
now clears the children's records in that case, so a reinserted entry
cannot reactivate the stale claim its previous incarnation left
behind and reject valid events below it.

Server-side copies installed the copied entry without enrolling in the
base protocol, so the copy's own event differed from the stale
pre-copy base and destroyed writes made to the destination after the
copy. The install now refreshes the base and takes its version from
the fenced readback.

* mount: deletion facts outlive the cache's knowledge of the entry

A versioned delete of a path the store held no entry for recorded
nothing, so a delayed older event recreated the path — permanently,
with the deletion's redelivery dedup-suppressed. The tombstone is now
written whenever a versioned event vacates a path: the deletion is a
fact about the path, not about what this cache happened to hold.

For an absent entry, the listing's absence floor now speaks whatever
older record remains: a tombstone at one position does not exhaust
what is known about the path when a newer snapshot has confirmed the
name still absent, and an event between the two was slipping past
both.

A committed copy whose readback failed installed a synthesized base
with local timestamps; the copy's real event legitimately differs from
it, and was read as foreign state — destroying writes made to the
destination after the copy. The handle now marks that its own event is
en route and adopts that event's state as the base without touching
the live entry or the dirty pages; the adoption is one-shot, so a
genuinely foreign event still invalidates.

* mount: authoritative acks cancel pending event adoption; tombstones scoped and pruned

The copy-event adoption flag could outlive its purpose: a flush after
the failed readback installs a newer base and advances the version, the
copy's own event is then version gated without consuming the flag, and
the next genuinely foreign event was silently adopted — base advanced,
live entry and dirty pages untouched — leaving the mount to later
overwrite that remote change. Every local acknowledgment now installs
its base through one helper that also cancels any pending adoption: the
ack supersedes the mutation the adoption was waiting for.

Tombstones were written for every versioned delete under the mount and
survived directory eviction by design, growing LevelDB with historical
deletions on delete-heavy mounts. They are now scoped to directories
whose cached state the fence actually protects — an uncached parent
never serves from the store nor applies the resurrecting insert — and a
completed listing prunes the direct-child tombstones its absence floor
supersedes, leaving only those above the snapshot. The store gains a
key-prefix visitor for the sweep.

* mount: acked saves install their value; trailer snapshots; direct-child prune range

A version must never advance without its value. saveEntry stamped any
open handle with the acknowledgment's version, but a handle opened
while the save was in flight holds the pre-mutation entry — stamping it
fenced out the events carrying the state it lacked, permanently, with
the local apply performing no invalidation and the redelivery
deduplicated. The acknowledged entry is now installed together with its
version, through the same guarded install the racing-open path uses:
under the handle lock, only when it outranks the handle, never over
dirty local writes.

Empty listings return no in-band snapshot — a snapshot-only response
would be read as an entry by older consumers — so directories that end
empty gained no absence floor and their tombstones were never pruned.
The filer now sends the snapshot in the stream trailer, which older
clients ignore, and the client reads it when no in-band snapshot
arrived. Empty directories get real floors, their tombstones prune,
and their buffered replays gain the snapshot filter instead of the
replay-all fallback.

Version records now encode the parent directory and name separated by
a NUL, making a directory's direct children one contiguous key range:
the tombstone prune scans exactly them under the cache lock, instead
of walking every descendant record — the whole store, for root.

* mount: fix dirty-page loss, uid/gid base, download race, copy adopt, leak; dedup

Correctness fixes from the versioned-invalidation review:

- A foreign delete/rename-away of a file held open with unflushed local
  writes destroyed the dirty pages unconditionally. A process may keep
  writing to an unlinked-but-open file and those writes were already
  acknowledged; preserve the pages when the handle is dirty.
- downloadRemoteEntry stored the handle's base with filer-side uid/gid
  while every candidate it is later compared against is in local form,
  so under a non-identity UidGidMapper an unchanged re-delivery looked
  foreign and force-destroyed dirty pages. Map the base to local.
- downloadRemoteEntry wrote the entry/base/version triple under only the
  handle's shared lock, so two concurrent reads of the same remote-only
  file could tear it. Serialize the install with a dedicated mutex
  (invalidation is already excluded by the exclusive handle lock).
- A committed server-side copy whose readback failed adopted the FIRST
  event past the version gate as its base; a foreign write delivered
  first was silently swallowed. Adopt only an event whose content
  matches the synthesized base — the copy's own event — and install any
  other normally.
- The deferred-create path relied on AcquireFileHandle installing the
  passed entry on a pre-existing handle, which the version rework
  dropped. Restore that install in the compat wrapper; the versioned
  open path keeps its gated install.

Growth and hot-path cost:

- Per-entry version records and tombstones leaked when a directory was
  evicted or read-through without a rebuild. An uncached directory
  gates its own inserts, so its records fence nothing; clear a
  directory's child version records when it is wiped for eviction.
- FindEntry paid for the version KvGet on every lookup/getattr cache hit
  and threw it away. FindEntry now reads only the entry; the hot
  lookupEntry cache-hit path skips the version entirely.

Cleanups:

- Extract ackVersionTsNs over the shared response interface, replacing
  the metadata-event-else-log-ts snippet copy-pasted at four ack sites.
- Extract acquireRenamePathLocks, replacing the verbatim sorted
  two-path lock fence in both rename handlers.

* mount: no resurrection on foreign delete, version no-event acks, gate downloads, tighten copy adopt

Follow-ups to the review patches:

- Preserving dirty pages on a foreign delete let the next flush pass the
  isDeleted guard and CreateEntry, resurrecting the remotely-unlinked
  name. Mark the handle deleted in the vacate branch: the open fd can
  still read its buffered writes, but a flush no longer recreates the
  file.
- A no-event acknowledgment (log fence only) synthesized a metadata
  event with TsNs 0, so the cache stored the entry unversioned and an
  older subscriber event rolled it back. Stamp the synthesized event
  with the ack's log position at all four ack sites.
- downloadRemoteEntry serialized its install but did not check the
  version, so an older response arriving last overwrote the entry/base
  while the monotonic version kept the newer value, fencing corrections
  out. Install only when the response is at least as new as the handle.
- sameEntryContent compared only size and chunks, so a foreign chmod
  with unchanged content was adopted as the copy's own event. Compare
  everything except server-assigned timestamps, so a metadata-only
  foreign change installs instead.

* mount: trim comments to the non-obvious why

The versioning work accumulated multi-line comment blocks restating what
the code says. Keep the constraint a reader cannot derive — why a fence
is exact, why a version must not advance without its value, why an
uncached parent's records fence nothing — and drop the rest.

* mount: distinguish rename from delete, tighten the download and adopt gates

- A rename emits a nil old-path invalidation just like an unlink, so the
  vacate branch marked the handle deleted and later writes through the
  already-open descriptor were skipped instead of persisted. Carry the
  delete/rename distinction on the invalidation and mark only an actual
  delete.
- The remote-download install accepted an unversioned response
  regardless of the handle's version, so during a rolling upgrade a
  delayed response could install stale content under a newer version.
  Require the response to be at least as new, with one exception: a
  handle still lacking local chunks takes the content anyway — it cannot
  read without it — but does not claim the response's log position.
- Copy-event adoption returned without installing, so a foreign touch
  arriving before the copy's own event lost its timestamps. Content is
  unchanged either way, so the dirty pages stay valid; a clean handle now
  takes the entry, while a dirty one keeps its diverged version.

* mount: one directory floor instead of a record per child; agree on TTL

Review feedback:

- Build completion wrote one KV record per direct child inside the cache
  write lock, so a large directory stalled every other cache operation
  for O(children) store writes. The directory's listing snapshot already
  covers every child it saw; make that floor the version for any child
  without a record of its own, and a child earns a record only when a
  later event touches it. One map write per build replaces the per-child
  writes, with the same fencing.
- The presence probe read the store directly and so counted a
  TTL-expired entry as present, judging the path by a record describing
  content that has logically vanished. It now applies the same expiry
  the read path does, and an expired path falls back to its directory
  floor.
- Preserve ErrNotFound identity when the commit-time re-read finds the
  object deleted, so callers still surface a 404.
- Assert the rename-away source fence timestamp in the invalidation test.

Also record the tombstone ceiling: distinct deleted names in a cached
directory accumulate until it is rebuilt or evicted, which prunes
everything at or below the new snapshot.

* mount: pin the fence's clock domain instead of letting skew decide

A log-position fence is stamped by one filer's clock under that filer's
in-process lock, so comparing it to an event another filer logged is
comparing two unrelated clocks. The two error directions are not equally
costly: applying an event the fence already covered is a re-apply the
base-equality check absorbs, while skipping one it does not cover leaves
the handle holding exactly the state the event was meant to correct,
with the subscription cursor already past it — the unhealable staleness
this whole PR exists to remove.

So refuse to guess. Fences now carry the signature of the filer that
stamped them, and a handle records it alongside the position. An event
is only fenced out when the filer that logged it is the one that stamped
the fence — the logging filer appends its own signature, so its presence
identifies the clock domain. Events from any other filer are applied.
Positions taken from events keep comparing as before; the subscription
already delivers those in order.

The invalidation callback takes a struct now: it carries the path,
entry, position, delete/rename distinction, and signatures, and was
about to need a fifth positional parameter.

* mount: follow a foreign rename; key page invalidation on content, not equality

- A rename's old-path invalidation now carries the destination, and the
  handle follows the file there: an open fd tracks the inode, and leaving
  it on the old path made its next flush recreate that name instead of
  updating the renamed file.
- Dirty pages overlay content, so only a content change invalidates them.
  Keying that on exact equality meant any timestamp-only event destroyed
  them, which the copy-adoption marker existed to paper over — a foreign
  touch could consume the marker and leave the copy's own event to drop
  the post-copy writes. Comparing content instead makes the marker
  unnecessary, so it is gone: a metadata-only event keeps the overlay,
  and a dirty handle keeps its diverged entry unless foreign content
  supersedes it.
- A remote download response that is merely older is now refused even
  when the handle still lacks chunks; only an unversioned one is taken
  (and claims no position), since an older response's content predates
  what the handle reflects.
- A refused or unversioned download no longer publishes to the metadata
  cache, where a zero-position event would clear the entry's version and
  let an older subscriber event roll the cache back.

* mount: page invalidation keys on content alone; unversioned writes claim no position

- sameEntryContent compared everything but timestamps, so a foreign
  chmod, chown, or xattr change counted as a content change and
  destroyed the dirty-page overlay. It was strict only to serve the
  copy-adoption marker, which is gone; its one caller now asks the
  question it actually needs — did the bytes change — so metadata-only
  events leave the overlay alone.
- A rename over an existing file destroys that file, but its open handle
  was left live and still pointed at the name the renamed source now
  occupies, so its flush could overwrite it. MovePath already reports the
  displaced inode; mark that handle deleted.
- An acknowledgment was refused whenever its position was numerically
  lower, even when a different filer stamped the fence it lost to. Two
  known, differing signatures mean unrelated clocks, so the comparison no
  longer applies there; unknown signatures still compare as before.
- A local write with no log position behind it now records that
  explicitly instead of deleting its version record. Absence means the
  directory listing covers the path, which is why the snapshot floor
  applies; local content the listing never saw must not inherit it, or
  the events that would correct it are fenced out.

* mount: widen the existing lookup functions instead of forking WithVersion twins

The versioning work grew a parallel function for every accessor that
needed to return a log position — lookupEntryWithVersion beside
lookupEntry, maybeLoadEntryWithVersion beside maybeLoadEntry,
FindEntryWithVersion beside FindEntry, AcquireFileHandleWithVersion
beside AcquireFileHandle, advanceEntryVersion beside
advanceEntryVersionTsNs, plus a getPbEntryWithVersion wrapper and an
InsertListedEntriesForTest hook. Two names for one operation is two
places to keep in step, and the split let callers pick the one that
happened to compile.

Each pair is now the single original name carrying the position, with
callers that do not want it discarding it. filer_pb.GetEntry returns the
fence its response already carried rather than a mount-side wrapper
re-issuing the lookup, and InsertEntry takes the position its content
reflects rather than a test-only twin that inserted without one.

The one behavioural knot the merge exposed: AcquireFileHandle had been
installing the entry on a pre-existing handle only in its unversioned
form, which conflated 'the caller is authoritative' with 'the lookup had
no version'. Deferred create is the only caller that means the former,
so it now installs explicitly and the map function just acquires.
2026-07-23 17:44:02 -07:00
18868e5204 fix(mount): run entry invalidations off the meta-cache apply loop (#10002)
* fix(mount): run entry invalidations off the meta-cache apply loop

The apply loop ran invalidateFunc inline, which acquires the open file
handle's lock in fhLockTable. Meanwhile flushMetadataToFiler holds that
same fh lock and then waits on the apply loop (applyLocalMetadataEvent).
When both target the same open file concurrently, the loop blocks on the
fh lock while the lock holder blocks on the loop: an ABBA deadlock that
backs up every later readdir/flush and hangs the mount.

Fix: dispatch entry invalidations to a dedicated FIFO worker goroutine so
the apply loop never blocks on locks held by goroutines waiting on it.
Adds a regression test reproducing the interleaving.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* perf(mount): update invalidate counter once per batch

Run the batch's invalidateFunc calls without re-taking invalidateMu per
item, then bump invalidateProcessed and broadcast once after the loop.
WaitForEntryInvalidations only needs the count to reach its target and a
batch always completes together, so the per-item lock + broadcast was
wasted work.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* mount: extract the invalidate worker into util.AsyncBatchWorker

The apply loop's off-thread entry-invalidation queue was a one-off mutex +
cond + slice + counters living inside MetaCache. Pull it out as a generic
unbounded FIFO worker so the deadlock-avoidance contract (never block the
producer, drain on shutdown, wait-for-quiesce) lives in one place.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Chris Lu <chris.lu@gmail.com>
2026-06-19 09:19:35 -07:00
Chris LuandGitHub 7c32e651f7 mount: fix deadlock reading an uncached remote-mounted file (#9995)
* mount: apply cached remote entry without blocking the read

Reading an uncached remote-mounted file hung forever. The read holds the
file-handle shared lock across the on-demand download, then synchronously
waits on the metadata apply loop to apply the cached entry. The filer's
update event for that same object reaches the apply loop first and runs
invalidateFunc, which wants the file-handle exclusive lock — held in shared
mode by the still-running read. The loop blocks on the read; the read blocks
on the loop.

Enqueue the apply without waiting so the read never blocks on the apply loop
while holding the lock. The handle is already updated via SetEntry, and the
filer subscription delivers the same event regardless.

* mount: regression test for uncached remote read deadlock

Drives the real Read path through downloadRemoteEntry with a stub filer that
broadcasts the matching invalidate event before returning, reproducing the
lock-ordering deadlock. Fails (times out) without the fix.
2026-06-16 16:47:59 -07:00
Chris LuandGitHub 1826a5d222 fix(mount): pin rebuild entries by their own inode, not inodeToPath (#9993)
isLocalOnlyEntry resolved the pinned-child check through inodeToPath. A kernel
Forget drops the path→inode mapping once the lookup count reaches zero, but an
async writeback flush — and the file handle, still in fhMap during the drain —
is keyed by inode and outlives that mapping. Between Release dispatching the
async flush and the flush reaching the filer, a Forget could unpin an in-flight
create, so a concurrent directory rebuild would wipe it and the file would
ENOENT until the flush lands and the cache refreshes.

Key the check off the inode the store entry already carries (createFile stamps
it into the placeholder), so the pin no longer depends on a mapping Forget can
remove.
2026-06-16 14:59:13 -07:00
Chris LuandGitHub 93f91c96ca fix(mount): keep a deferred local create from vanishing when its dir is rebuilt (#9991)
wfs.Create defers the filer create to Flush and inserts a local-only
placeholder into the metaCache (dirtyMetadata=true), so a just-created file
exists locally before the filer has it. When the parent directory falls out of
cache (hot-dir read-through, idle evict) and is rebuilt, EnsureVisited wipes the
store and refills from a filer listing that does not yet include the un-flushed
create, then markCachedFn publishes the directory authoritatively cached without
it. lookupEntry then returns an authoritative ENOENT and ReadDir returns nothing
— the file disappears from the mount although the client created it. Under
concurrent read+write churn on one directory this is the ConcurrentReadWrite
flake.

Preserve children the mount flags as local-only (open dirty handle or pending
async flush — the signal lookupEntry already trusts) across the rebuild's wipe
instead of blind-deleting them. Unpinned stale children are still dropped so a
rebuild cannot resurrect a deleted entry.
2026-06-16 13:57:12 -07:00
Chris LuandGitHub b5a952bcb1 fix(mount): don't strand a directory cached-but-empty when an eviction races a rebuild (#9791)
* fix(mount): don't strand a directory cached-but-empty when an off-loop wipe races a rebuild

Idle eviction, kernel Forget, and the copy-range fallback cleared a
directory's cached entries directly, off the metaCache apply loop, after
resetting the cached flag in inodeToPath as a separate step. A concurrent
rebuild could publish a fresh listing (markCachedFn) in between, so the late
DeleteFolderChildren left the directory flagged cached over an empty store.
lookupEntry then returns an authoritative ENOENT and ReadDir returns nothing,
so every file in the directory disappears from the mount although it is still
present on the filer.

Route those wipes through a new apply-loop step that resets the flag and wipes
the store together, serialized with a build's markCachedFn, and skips a
directory while it is building.

* fix(mount): route the meta-event retry cleanup through the apply-loop purge

The subscription-retry callback wiped the mount root's cached children
directly off the apply loop and reset the cache flags as a separate step — the
same pattern that can leave a concurrently-rebuilding root cached-but-empty.
Invalidate all flags (safe on its own, it never deletes entries) then purge the
root's children through the apply loop.
2026-06-02 14:43:46 -07:00
Chris LuandGitHub 2264941a17 fix(mount): update parent directory mtime/ctime on deferred file create (#9021)
* fix(mount): update parent directory mtime/ctime on deferred file create

* style: run go fmt on mount package
2026-04-10 13:05:48 -07:00
479e72b5ab mount: add option to show system entries (#8829)
* mount: add option to show system entries

* address gemini code review's suggested changes

* rename flag from -showSystemEntries to -includeSystemEntries

* meta_cache: purge hidden system entries on filer events

---------

Co-authored-by: Chris Lu <chris.lu@gmail.com>
2026-03-29 13:33:17 -07:00
8cde3d4486 Add data file compaction to iceberg maintenance (Phase 2) (#8503)
* Add iceberg_maintenance plugin worker handler (Phase 1)

Implement automated Iceberg table maintenance as a new plugin worker job
type. The handler scans S3 table buckets for tables needing maintenance
and executes operations in the correct Iceberg order: expire snapshots,
remove orphan files, and rewrite manifests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add data file compaction to iceberg maintenance handler (Phase 2)

Implement bin-packing compaction for small Parquet data files:
- Enumerate data files from manifests, group by partition
- Merge small files using parquet-go (read rows, write merged output)
- Create new manifest with ADDED/DELETED/EXISTING entries
- Commit new snapshot with compaction metadata

Add 'compact' operation to maintenance order (runs before expire_snapshots),
configurable via target_file_size_bytes and min_input_files thresholds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix memory exhaustion in mergeParquetFiles by processing files sequentially

Previously all source Parquet files were loaded into memory simultaneously,
risking OOM when a compaction bin contained many small files. Now each file
is loaded, its rows are streamed into the output writer, and its data is
released before the next file is loaded — keeping peak memory proportional
to one input file plus the output buffer.

* Validate bucket/namespace/table names against path traversal

Reject names containing '..', '/', or '\' in Execute to prevent
directory traversal via crafted job parameters.

* Add filer address failover in iceberg maintenance handler

Try each filer address from cluster context in order instead of only
using the first one. This improves resilience when the primary filer
is temporarily unreachable.

* Add separate MinManifestsToRewrite config for manifest rewrite threshold

The rewrite_manifests operation was reusing MinInputFiles (meant for
compaction bin file counts) as its manifest count threshold. Add a
dedicated MinManifestsToRewrite field with its own config UI section
and default value (5) so the two thresholds can be tuned independently.

* Fix risky mtime fallback in orphan removal that could delete new files

When entry.Attributes is nil, mtime defaulted to Unix epoch (1970),
which would always be older than the safety threshold, causing the
file to be treated as eligible for deletion. Skip entries with nil
Attributes instead, matching the safer logic in operations.go.

* Fix undefined function references in iceberg_maintenance_handler.go

Use the exported function names (ShouldSkipDetectionByInterval,
BuildDetectorActivity, BuildExecutorActivity) matching their
definitions in vacuum_handler.go.

* Remove duplicated iceberg maintenance handler in favor of iceberg/ subpackage

The IcebergMaintenanceHandler and its compaction code in the parent
pluginworker package duplicated the logic already present in the
iceberg/ subpackage (which self-registers via init()). The old code
lacked stale-plan guards, proper path normalization, CAS-based xattr
updates, and error-returning parseOperations.

Since the registry pattern (default "all") makes the old handler
unreachable, remove it entirely. All functionality is provided by
iceberg.Handler with the reviewed improvements.

* Fix MinManifestsToRewrite clamping to match UI minimum of 2

The clamp reset values below 2 to the default of 5, contradicting the
UI's advertised MinValue of 2. Clamp to 2 instead.

* Sort entries by size descending in splitOversizedBin for better packing

Entries were processed in insertion order which is non-deterministic
from map iteration. Sorting largest-first before the splitting loop
improves bin packing efficiency by filling bins more evenly.

* Add context cancellation check to drainReader loop

The row-streaming loop in drainReader did not check ctx between
iterations, making long compaction merges uncancellable. Check
ctx.Done() at the top of each iteration.

* Fix splitOversizedBin to always respect targetSize limit

The minFiles check in the split condition allowed bins to grow past
targetSize when they had fewer than minFiles entries, defeating the
OOM protection. Now bins always split at targetSize, and a trailing
runt with fewer than minFiles entries is merged into the previous bin.

* Add integration tests for iceberg table maintenance plugin worker

Tests start a real weed mini cluster, create S3 buckets and Iceberg
table metadata via filer gRPC, then exercise the iceberg.Handler
operations (ExpireSnapshots, RemoveOrphans, RewriteManifests) against
the live filer. A full maintenance cycle test runs all operations in
sequence and verifies metadata consistency.

Also adds exported method wrappers (testing_api.go) so the integration
test package can call the unexported handler methods.

* Fix splitOversizedBin dropping files and add source path to drainReader errors

The runt-merge step could leave leading bins with fewer than minFiles
entries (e.g. [80,80,10,10] with targetSize=100, minFiles=2 would drop
the first 80-byte file). Replace the filter-based approach with an
iterative merge that folds any sub-minFiles bin into its smallest
neighbor, preserving all eligible files.

Also add the source file path to drainReader error messages so callers
can identify which Parquet file caused a read/write failure.

* Harden integration test error handling

- s3put: fail immediately on HTTP 4xx/5xx instead of logging and
  continuing
- lookupEntry: distinguish NotFound (return nil) from unexpected RPC
  errors (fail the test)
- writeOrphan and orphan creation in FullMaintenanceCycle: check
  CreateEntryResponse.Error in addition to the RPC error

* go fmt

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 11:27:42 -07:00
3f946fc0c0 mount: make metadata cache rebuilds snapshot-consistent (#8531)
* filer: expose metadata events and list snapshots

* mount: invalidate hot directory caches

* mount: read hot directories directly from filer

* mount: add sequenced metadata cache applier

* mount: apply metadata responses through cache applier

* mount: replay snapshot-consistent directory builds

* mount: dedupe self metadata events

* mount: factor directory build cleanup

* mount: replace proto marshal dedup with composite key and ring buffer

The dedup logic was doing a full deterministic proto.Marshal on every
metadata event just to produce a dedup key. Replace with a cheap
composite string key (TsNs|Directory|OldName|NewName).

Also replace the sliding-window slice (which leaked the backing array
unboundedly) with a fixed-size ring buffer that reuses the same array.

* filer: remove mutex and proto.Clone from request-scoped MetadataEventSink

MetadataEventSink is created per-request and only accessed by the
goroutine handling the gRPC call. The mutex and double proto.Clone
(once in Record, once in Last) were unnecessary overhead on every
filer write operation. Store the pointer directly instead.

* mount: skip proto.Clone for caller-owned metadata events

Add ApplyMetadataResponseOwned that takes ownership of the response
without cloning. Local metadata events (mkdir, create, flush, etc.)
are freshly constructed and never shared, so the clone is unnecessary.

* filer: only populate MetadataEvent on successful DeleteEntry

Avoid calling eventSink.Last() on error paths where the sink may
contain a partial event from an intermediate child deletion during
recursive deletes.

* mount: avoid map allocation in collectDirectoryNotifications

Replace the map with a fixed-size array and linear dedup. There are
at most 3 directories to notify (old parent, new parent, new child
if directory), so a 3-element array avoids the heap allocation on
every metadata event.

* mount: fix potential deadlock in enqueueApplyRequest

Release applyStateMu before the blocking channel send. Previously,
if the channel was full (cap 128), the send would block while holding
the mutex, preventing Shutdown from acquiring it to set applyClosed.

* mount: restore signature-based self-event filtering as fast path

Re-add the signature check that was removed when content-based dedup
was introduced. Checking signatures is O(1) on a small slice and
avoids enqueuing and processing events that originated from this
mount instance. The content-based dedup remains as a fallback.

* filer: send snapshotTsNs only in first ListEntries response

The snapshot timestamp is identical for every entry in a single
ListEntries stream. Sending it in every response message wastes
wire bandwidth for large directories. The client already reads
it only from the first response.

* mount: exit read-through mode after successful full directory listing

MarkDirectoryRefreshed was defined but never called, so directories
that entered read-through mode (hot invalidation threshold) stayed
there permanently, hitting the filer on every readdir even when cold.
Call it after a complete read-through listing finishes.

* mount: include event shape and full paths in dedup key

The previous dedup key only used Names, which could collapse distinct
rename targets. Include the event shape (C/D/U/R), source directory,
new parent path, and both entry names so structurally different events
are never treated as duplicates.

* mount: drain pending requests on shutdown in runApplyLoop

After receiving the shutdown sentinel, drain any remaining requests
from applyCh non-blockingly and signal each with errMetaCacheClosed
so callers waiting on req.done are released.

* mount: include IsDirectory in synthetic delete events

metadataDeleteEvent now accepts an isDirectory parameter so the
applier can distinguish directory deletes from file deletes. Rmdir
passes true, Unlink passes false.

* mount: fall back to synthetic event when MetadataEvent is nil

In mknod and mkdir, if the filer response omits MetadataEvent (e.g.
older filer without the field), synthesize an equivalent local
metadata event so the cache is always updated.

* mount: make Flush metadata apply best-effort after successful commit

After filer_pb.CreateEntryWithResponse succeeds, the entry is
persisted. Don't fail the Flush syscall if the local metadata cache
apply fails — log and invalidate the directory cache instead.
Also fall back to a synthetic event when MetadataEvent is nil.

* mount: make Rename metadata apply best-effort

The rename has already succeeded on the filer by the time we apply
the local metadata event. Log failures instead of returning errors
that would be dropped by the caller anyway.

* mount: make saveEntry metadata apply best-effort with fallback

After UpdateEntryWithResponse succeeds, treat local metadata apply
as non-fatal. Log and invalidate the directory cache on failure.
Also fall back to a synthetic event when MetadataEvent is nil.

* filer_pb: preserve snapshotTsNs on error in ReadDirAllEntriesWithSnapshot

Return the snapshot timestamp even when the first page fails, so
callers receive the snapshot boundary when partial data was received.

* filer: send snapshot token for empty directory listings

When no entries are streamed, send a final ListEntriesResponse with
only SnapshotTsNs so clients always receive the snapshot boundary.

* mount: distinguish not-found vs transient errors in lookupEntry

Return fuse.EIO for non-not-found filer errors instead of
unconditionally returning ENOENT, so transient failures don't
masquerade as missing entries.

* mount: make CacheRemoteObject metadata apply best-effort

The file content has already been cached successfully. Don't fail
the read if the local metadata cache update fails.

* mount: use consistent snapshot for readdir in direct mode

Capture the SnapshotTsNs from the first loadDirectoryEntriesDirect
call and store it on the DirectoryHandle. Subsequent batch loads
pass this stored timestamp so all batches use the same snapshot.

Also export DoSeaweedListWithSnapshot so mount can use it directly
with snapshot passthrough.

* filer_pb: fix test fake to send SnapshotTsNs only on first response

Match the server behavior: only the first ListEntriesResponse in a
page carries the snapshot timestamp, subsequent entries leave it zero.

* Fix nil pointer dereference in ListEntries stream consumers

Remove the empty-directory snapshot-only response from ListEntries
that sent a ListEntriesResponse with Entry==nil, which crashed every
raw stream consumer that assumed resp.Entry is always non-nil.

Also add defensive nil checks for resp.Entry in all raw ListEntries
stream consumers across: S3 listing, broker topic lookup, broker
topic config, admin dashboard, topic retention, hybrid message
scanner, Kafka integration, and consumer offset storage.

* Add nil guards for resp.Entry in remaining ListEntries stream consumers

Covers: S3 object lock check, MQ management dashboard (version/
partition/offset loops), and topic retention version loop.

* Make applyLocalMetadataEvent best-effort in Link and Symlink

The filer operations already succeeded; failing the syscall because
the local cache apply failed is wrong. Log a warning and invalidate
the parent directory cache instead.

* Make applyLocalMetadataEvent best-effort in Mkdir/Rmdir/Mknod/Unlink

The filer RPC already committed; don't fail the syscall when the
local metadata cache apply fails. Log a warning and invalidate the
parent directory cache to force a re-fetch on next access.

* flushFileMetadata: add nil-fallback for metadata event and best-effort apply

Synthesize a metadata event when resp.GetMetadataEvent() is nil
(matching doFlush), and make the apply best-effort with cache
invalidation on failure.

* Prevent double-invocation of cleanupBuild in doEnsureVisited

Add a cleanupDone guard so the deferred cleanup and inline error-path
cleanup don't both call DeleteFolderChildren/AbortDirectoryBuild.

* Fix comment: signature check is O(n) not O(1)

* Prevent deferred cleanup after successful CompleteDirectoryBuild

Set cleanupDone before returning from the success path so the
deferred context-cancellation check cannot undo a published build.

* Invalidate parent directory caches on rename metadata apply failure

When applyLocalMetadataEvent fails during rename, invalidate the
source and destination parent directory caches so subsequent accesses
trigger a re-fetch from the filer.

* Add event nil-fallback and cache invalidation to Link and Symlink

Synthesize metadata events when the server doesn't return one, and
invalidate parent directory caches on apply failure.

* Match requested partition when scanning partition directories

Parse the partition range format (NNNN-NNNN) and match against the
requested partition parameter instead of using the first directory.

* Preserve snapshot timestamp across empty directory listings

Initialize actualSnapshotTsNs from the caller-requested value so it
isn't lost when the server returns no entries. Re-add the server-side
snapshot-only response for empty directories (all raw stream consumers
now have nil guards for Entry).

* Fix CreateEntry error wrapping to support errors.Is/errors.As

Use errors.New + %w instead of %v for resp.Error so callers can
unwrap the underlying error.

* Fix object lock pagination: only advance on non-nil entries

Move entriesReceived inside the nil check so nil entries don't
cause repeated ListEntries calls with the same lastFileName.

* Guard Attributes nil check before accessing Mtime in MQ management

* Do not send nil-Entry response for empty directory listings

The snapshot-only ListEntriesResponse (with Entry == nil) for empty
directories breaks consumers that treat any received response as an
entry (Java FilerClient, S3 listing). The Go client-side
DoSeaweedListWithSnapshot already preserves the caller-requested
snapshot via actualSnapshotTsNs initialization, so the server-side
send is unnecessary.

* Fix review findings: subscriber dedup, invalidation normalization, nil guards, shutdown race

- Remove self-signature early-return in processEventFn so all events
  flow through the applier (directory-build buffering sees self-originated
  events that arrive after a snapshot)
- Normalize NewParentPath in collectEntryInvalidations to avoid duplicate
  invalidations when NewParentPath is empty (same-directory update)
- Guard resp.Entry.Attributes for nil in admin_server.go and
  topic_retention.go to prevent panics on entries without attributes
- Fix enqueueApplyRequest race with shutdown by using select on both
  applyCh and applyDone, preventing sends after the apply loop exits
- Add cleanupDone check to deferred cleanup in meta_cache_init.go for
  clarity alongside the existing guard in cleanupBuild
- Add empty directory test case for snapshot consistency

* Propagate authoritative metadata event from CacheRemoteObjectToLocalCluster and generate client-side snapshot for empty directories

- Add metadata_event field to CacheRemoteObjectToLocalClusterResponse
  proto so the filer-emitted event is available to callers
- Use WithMetadataEventSink in the server handler to capture the event
  from NotifyUpdateEvent and return it on the response
- Update filehandle_read.go to prefer the RPC's metadata event over
  a locally fabricated one, falling back to metadataUpdateEvent when
  the server doesn't provide one (e.g., older filers)
- Generate a client-side snapshot cutoff in DoSeaweedListWithSnapshot
  when the server sends no snapshot (empty directory), so callers like
  CompleteDirectoryBuild get a meaningful boundary for filtering
  buffered events

* Skip directory notifications for dirs being built to prevent mid-build cache wipe

When a metadata event is buffered during a directory build,
applyMetadataSideEffects was still firing noteDirectoryUpdate for the
building directory. If the directory accumulated enough updates to
become "hot", markDirectoryReadThrough would call DeleteFolderChildren,
wiping entries that EnsureVisited had already inserted. The build would
then complete and mark the directory cached with incomplete data.

Fix by using applyMetadataSideEffectsSkippingBuildingDirs for buffered
events, which suppresses directory notifications for dirs currently in
buildingDirs while still applying entry invalidations.

* Add test for directory notification suppression during active build

TestDirectoryNotificationsSuppressedDuringBuild verifies that metadata
events targeting a directory under active EnsureVisited build do NOT
fire onDirectoryUpdate for that directory. In production, this prevents
markDirectoryReadThrough from calling DeleteFolderChildren mid-build,
which would wipe entries already inserted by the listing.

The test inserts an entry during a build, sends multiple metadata events
for the building directory, asserts no notifications fired for it,
verifies the entry survives, and confirms buffered events are replayed
after CompleteDirectoryBuild.

* Fix create invalidations, build guard, event shape, context, and snapshot error path

- collectEntryInvalidations: invalidate FUSE kernel cache on pure
  create events (OldEntry==nil && NewEntry!=nil), not just updates
  and deletes
- completeDirectoryBuildNow: only call markCachedFn when an active
  build existed (state != nil), preventing an unpopulated directory
  from being marked as cached
- Add metadataCreateEvent helper that produces a create-shaped event
  (NewEntry only, no OldEntry) and use it in mkdir, mknod, symlink,
  and hardlink create fallback paths instead of metadataUpdateEvent
  which incorrectly set both OldEntry and NewEntry
- applyMetadataResponseEnqueue: use context.Background() for the
  queued mutation so a cancelled caller context cannot abort the
  apply loop mid-write
- DoSeaweedListWithSnapshot: move snapshot initialization before
  ListEntries call so the error path returns the preserved snapshot
  instead of 0

* Fix review findings: test loop, cache race, context safety, snapshot consistency

- Fix build test loop starting at i=1 instead of i=0, missing new-0.txt verification
- Re-check IsDirectoryCached after cache miss to avoid ENOENT race with markDirectoryReadThrough
- Use context.Background() in enqueueAndWait so caller cancellation can't abort build/complete mid-way
- Pass dh.snapshotTsNs in skip-batch loadDirectoryEntriesDirect for snapshot consistency
- Prefer resp.MetadataEvent over fallback in Unlink event derivation
- Add comment on MetadataEventSink.Record single-event assumption

* Fix empty-directory snapshot clock skew and build cancellation race

Empty-directory snapshot: Remove client-side time.Now() synthesis when
the server returns no entries. Instead return snapshotTsNs=0, and in
completeDirectoryBuildNow replay ALL buffered events when snapshot is 0.
This eliminates the clock-skew bug where a client ahead of the filer
would filter out legitimate post-list events.

Build cancellation: Use context.Background() for BeginDirectoryBuild
and CompleteDirectoryBuild calls in doEnsureVisited, so errgroup
cancellation doesn't cause enqueueAndWait to return early and trigger
cleanupBuild while the operation is still queued.

* Add tests for empty-directory build replay and cancellation resilience

TestEmptyDirectoryBuildReplaysAllBufferedEvents: verifies that when
CompleteDirectoryBuild receives snapshotTsNs=0 (empty directory, no
server snapshot), ALL buffered events are replayed regardless of their
TsNs values — no clock-skew-sensitive filtering occurs.

TestBuildCompletionSurvivesCallerCancellation: verifies that once
CompleteDirectoryBuild is enqueued, a cancelled caller context does not
prevent the build from completing. The apply loop runs with
context.Background(), so the directory becomes cached and buffered
events are replayed even when the caller gives up waiting.

* Fix directory subtree cleanup, Link rollback, test robustness

- applyMetadataResponseLocked: when a directory entry is deleted or
  moved, call DeleteFolderChildren on the old path so cached descendants
  don't leak as stale entries.

- Link: save original HardLinkId/Counter before mutation. If
  CreateEntryWithResponse fails after the source was already updated,
  rollback the source entry to its original state via UpdateEntry.

- TestBuildCompletionSurvivesCallerCancellation: replace fixed
  time.Sleep(50ms) with a deadline-based poll that checks
  IsDirectoryCached in a loop, failing only after 2s timeout.

- TestReadDirAllEntriesWithSnapshotEmptyDirectory: assert that
  ListEntries was actually invoked on the mock client so the test
  exercises the RPC path.

- newMetadataEvent: add early return when both oldEntry and newEntry are
  nil to avoid emitting events with empty Directory.

---------

Co-authored-by: Copilot <copilot@github.com>
2026-03-07 09:19:40 -08:00
Chris LuGitHubgemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2ee6e4f391 mount: refresh and evict hot dir cache (#8174)
* mount: refresh and evict hot dir cache

* mount: guard dir update window and extend TTL

* mount: reuse timestamp for cache mark

* Apply suggestion from @gemini-code-assist[bot]

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* mount: make dir cache tuning configurable

* mount: dedupe dir update notices

* mount: restore invalidate-all cache helper

* mount: keep hot dir tuning constants

* mount: centralize cache state reset

* mount: mark refresh completion time

* mount: allow disabling idle eviction

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-31 13:46:37 -08:00
9012069bd7 chore: execute goimports to format the code (#7983)
* chore: execute goimports to format the code

Signed-off-by: promalert <promalert@outlook.com>

* goimports -w .

---------

Signed-off-by: promalert <promalert@outlook.com>
Co-authored-by: Chris Lu <chris.lu@gmail.com>
2026-01-07 13:06:08 -08:00
Chris LuandGitHub 6442da6f17 mount: efficient file lookup in large directories, skipping directory caching (#7818)
* mount: skip directory caching on file lookup and write

When opening or creating a file in a directory that hasn't been cached yet,
don't list the entire directory. Instead:
- For reads: fetch only the single file's metadata directly from the filer
- For writes: create on filer but skip local cache insertion

This fixes a performance issue where opening a file in a directory
with millions of files would hang because EnsureVisited() had to
list all entries before the open could complete.

The directory will still be cached when explicitly listed (ReadDir),
but individual file operations now bypass the full directory caching.

Key optimizations:
- Extract shared lookupEntry() method to eliminate code duplication
- Skip EnsureVisited on Lookup (file open)
- Skip cache insertion on Mknod, Mkdir, Symlink, Link if dir not cached
- Skip cache update on file sync/flush if dir not cached
- If directory IS cached and entry not found, return ENOENT immediately

Fixes #7145

* mount: add error handling for meta cache insert/update operations

Handle errors from metaCache.InsertEntry and metaCache.UpdateEntry calls
instead of silently ignoring them. This prevents silent cache inconsistencies
and ensures errors are properly propagated.

Files updated:
- filehandle_read.go: handle InsertEntry error in downloadRemoteEntry
- weedfs_file_sync.go: handle InsertEntry error in doFlush
- weedfs_link.go: handle UpdateEntry and InsertEntry errors in Link
- weedfs_symlink.go: handle InsertEntry error in Symlink

* mount: use error wrapping (%w) for consistent error handling

Use %w instead of %v in fmt.Errorf to preserve the original error,
allowing it to be inspected up the call stack with errors.Is/As.
2025-12-18 21:19:15 -08:00
Chris LuandGitHub 0cd9f34177 mount: improve EnsureVisited performance with dedup, parallelism, and batching (#7697)
* mount: add singleflight to deduplicate concurrent EnsureVisited calls

When multiple goroutines access the same uncached directory simultaneously,
they would all make redundant network requests to the filer. This change
uses singleflight.Group to ensure only one goroutine fetches the directory
entries while others wait for the result.

This fixes a race condition where concurrent lookups or readdir operations
on the same uncached directory would:
1. Make duplicate network requests to the filer
2. Insert duplicate entries into LevelDB cache
3. Waste CPU and network bandwidth

* mount: fetch parent directories in parallel during EnsureVisited

Previously, when accessing a deep path like /a/b/c/d, the parent directories
were fetched serially from target to root. This change:

1. Collects all uncached directories from target to root first
2. Fetches them all in parallel using errgroup
3. Relies on singleflight (from previous commit) for deduplication

This reduces latency when accessing deep uncached paths, especially in
high-latency network environments where parallel requests can significantly
improve performance.

* mount: add batch inserts for LevelDB meta cache

When populating the meta cache from filer, entries were inserted one-by-one
into LevelDB. This change:

1. Adds BatchInsertEntries method to LevelDBStore that uses LevelDB's
   native batch write API
2. Updates MetaCache to keep a direct reference to the LevelDB store
   for batch operations
3. Modifies doEnsureVisited to collect entries and insert them in
   batches of 100 entries

Batch writes are more efficient because:
- Reduces number of individual write operations
- Reduces disk syncs
- Improves throughput for large directories

* mount: fix potential nil dereference in MarkChildrenCached

Add missing check for inode existence in inode2path map before accessing
the InodeEntry. This prevents a potential nil pointer dereference if the
inode exists in path2inode but not in inode2path (which could happen due
to race conditions or bugs).

This follows the same pattern used in IsChildrenCached which properly
checks for existence before accessing the entry.

* mount: fix batch flush when last entry is hidden

The previous batch insert implementation relied on the isLast flag to flush
remaining entries. However, if the last entry is a hidden system entry
(like 'topics' or 'etc' in root), the callback returns early and the
remaining entries in the batch are never flushed.

Fix by:
1. Only flush when batch reaches threshold inside the callback
2. Flush any remaining entries after ReadDirAllEntries completes
3. Use error wrapping instead of logging+returning to avoid duplicate logs
4. Create new slice after flush to allow GC of flushed entries
5. Add documentation for batchInsertSize constant

This ensures all entries are properly inserted regardless of whether
the last entry is hidden, and prevents memory retention issues.

* mount: add context support for cancellation in EnsureVisited

Thread context.Context through the batch insert call chain to enable
proper cancellation and timeout support:

1. Use errgroup.WithContext() so if one fetch fails, others are cancelled
2. Add context parameter to BatchInsertEntries for consistency with InsertEntry
3. Pass context to ReadDirAllEntries for cancellation during network calls
4. Check context cancellation before starting work in doEnsureVisited
5. Use %w for error wrapping to preserve error types for inspection

This prevents unnecessary work when one directory fetch fails and makes
the batch operations consistent with the existing context-aware APIs.
2025-12-09 23:44:15 -08:00
+1
tam-i13GitHubgemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>Roman TamarovChris Luchrislugemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
b669607fcd Add error list each entry func (#7485)
* added error return in type ListEachEntryFunc

* return error if errClose

* fix fmt.Errorf

* fix return errClose

* use %w fmt.Errorf

* added entry in messege error

* add callbackErr in ListDirectoryEntries

* fix error

* add log

* clear err when the scanner stops on io.EOF, so returning err doesn’t surface EOF as a failure.

* more info in error

* add ctx to logs, error handling

* fix return eachEntryFunc

* fix

* fix log

* fix return

* fix foundationdb test s

* fix eachEntryFunc

* fix return resEachEntryFuncErr

* Update weed/filer/filer.go

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Update weed/filer/elastic/v7/elastic_store.go

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Update weed/filer/hbase/hbase_store.go

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Update weed/filer/foundationdb/foundationdb_store.go

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Update weed/filer/ydb/ydb_store.go

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* fix

* add scanErr

---------

Co-authored-by: Roman Tamarov <r.tamarov@kryptonite.ru>
Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
Co-authored-by: chrislu <chris.lu@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-25 19:35:19 -08:00
jang1leeandGitHub 4ad669b2aa Fixes files with TTL can not be read in a mounted folder (#6646) 2025-03-19 23:11:41 -07:00
jang1leeandGitHub f7f6e1158e Fixes files with TTL are not listed in a mounted folder. (#6621) 2025-03-12 06:07:41 -07:00
chrislu 9ca30e52d5 fuse mount handles ttl entries
fix https://github.com/seaweedfs/seaweedfs/issues/5527
2024-08-08 08:18:54 -07:00
0cb9ddd8ec Fix data loss: add lock for metacache (#4664)
Co-authored-by: wang wusong <wangwusong@virtaitech.com>
2023-07-11 22:23:32 -07:00
wusongandGitHub de081c0d64 [mount] fix metacache update (#4161) 2023-01-29 07:55:27 -08:00
549354e324 Fix hardlink counting (#4042)
Signed-off-by: wusong <wangwusong@virtaitech.com>

Signed-off-by: wusong <wangwusong@virtaitech.com>
Co-authored-by: wusong <wangwusong@virtaitech.com>
2022-12-08 10:50:57 -08:00
chrislu 303bd067b5 Revert "rename: delete source entry metadata only, skipping hard links"
This reverts commit 03466f955e.

fix https://github.com/seaweedfs/seaweedfs/issues/3386
2022-07-31 22:51:41 -07:00
chrislu 26dbc6c905 move to https://github.com/seaweedfs/seaweedfs 2022-07-29 00:17:28 -07:00
chrislu fcf3714443 mount: add back support for filer.path 2022-02-28 12:16:53 -08:00
chrislu 03466f955e rename: delete source entry metadata only, skipping hard links 2022-02-25 02:57:54 -08:00
chrislu dbeeda8123 listen for metadata updates 2022-02-14 01:09:31 -08:00