mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-15 08:18:42 +01:00
* mount: fence a dropped handle only when it holds unpublished content dirtyMetadata is also set by chmod, xattr and fallocate, so a failed metadata-only close refused every later write of the inode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: fence the inode before its last handle leaves the handle map A reopen between the map removal and the fence could publish at the stale kernel EOF. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: fence a failed metadata flush only when the handle holds content A refused synchronous flush or an abandoned writeback metadata flush of a handle with only chmod, xattr or fallocate changes fenced the inode, so later writes were refused until FORGET. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: count a size change on an open handle as unpublished content A truncate or extend through an open handle changes the entry's size and chunks, but it set only dirtyMetadata. A failed close then skipped the final-release fence, so a reopen before FORGET could publish against the untruncated entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: mark copied content dirty before each page of a streaming copy A copy that failed or was cancelled after earlier pages were accepted left dirtyContent false, so the final-release fence skipped that content. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: mark copied content dirty only after a page is accepted A copy whose first page is rejected leaves the destination unchanged and the kernel EOF where it was, so fencing the inode on release only blocked later writes until FORGET. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: skip the server-side copy while the destination holds unpublished content A whole-file server copy clears dirtyContent when it commits, so a page a cancelled streaming copy left behind would lose its release fence. Fall back to the streaming copy instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: skip the server-side copy while the source holds unpublished content The filer copies the stored source, so pages still pending on the source handle would be missing from the destination. The streaming copy reads them through the dirty pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: check the publication fence before a server-side whole-file copy The filer copy publishes the destination without going through the flush-time guard, so a fenced writeback inode could still take new content before FORGET. Fall back to the streaming copy, whose flush is guarded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
131 lines
3.8 KiB
Go
131 lines
3.8 KiB
Go
package mount
|
|
|
|
import (
|
|
"net/http"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/go-fuse/v2/fuse"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
// isAppendWrite honors current F_SETFL flags; adapters without flags use the open.
|
|
func (wfs *WFS) isAppendWrite(in *fuse.WriteIn) bool {
|
|
if in.Flags != 0 {
|
|
return in.Flags&syscall.O_APPEND != 0
|
|
}
|
|
return wfs.fhMap.isAppendHandle(FileHandleId(in.Fh))
|
|
}
|
|
|
|
/**
|
|
* Write data
|
|
*
|
|
* Write should return exactly the number of bytes requested
|
|
* except on error. An exception to this is when the file has
|
|
* been opened in 'direct_io' mode, in which case the return value
|
|
* of the write system call will reflect the return value of this
|
|
* operation.
|
|
*
|
|
* Unless FUSE_CAP_HANDLE_KILLPRIV is disabled, this method is
|
|
* expected to reset the setuid and setgid bits.
|
|
*
|
|
* fi->fh will contain the value set by the open method, or will
|
|
* be undefined if the open method didn't set any value.
|
|
*
|
|
* Valid replies:
|
|
* fuse_reply_write
|
|
* fuse_reply_err
|
|
*
|
|
* @param req request handle
|
|
* @param ino the inode number
|
|
* @param buf data to write
|
|
* @param size number of bytes to write
|
|
* @param off offset to write to
|
|
* @param fi file information
|
|
*/
|
|
func (wfs *WFS) Write(cancel <-chan struct{}, in *fuse.WriteIn, data []byte) (written uint32, code fuse.Status) {
|
|
|
|
// Check quota including uncommitted writes for real-time enforcement
|
|
if wfs.IsOverQuotaWithUncommitted() {
|
|
return 0, fuse.Status(syscall.ENOSPC)
|
|
}
|
|
|
|
fh := wfs.GetHandle(FileHandleId(in.Fh))
|
|
if fh == nil {
|
|
return 0, fuse.ENOENT
|
|
}
|
|
|
|
fh.dirtyPages.writerPattern.MonitorWriteAt(int64(in.Offset), int(in.Size))
|
|
|
|
tsNs := time.Now().UnixNano()
|
|
|
|
fhActiveLock := fh.wfs.fhLockTable.AcquireLock("Write", fh.fh, util.ExclusiveLock)
|
|
defer fh.wfs.fhLockTable.ReleaseLock(fh.fh, fhActiveLock)
|
|
|
|
entry := fh.GetEntry()
|
|
if entry == nil {
|
|
return 0, fuse.OK
|
|
}
|
|
|
|
// Only an append past all locally owned bytes needs the stored EOF guard.
|
|
if in.Offset > entry.Attributes.FileSize && wfs.isAppendWrite(in) {
|
|
if !fh.pendingAppend || in.Offset < fh.pendingAppendOffset {
|
|
fh.pendingAppendOffset = in.Offset
|
|
}
|
|
fh.pendingAppend = true
|
|
}
|
|
entry.Content = nil
|
|
offset := int64(in.Offset)
|
|
oldFileSize := int64(entry.Attributes.FileSize)
|
|
newFileSize := max(offset+int64(len(data)), oldFileSize)
|
|
entry.Attributes.FileSize = uint64(newFileSize)
|
|
|
|
// POSIX: writes update mtime and ctime. Stamp the entry now so the
|
|
// flush path persists the write time rather than overwriting any
|
|
// user-set mtime (e.g., utimes/touch -m -d) at flush time.
|
|
writeNow := time.Unix(0, tsNs)
|
|
entry.Attributes.Mtime = writeNow.Unix()
|
|
entry.Attributes.MtimeNs = int32(writeNow.Nanosecond())
|
|
entry.Attributes.Ctime = writeNow.Unix()
|
|
entry.Attributes.CtimeNs = int32(writeNow.Nanosecond())
|
|
|
|
// Track uncommitted bytes for real-time quota enforcement.
|
|
// Only count the new bytes being added beyond the current file size.
|
|
if newFileSize > oldFileSize {
|
|
wfs.AddUncommittedBytes(newFileSize - oldFileSize)
|
|
}
|
|
|
|
// glog.V(4).Infof("%v write [%d,%d) %d", fh.f.fullpath(), req.Offset, req.Offset+int64(len(req.Data)), len(req.Data))
|
|
|
|
if err := fh.dirtyPages.AddPage(offset, data, fh.dirtyPages.writerPattern.IsSequentialMode(), tsNs); err != nil {
|
|
glog.Errorf("AddPage error: %v", err)
|
|
return 0, writeErrorToFuseStatus(err)
|
|
}
|
|
|
|
written = uint32(len(data))
|
|
|
|
if offset == 0 {
|
|
// detect mime type
|
|
fh.contentType = http.DetectContentType(data)
|
|
}
|
|
|
|
fh.dirtyMetadata = true
|
|
fh.dirtyContent = true
|
|
|
|
// Invalidate the mtime cache so the next Open will not set FOPEN_KEEP_CACHE.
|
|
wfs.invalidateOpenMtimeCache(in.NodeId)
|
|
|
|
// POSIX: clear SUID/SGID bits on write by non-root users.
|
|
if in.Uid != 0 {
|
|
entry.Attributes.FileMode &^= 0o6000
|
|
}
|
|
|
|
if IsDebugFileReadWrite {
|
|
// print("+")
|
|
fh.mirrorFile.WriteAt(data, offset)
|
|
}
|
|
|
|
return written, fuse.OK
|
|
}
|