mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-28 16:00:37 +01:00
* mount: hold the entry lock while reading chunk size in GetAttr/SetAttr Async upload workers append chunks to an open handle's shared entry under the LockedEntry lock (FileHandle.AddChunks), but GetAttr and SetAttr computed FileSize by iterating entry.Chunks without taking it. A concurrent append that reallocated the backing array tore the slice read and crashed in filer.TotalSize. Surfaces with -writebackCache, where handles stay open and flush asynchronously while metadata ops keep arriving. Take the LockedEntry lock for those reads (and SetAttr's truncate rewrite). * mount: re-read entry under the lock in GetAttr/SetAttr If SetEntry swapped the handle's entry pointer between maybeReadEntry and the lock acquisition, the old pointer is orphaned. Re-read fh.entry.Entry under the lock so SetAttr mutates the live entry instead of losing the update, and GetAttr reports the current one. * mount: cover the truncate path in TestAttrChunkRace Alternate SetAttr between mtime-only and a shrinking size so the test also exercises the entry.Chunks rewrite under fh.entry.Lock, not just the read-side size walk. * mount: snapshot chunks under the entry lock on the read path readFromChunks holds fh.entryLock (excludes SetAttr) but not the LockedEntry lock the async uploader appends under, so IsInRemoteOnly, the FileSize fallback, and the RDMA/peer chunk walks read entry.Chunks while AddChunks reallocated it — the same torn-slice crash as GetAttr/SetAttr. Snapshot size, inline content, and the chunk list under a brief LockedEntry RLock, then hand the snapshot to the RDMA/peer helpers instead of holding the lock across network I/O. The captured slice stays valid: append never mutates the old backing array, and truncate is excluded by the fh.entryLock.
153 lines
4.2 KiB
Go
153 lines
4.2 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(),
|
|
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)
|
|
if err != nil {
|
|
t.Fatalf("NewChunkGroup: %v", err)
|
|
}
|
|
fh := &FileHandle{
|
|
fh: FileHandleId(1),
|
|
inode: inode,
|
|
wfs: wfs,
|
|
entry: &LockedEntry{Entry: entry},
|
|
entryChunkGroup: chunkGroup,
|
|
}
|
|
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)
|
|
if err != nil {
|
|
t.Fatalf("NewChunkGroup: %v", err)
|
|
}
|
|
fh := &FileHandle{
|
|
fh: FileHandleId(1),
|
|
inode: inode,
|
|
wfs: wfs,
|
|
entry: &LockedEntry{Entry: entry},
|
|
entryChunkGroup: chunkGroup,
|
|
}
|
|
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()
|
|
}
|