Files
seaweedfs/weed/mount/wfs_save.go
T
c2b2ed769b fix(mount): preserve appended chunks during stale attribute updates (#12593)
* fix(mount): preserve acknowledged chunks during attribute updates

* mount: keep the WFS field block aligned

Adding the publication-lock fields inside the aligned block made gofmt
re-indent 25 unrelated lines. Put them in their own group after
dirIdleEvict, matching the enterprise layout, so syncs do not conflict there.

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

* mount: finish attribute saves through a FUSE interrupt

saveEntryAttributes derived its RPC context from the syscall's cancel
channel, so an interrupt while the conditional UpdateEntry was at the
owner returned EIO for an update the owner then applied, and skipped the
local install and retention. Keep the metadata deadline but drop the
cancellation, like the other metadata mutations.

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

* mount: map attribute CAS ids in place instead of cloning the entry

saveEntryAttributes cloned the whole entry three times per attempt only
to rewrite uid/gid for the filer, and each clone is as large as the
chunk list. Switch the ids in place and restore the exact local values
after the call, keeping one clone. The test now checks the reply carries
the local uid.

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

* mount: compare attribute updates against the filer's own uid/gid

With -map.uid/-map.gid, a filer id that is also a mapped local id reads
back unchanged and maps forward to another filer id. The IF_ENTRY_EQUAL
expected entry then never matched, so chmod, utimes and xattr changes on
such files failed with EAGAIN after three attempts, and an update would
have rewritten the owner. Keep the filer ids read with the entry for the
ids the syscall leaves alone.

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

* fix(mount): align attribute retention with read lock order

* mount: take the handle lock before entryLock in the attribute snapshot

attributeEntrySnapshot held entryLock while it queued for the shared
handle lock. A Read holding that lock waits on entryLock, the snapshot
waits behind a queued exclusive (an invalidation or a Write), and the
exclusive waits on the Read.

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

* fix(mount): complete handle lock order and preserve explicit ID intent

* mount: re-read a refused attribute update from the file's owner filer

The filer checks the IF_ENTRY_EQUAL condition on the owner of the path's
write route key. The retry read any filer, so behind one that had not
applied the owner's latest update, every attempt sent the same stale base
and the syscall failed with EAGAIN. Read from the owner once the lock ring
is known, like lookupFilerEntry, but keep the filer's raw ids.

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:28:47 -08:00

130 lines
4.7 KiB
Go

package mount
import (
"context"
"fmt"
"syscall"
"time"
"github.com/seaweedfs/go-fuse/v2/fuse"
"google.golang.org/protobuf/proto"
"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 %s", path)
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
}
wfs.applySavedEntry(path, entry, resp)
return fuse.OK
}
// applySavedEntry publishes an acknowledged entry with filer-side uid/gid.
func (wfs *WFS) applySavedEntry(path util.FullPath, entry *filer_pb.Entry, resp *filer_pb.UpdateEntryResponse) {
parentDir, _ := path.DirAndName()
// The mutation is acknowledged; bring any open handle for this path up
// to the acknowledged state — a handle opened while this save was in
// flight holds an older entry, and advancing its version alone would
// fence out the events carrying what it lacks. A no-change update
// returns no event but still carries the log position it confirmed.
ackVersion := ackVersionTsNs(resp)
if inode, found := wfs.inodeToPath.GetInode(path); found {
if fh, fhFound := wfs.fhMap.FindFileHandle(inode); fhFound {
ackedEntry := proto.Clone(entry).(*filer_pb.Entry)
wfs.mapPbIdFromFilerToLocal(ackedEntry)
fh.installAckedEntry(ackedEntry, ackVersion, resp.GetLogSignature())
}
}
event := resp.GetMetadataEvent()
if event == nil {
event = metadataUpdateEvent(parentDir, entry)
if event != nil {
event.TsNs = ackVersion
}
}
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))
}
}
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
}