mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-15 00:09:00 +01:00
* 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>
159 lines
4.5 KiB
Go
159 lines
4.5 KiB
Go
package mount
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/go-fuse/v2/fuse"
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
// TestAttrChunkRace guards the locking around an open handle's chunk slice.
|
|
//
|
|
// With writebackCache, async upload workers append chunks to an open file
|
|
// handle's shared entry under the LockedEntry lock (FileHandle.AddChunks),
|
|
// while metadata ops compute the file size by iterating entry.Chunks. SetAttr
|
|
// and GetAttr used to read that slice without the LockedEntry lock, so a
|
|
// concurrent append that reallocated the backing array produced a torn slice
|
|
// read and a nil pointer dereference in filer.TotalSize. Run under -race.
|
|
func TestAttrChunkRace(t *testing.T) {
|
|
wfs := &WFS{
|
|
option: &Option{},
|
|
inodeToPath: NewInodeToPath(util.FullPath("/"), 0),
|
|
fhMap: NewFileHandleToInode(),
|
|
fhLockTable: util.NewLockTable[FileHandleId](),
|
|
openMtimeCache: make(map[uint64][2]int64, 8),
|
|
}
|
|
|
|
const inode = uint64(42)
|
|
fullPath := util.FullPath("/dir/sample.txt")
|
|
wfs.inodeToPath.Lookup(fullPath, 1, false, false, inode, true)
|
|
|
|
entry := &filer_pb.Entry{
|
|
Name: "sample.txt",
|
|
Attributes: &filer_pb.FuseAttributes{FileMode: 0644},
|
|
}
|
|
chunkGroup, err := filer.NewChunkGroup(nil, nil, nil, 1, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewChunkGroup: %v", err)
|
|
}
|
|
fh := &FileHandle{
|
|
fh: FileHandleId(1),
|
|
inode: inode,
|
|
wfs: wfs,
|
|
entry: &LockedEntry{Entry: entry},
|
|
entryChunkGroup: chunkGroup,
|
|
// The upload worker below represents pending local content writes,
|
|
// so metadata remains on the handle until their normal flush.
|
|
dirtyMetadata: true,
|
|
}
|
|
fh.dirtyPages = newPageWriter(fh, 1<<20)
|
|
wfs.fhMap.inode2fh[inode] = fh
|
|
wfs.fhMap.fh2inode[fh.fh] = inode
|
|
|
|
const iterations = 2000
|
|
var wg sync.WaitGroup
|
|
wg.Add(3)
|
|
|
|
// Async uploader: append chunks, reallocating the backing array.
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < iterations; i++ {
|
|
fh.AddChunks([]*filer_pb.FileChunk{{FileId: "x", Offset: int64(i), Size: 1}})
|
|
}
|
|
}()
|
|
|
|
// SetAttr: mtime-only recomputes FileSize by iterating chunks; a shrinking
|
|
// size takes the truncate path that rewrites entry.Chunks under the lock.
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < iterations; i++ {
|
|
in := &fuse.SetAttrIn{}
|
|
in.NodeId = inode
|
|
if i%2 == 0 {
|
|
in.Valid = fuse.FATTR_MTIME
|
|
in.Mtime = uint64(i)
|
|
} else {
|
|
in.Valid = fuse.FATTR_SIZE
|
|
in.Size = uint64(i % 8)
|
|
}
|
|
var out fuse.AttrOut
|
|
wfs.SetAttr(nil, in, &out)
|
|
}
|
|
}()
|
|
|
|
// GetAttr also computes FileSize by iterating chunks.
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < iterations; i++ {
|
|
in := &fuse.GetAttrIn{}
|
|
in.NodeId = inode
|
|
var out fuse.AttrOut
|
|
wfs.GetAttr(nil, in, &out)
|
|
}
|
|
}()
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
// TestReadFromChunksRace guards the read path's chunk-slice access. The read
|
|
// path holds fh.entryLock (which excludes SetAttr) but not the LockedEntry lock
|
|
// the async uploader appends under, so readFromChunks used to compute FileSize
|
|
// and walk entry.Chunks while AddChunks reallocated the slice. Run under -race.
|
|
func TestReadFromChunksRace(t *testing.T) {
|
|
wfs := &WFS{
|
|
option: &Option{},
|
|
inodeToPath: NewInodeToPath(util.FullPath("/"), 0),
|
|
fhMap: NewFileHandleToInode(),
|
|
}
|
|
|
|
const inode = uint64(42)
|
|
fullPath := util.FullPath("/dir/sample.txt")
|
|
wfs.inodeToPath.Lookup(fullPath, 1, false, false, inode, true)
|
|
|
|
// FileSize 0 forces readFromChunks down the filer.FileSize(chunks) branch.
|
|
entry := &filer_pb.Entry{
|
|
Name: "sample.txt",
|
|
Attributes: &filer_pb.FuseAttributes{FileMode: 0644},
|
|
}
|
|
chunkGroup, err := filer.NewChunkGroup(nil, nil, nil, 1, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewChunkGroup: %v", err)
|
|
}
|
|
fh := &FileHandle{
|
|
fh: FileHandleId(1),
|
|
inode: inode,
|
|
wfs: wfs,
|
|
entry: &LockedEntry{Entry: entry},
|
|
entryChunkGroup: chunkGroup,
|
|
}
|
|
fh.dirtyPages = newPageWriter(fh, 1<<20)
|
|
wfs.fhMap.inode2fh[inode] = fh
|
|
wfs.fhMap.fh2inode[fh.fh] = inode
|
|
|
|
const iterations = 2000
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < iterations; i++ {
|
|
fh.AddChunks([]*filer_pb.FileChunk{{FileId: "x", Offset: int64(i), Size: 1}})
|
|
}
|
|
}()
|
|
|
|
// A read past EOF returns before touching the volume tier, but only after
|
|
// the racy size/chunk snapshot has run.
|
|
go func() {
|
|
defer wg.Done()
|
|
buff := make([]byte, 16)
|
|
for i := 0; i < iterations; i++ {
|
|
fh.readFromChunks(buff, 1<<62)
|
|
}
|
|
}()
|
|
|
|
wg.Wait()
|
|
}
|