mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-07-24 09:02:59 +00:00
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.
97 lines
2.8 KiB
Go
97 lines
2.8 KiB
Go
package mount
|
|
|
|
import (
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/seaweedfs/go-fuse/v2/fuse"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
func grpcErrorToFuseStatus(err error) fuse.Status {
|
|
if err == nil {
|
|
return fuse.OK
|
|
}
|
|
|
|
// Unpack error for inspection
|
|
if s, ok := status.FromError(err); ok {
|
|
switch s.Code() {
|
|
case codes.OK:
|
|
return fuse.OK
|
|
case codes.Canceled, codes.DeadlineExceeded:
|
|
return fuse.Status(syscall.ETIMEDOUT)
|
|
case codes.Unavailable:
|
|
return fuse.Status(syscall.EAGAIN)
|
|
case codes.ResourceExhausted:
|
|
return fuse.Status(syscall.EAGAIN) // Or syscall.ENOSPC
|
|
case codes.PermissionDenied:
|
|
return fuse.Status(syscall.EACCES)
|
|
case codes.Unauthenticated:
|
|
return fuse.Status(syscall.EPERM)
|
|
case codes.NotFound:
|
|
return fuse.ENOENT
|
|
case codes.AlreadyExists:
|
|
return fuse.Status(syscall.EEXIST)
|
|
case codes.InvalidArgument:
|
|
return fuse.EINVAL
|
|
}
|
|
}
|
|
|
|
// String matching for errors that don't have proper gRPC codes but are known
|
|
errStr := err.Error()
|
|
if strings.Contains(errStr, "transport") {
|
|
return fuse.Status(syscall.EAGAIN)
|
|
}
|
|
// Add other string matches if necessary
|
|
|
|
return fuse.EIO
|
|
}
|
|
|
|
// isStorageFullError reports whether a chunk upload failed because the cluster
|
|
// has no writable volume left (disks full and volume growth failed). The
|
|
// master's message reaches the mount as plain text inside
|
|
// AssignVolumeResponse.Error, so match on text rather than a status code.
|
|
func isStorageFullError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
errStr := strings.ToLower(err.Error())
|
|
return strings.Contains(errStr, "no writable volumes") ||
|
|
strings.Contains(errStr, "no free volumes")
|
|
}
|
|
|
|
// writeErrorToFuseStatus maps a dirty-page write/flush failure to the status
|
|
// returned to the kernel: ENOSPC when the cluster is out of space, EIO otherwise.
|
|
func writeErrorToFuseStatus(err error) fuse.Status {
|
|
if isStorageFullError(err) {
|
|
return fuse.Status(syscall.ENOSPC)
|
|
}
|
|
return fuse.EIO
|
|
}
|
|
|
|
// isRetryableFilerError reports whether a filer RPC error looks transient
|
|
// enough to retry. It takes a conservative whitelist approach: only errors
|
|
// that clearly describe a permanent application-level failure
|
|
// (NotFound/AlreadyExists/InvalidArgument/PermissionDenied/Unauthenticated/
|
|
// FailedPrecondition) short-circuit the retry loop. Everything else —
|
|
// transport errors, Canceled/Unavailable/ResourceExhausted, or errors with no
|
|
// gRPC status — is treated as potentially transient and retried.
|
|
func isRetryableFilerError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
if s, ok := status.FromError(err); ok {
|
|
switch s.Code() {
|
|
case codes.NotFound,
|
|
codes.AlreadyExists,
|
|
codes.InvalidArgument,
|
|
codes.PermissionDenied,
|
|
codes.Unauthenticated,
|
|
codes.FailedPrecondition:
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|