mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-15 16:28:30 +01:00
* mount: fail the write when the swap file chunk cannot be created #7957 added a nil check after NewSwapFileChunk so a failed swap file creation returns an error instead of panicking. The result was assigned to pageChunk, a PageChunk interface, before the check, and an interface holding a nil *SwapFileChunk is not nil, so the check never fired: SaveDataAt stored the nil chunk and panicked in WriteDataAt. Check the *SwapFileChunk before converting it. The new test points the swap directory below a regular file; on master it panics in SwapFileChunk.WriteDataAt. Signed-off-by: anant <170962632+antcybersec@users.noreply.github.com> * mount: grow the file only after its write is buffered Write set the new size, times, pending append offset and uncommitted bytes before AddPage. When AddPage failed (now that a swap file chunk that cannot be created returns an error), the write was reported as failed but the larger size stayed on the handle's entry, so the next flush published it with a hole where the data should be. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: report a partly buffered write as a short write PageWriter.AddPage fills one chunk at a time, so when a later chunk cannot be created the earlier chunks already hold their bytes. Return how much was buffered, and let Write grow the size and times over that prefix and report it, instead of failing the whole write while the prefix still reaches the next flush. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: report the bytes a failed copy_file_range already buffered A streaming copy whose page fails partway returned an error even though earlier pages, or the start of the failing one, were already buffered and would reach the next flush with the destination size left short. Count the buffered bytes, grow the destination over them, and return them as a short copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: free the copy test's destination pages The buffered memory chunk counts toward the process-wide memory chunk ceiling, so repeated runs of the test pushed later ones onto the unusable swap directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: count only the bytes a swap chunk stored A swap chunk whose file write fails stores a prefix or nothing, but SaveDataAt returned no error and AddPage counted the whole span as buffered. SaveDataAt now reports the short write, and AddPage passes the stored count up so a write is never acknowledged past it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: fail a partly buffered writeback write Kernel writeback ignores the count in a WRITE reply, so a short write there drops the unbuffered rest without an error. A write flagged as page-cache writeback now keeps its buffered prefix but returns the error, which the kernel reports on fsync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Signed-off-by: anant <170962632+antcybersec@users.noreply.github.com> Co-authored-by: anant <170962632+antcybersec@users.noreply.github.com> Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
141 lines
4.1 KiB
Go
141 lines
4.1 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
|
|
}
|
|
|
|
// Size and times cover only the bytes buffered, or the next flush
|
|
// publishes a hole where the data should be. A partly buffered write
|
|
// is reported as a short write, or as an error to kernel writeback,
|
|
// which ignores the count.
|
|
offset := int64(in.Offset)
|
|
buffered, err := fh.dirtyPages.AddPage(offset, data, fh.dirtyPages.writerPattern.IsSequentialMode(), tsNs)
|
|
if err != nil {
|
|
glog.Errorf("AddPage error: %v", err)
|
|
if buffered == 0 {
|
|
return 0, writeErrorToFuseStatus(err)
|
|
}
|
|
data = data[:buffered]
|
|
}
|
|
fh.pageWrites.Add(1)
|
|
|
|
// 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
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
if err != nil && in.WriteFlags&fuse.WRITE_CACHE != 0 {
|
|
return 0, writeErrorToFuseStatus(err)
|
|
}
|
|
return written, fuse.OK
|
|
}
|