mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-12 09:30:46 +02:00
* operation: bound AssignVolume with a deadline AssignVolume ran on context.Background(), so when the filer is overwhelmed the RPC could block indefinitely and wedge every caller holding the connection. Give it a 30s deadline so a stuck assign fails and the caller's retry/error path runs instead of hanging forever. * mount: abort flush when the FUSE request is interrupted On close(), a killed process blocks in fuse_flush waiting for the mount to answer. doFlush ran its metadata CreateEntry on context.Background() and ignored the kernel interrupt channel, so against an overwhelmed filer the flush never completed and the process stayed in uninterruptible sleep -- making the pod un-killable. Derive a context from the FUSE cancel channel in Flush/Fsync and thread it through doFlush -> flushMetadataToFiler -> streamCreateEntry; the retry loop stops as soon as the context is cancelled. Release and the pre-rename flush keep a non-cancellable context since they must finish regardless. * operation: harden the AssignVolume timeout test Make the test double's signal send non-blocking and bound the receive with a timeout so a regression can't wedge the test instead of failing it.
105 lines
3.7 KiB
Go
105 lines
3.7 KiB
Go
package mount
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/go-fuse/v2/fuse"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
func (wfs *WFS) saveEntry(path util.FullPath, entry *filer_pb.Entry) (code fuse.Status) {
|
|
|
|
parentDir, _ := path.DirAndName()
|
|
|
|
wfs.mapPbIdFromLocalToFiler(entry)
|
|
defer wfs.mapPbIdFromFilerToLocal(entry)
|
|
|
|
request := &filer_pb.UpdateEntryRequest{
|
|
Directory: parentDir,
|
|
Entry: entry,
|
|
Signatures: []int32{wfs.signature},
|
|
}
|
|
|
|
glog.V(1).Infof("save entry: %v", request)
|
|
|
|
var resp *filer_pb.UpdateEntryResponse
|
|
err := retryMetadataFlushIf(context.Background(), func() error {
|
|
var callErr error
|
|
resp, callErr = wfs.streamUpdateEntry(context.Background(), request)
|
|
return callErr
|
|
}, isRetryableFilerError, func(nextAttempt, totalAttempts int, backoff time.Duration, err error) {
|
|
glog.Warningf("saveEntry %s: retrying UpdateEntry (attempt %d/%d) after %v: %v",
|
|
path, nextAttempt, totalAttempts, backoff, err)
|
|
})
|
|
|
|
if err != nil {
|
|
// Wrap with %w so grpcErrorToFuseStatus can still unwrap the gRPC status
|
|
// (e.g. codes.Canceled → ETIMEDOUT). Using %v would stringify the error and
|
|
// status.FromError would fall through to the default EIO.
|
|
err = fmt.Errorf("UpdateEntry dir %s: %w", path, err)
|
|
fuseStatus := grpcErrorToFuseStatus(err)
|
|
if fuseStatus == fuse.EIO {
|
|
glog.Errorf("saveEntry failed for %s: %v (returning EIO)", path, err)
|
|
} else {
|
|
glog.V(1).Infof("saveEntry failed for %s: %v (returning %v)", path, err, fuseStatus)
|
|
}
|
|
return fuseStatus
|
|
}
|
|
|
|
event := resp.GetMetadataEvent()
|
|
if event == nil {
|
|
event = metadataUpdateEvent(parentDir, entry)
|
|
}
|
|
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil {
|
|
glog.Warningf("saveEntry %s: best-effort metadata apply failed: %v", path, applyErr)
|
|
wfs.inodeToPath.InvalidateChildrenCache(util.FullPath(parentDir))
|
|
}
|
|
|
|
return fuse.OK
|
|
}
|
|
|
|
func (wfs *WFS) mapPbIdFromFilerToLocal(entry *filer_pb.Entry) {
|
|
if entry.Attributes == nil {
|
|
return
|
|
}
|
|
entry.Attributes.Uid, entry.Attributes.Gid = wfs.option.UidGidMapper.FilerToLocal(entry.Attributes.Uid, entry.Attributes.Gid)
|
|
}
|
|
func (wfs *WFS) mapPbIdFromLocalToFiler(entry *filer_pb.Entry) {
|
|
if entry.Attributes == nil {
|
|
return
|
|
}
|
|
entry.Attributes.Uid, entry.Attributes.Gid = wfs.option.UidGidMapper.LocalToFiler(entry.Attributes.Uid, entry.Attributes.Gid)
|
|
}
|
|
|
|
// sanitizeFuseName scrubs a name arriving from the kernel before it is placed
|
|
// in a proto string field. Linux (and macOS) pass raw bytes for filenames;
|
|
// apps like GNOME Trash produce partial files whose names contain binary
|
|
// payloads. Proto3 `string` fields require valid UTF-8, so an unsanitized
|
|
// name causes gRPC to fail the whole AssignVolume / CreateEntry / DeleteEntry
|
|
// RPC with "grpc: error while marshaling: string field contains invalid
|
|
// UTF-8", which surfaces to userspace as EIO. Sanitizing at every FUSE
|
|
// boundary keeps filer RPCs marshalable and prevents a single ill-named file
|
|
// from poisoning the shared gRPC channel for every other in-flight request.
|
|
//
|
|
// Delegates to util.SanitizeUTF8Name so the replacement character is chosen
|
|
// in exactly one place across the codebase.
|
|
func sanitizeFuseName(name string) string {
|
|
return util.SanitizeUTF8Name(name)
|
|
}
|
|
|
|
func checkName(name string) (string, fuse.Status) {
|
|
name = sanitizeFuseName(name)
|
|
// The Linux FUSE kernel module enforces NAME_MAX=255 at the VFS layer.
|
|
// Return ENAMETOOLONG early to avoid creating entries that cannot be
|
|
// looked up via normal syscalls (stat, chmod, etc.).
|
|
if len(name) > 255 {
|
|
return name, fuse.Status(syscall.ENAMETOOLONG)
|
|
}
|
|
return name, fuse.OK
|
|
}
|