Commit Graph
5 Commits
Author SHA1 Message Date
d7ae08ecad fix(mount): refuse stale whole-entry content flushes (#12594)
* fix(mount): condition content flushes on the observed entry

* mount: stop retrying a flush the filer refused on its condition

The filer reports a failed CreateEntry condition as FilerError_PRECONDITION_FAILED
in the response, which surfaces as filer_pb.ErrPreconditionFailed rather than a
FailedPrecondition status. The flush retry loop treated it as transient and
replayed the refused flush three more times over 7 s before returning EIO.

The test filer now answers like the real one.

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

* mount: take the handle base from the entry the filer stored

After an acknowledged flush the handle base was the requested entry, but the
filer stores it with a fresh commit_ts_ns (and fills crtime, inode, atime), so
the next IF_ENTRY_EQUAL flush on the same handle never matched: write, fsync,
write, fsync returned EIO, as did close after a periodic metadata flush and the
first flush after an eager create. Use the stored entry from the
acknowledgement's metadata event.

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

* mount: condition the periodic metadata flush like the close flush

The periodic metadata flush of an open, dirty handle sent the same whole entry
through an unconditional CreateEntry, so a file held open past the flush
interval still overwrote a peer's acknowledged append, and the close flush then
matched the overwritten state. Both paths now publish through one helper that
carries the handle's write condition.

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

* mount: republish a refused flush once when the stored entry is unchanged

A handle's base can differ from the stored entry without any peer write: a
rename while the file is open restamps the entry under its new name, and with
-map.uid a filer id that is also a mapped local id does not map back to itself.
Each made every flush of the handle fail with EIO. On a miss, read the stored
entry and, if it still holds the observed state apart from name, commit stamp,
id encoding and derived size, retry once with that entry as the condition.

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

* mount: take the filer's entry once a write open holds the DLM lock

The lock orders this writer after the previous holder, but the holder's
flush may not have reached the local cache yet, so the handle's base
named the entry from before the wait and the conditional flush refused.

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

* mount: share the open-existing path of Create

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

* mount: open the file a previous DLM holder created during a deferred create

A deferred create never asks the filer, so a create that waited for the
lock went on to publish over a file the previous holder had just made,
and its flush expecting no entry was refused.

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

* master: send the lock ring snapshot to mounts

A mount running -dlm is a lock client like the S3 gateway, and needs the
ring on connect to reach a file's owner filer before the next membership
change is broadcast.

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

* mount: follow the masters' lock ring under -dlm

The lock client only knew the seed filer, so every lock took the forward
hop and the mount had no way to tell which filer owns a file. Subscribe to
the masters named by the filer, like the S3 gateway, and wait briefly for
the first ring before serving.

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

* mount: read the DLM-ordered entry from the file's owner filer

The filer checks a flush's condition on the owner of the path's write
route key, and another filer may not have applied the previous holder's
flush yet. Look the entry up on that owner once the ring is known.

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

---------

Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:15:08 -08:00
Chris Lu 5553e7b876 mount: surface ENOSPC instead of endless waiting when the cluster is full (#10341)
When every volume is full, a chunk upload fails only after the assign
retry budget, the failed chunk's data is dropped, and the mount kept
accepting writes anyway. cp would crawl for hours pushing the rest of
the file through a pipeline that could not persist it, and only close()
reported an error - a generic EIO.

Poison the file handle on the first failed chunk upload so subsequent
writes fail immediately, and map "no writable volumes" / "no free
volumes" upload errors to ENOSPC so the writing process aborts with
"No space left on device". Also guard lastErr with a mutex: it was
written concurrently by uploader goroutines, and keep the first error
so later failures do not mask the root cause.
2026-07-15 22:01:19 -07:00
Chris Lu f1d5f31a93 fix(mount): retry saveEntry on transient filer errors; stop mismapping Canceled to EIO (#9141)
* fix(mount): retry saveEntry on transient filer errors, stop mismapping Canceled to EIO

When the mount's gRPC connection to the filer flaps (e.g. a transient
restart or network blip), every in-flight setattr/utimes/chmod/xattr/
rename-driven saveEntry returns "code = Canceled desc = grpc: the client
connection is closing" at the same instant. Two bugs in saveEntry then
turned each of those into a hard EIO for the user:

1. The error was wrapped with fmt.Errorf(... %v ...) before being passed
   to grpcErrorToFuseStatus. %v stringifies the status, so
   status.FromError could no longer unwrap the gRPC code and the
   Canceled→ETIMEDOUT branch in the classifier never fired; every
   Canceled error fell through to the default EIO.

2. saveEntry issued a single streamUpdateEntry call with no retry,
   unlike doFlush which already wraps its CreateEntry in
   retryMetadataFlush. One stream flap therefore propagated straight to
   the FUSE caller instead of being ridden out across the 4-attempt /
   ~7s backoff window.

Wrap the UpdateEntry call in retryMetadataFlush (matching doFlush and
completeAsyncFlush) and switch the wrap verb to %w so the classifier
can still see the gRPC code. This recovers transient closes silently
and, if retries are exhausted, returns ETIMEDOUT instead of EIO.

Reported by rclone users in #9139 where a large concurrent copy
(hundreds of .partial uploads per filer flap) surfaced as walls of EIOs
because each .partial rename's post-setattr hit saveEntry at the worst
possible moment.

* mount: skip saveEntry retries on permanent filer errors

Address gemini-code-assist review on #9141: blindly retrying every
UpdateEntry failure with exponential backoff means interactive FUSE ops
like chmod/utimes/xattr can hang for ~7s before surfacing clearly
permanent errors (NotFound, PermissionDenied, InvalidArgument, etc.).

Introduce retryMetadataFlushIf, a variant of retryMetadataFlush that
accepts a shouldRetry predicate, and an isRetryableFilerError classifier
that short-circuits on a conservative whitelist of terminal gRPC codes.
Transient errors (Canceled / Unavailable / DeadlineExceeded /
ResourceExhausted / Internal) and non-gRPC errors still retry, so the
original fix for #9139 (rclone EIO burst during filer connection
flaps) is preserved.
2026-04-20 00:31:37 -07:00
Chris Lu d4ecfaeda7 Enable writeback_cache and async_dio FUSE options (#7980)
* Enable writeback_cache and async_dio FUSE options

Fixes #7978

- Update mount_std.go to use EnableWriteback and EnableAsyncDio from go-fuse
- Add go.mod replace directive to use local go-fuse with capability support
- Remove temporary workaround that disabled these options

This enables proper FUSE kernel capability negotiation for writeback cache
and async direct I/O, improving performance for small writes and concurrent
direct I/O operations.

* Address PR review comments

- Remove redundant nil checks for writebackCache and asyncDio flags
- Update go.mod replace directive to use seaweedfs/go-fuse fork instead of local path

* Add TODO comment for go.mod replace directive

The replace directive must use a local path until seaweedfs/go-fuse#1 is merged.
After merge, this should be updated to use the proper version.

* Use seaweedfs/go-fuse v2.9.0 instead of local repository

Replace local path with seaweedfs/go-fuse v2.9.0 fork which includes
the writeback_cache and async_dio capability support.

* Use github.com/seaweedfs/go-fuse/v2 directly without replace directive

- Updated all imports to use github.com/seaweedfs/go-fuse/v2
- Removed replace directive from go.mod
- Using seaweedfs/go-fuse v2.0.0-20260106181308-87f90219ce09 which includes:
  * writeback_cache and async_dio support
  * Corrected module path

* Update to seaweedfs/go-fuse v2.9.1

Use v2.9.1 tag which includes the corrected module path
(github.com/seaweedfs/go-fuse/v2) along with writeback_cache
and async_dio support.
2026-01-06 10:50:54 -08:00
Chris Lu 998bcf2b3f classify grpc errors 2025-12-31 13:40:14 -08:00