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, } 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) 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() }