mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-13 09:38:36 +02:00
* mount: propagate request context through metadata lookups The FUSE interrupt channel never reached the filer RPC: lookupEntry ran its LookupEntry under context.Background(), so a stalled filer parked ops like ListXAttr forever even after the kernel gave up on them. Thread a context from each op's cancel channel through maybeReadEntry, maybeLoadEntry, lookupEntry, ensureDirectoryVisited and waitForPendingAsyncFlush; the existing withFilerClient attempt bound turns it into both an interrupt and a deadline. A caller cancellation now surfaces as EINTR instead of waiting out the RPC. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: reuse ctxFromCancel for the read path's interrupt bridge Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: regression tests for canceled and stalled filer lookups Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: thread ctx through AcquireHandle Open, Create and Mknod all carry the request's cancel channel; let it bound the pending-flush wait and metadata load inside AcquireHandle too. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: keep flush waits and write sequences uninterruptible The cancel-derived context must only cover metadata reads: an interrupted Unlink or Rename that skips the pending-flush wait can race the async write back into existence, and Link/Rename mutations that run under a canceled context abort mid-sequence, strand the rollback, and leave wrong hard-link counts. Restore the blocking flush wait and run each op's write phase on context.Background(). Rmdir and Unlink also propagate EINTR from the parent lookup instead of proceeding past a permission check the interrupt prevented. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: interrupt shared directory builds and directory reads Waiters behind another request's singleflight directory build blocked until it finished even when interrupted; switch the wait to DoChan so the request context can abandon it. ReadDir paths now carry the request context through EnsureListingFresh, cache listings and direct reads, and map its cancellation to EINTR rather than EIO. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: assert exact statuses in filer-stall regression tests Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: let read-side flush waits release on interrupt waitForPendingAsyncFlush reports whether the flush actually finished: lookupEntry and AcquireHandle return EINTR when the request is abandoned mid-wait, while Unlink and Rename keep waiting on context.Background() so the delete/rename still sees the flushed state. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: propagate interrupt through remaining lookup checks Rename's source and destination permission lookups now return EINTR on interrupt instead of skipping checkStickyBit or masking an existing target, and the readdir preload visit maps cancellation to EINTR like the other branches. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: fail closed on errored rename prerequisite lookups * mount: stop rmdir on interrupted target lookup, fail closed on dir lookup errors --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
234 lines
6.8 KiB
Go
234 lines
6.8 KiB
Go
package mount
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/go-fuse/v2/fuse"
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/mount/meta_cache"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
const benchDirEntryCount = 200000
|
|
|
|
// benchSink drains a readdir the way a front end does. sinkLimit is how many
|
|
// entries one round accepts, standing in for the kernel reply buffer or the
|
|
// WinFsp adapter's batch.
|
|
type benchSink struct {
|
|
plus bool
|
|
takesRef bool
|
|
sinkLimit int
|
|
count int
|
|
inodes []uint64
|
|
lastOff uint64
|
|
attrs []fuse.EntryOut
|
|
}
|
|
|
|
func (s *benchSink) reset() {
|
|
s.count = 0
|
|
s.inodes = s.inodes[:0]
|
|
s.attrs = s.attrs[:0]
|
|
}
|
|
|
|
func (s *benchSink) AddEntry(entry fuse.DirEntry) bool {
|
|
if s.count >= s.sinkLimit {
|
|
return false
|
|
}
|
|
s.count++
|
|
s.lastOff = entry.Off
|
|
s.inodes = append(s.inodes, entry.Ino)
|
|
return true
|
|
}
|
|
|
|
func (s *benchSink) AddEntryPlus(entry fuse.DirEntry) *fuse.EntryOut {
|
|
if s.count >= s.sinkLimit {
|
|
return nil
|
|
}
|
|
s.count++
|
|
s.lastOff = entry.Off
|
|
s.inodes = append(s.inodes, entry.Ino)
|
|
s.attrs = append(s.attrs, fuse.EntryOut{})
|
|
return &s.attrs[len(s.attrs)-1]
|
|
}
|
|
|
|
func (s *benchSink) TakesLookupRef() bool { return s.takesRef }
|
|
|
|
func inodeTableSize(i *InodeToPath) int {
|
|
i.RLock()
|
|
defer i.RUnlock()
|
|
return len(i.inode2path)
|
|
}
|
|
|
|
func newBenchWFS(tb testing.TB, dir util.FullPath, n int) *WFS {
|
|
tb.Helper()
|
|
|
|
uidGidMapper, err := meta_cache.NewUidGidMapper("", "")
|
|
if err != nil {
|
|
tb.Fatalf("uid/gid mapper: %v", err)
|
|
}
|
|
|
|
root := util.FullPath("/")
|
|
option := &Option{
|
|
ChunkSizeLimit: 1024,
|
|
ConcurrentReaders: 1,
|
|
VolumeServerAccess: "filerProxy",
|
|
FilerAddresses: []pb.ServerAddress{pb.NewServerAddressWithGrpcPort("127.0.0.1:1", 1)},
|
|
GrpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
FilerMountRootPath: "/",
|
|
MountUid: 99,
|
|
MountGid: 100,
|
|
MountMode: 0o777,
|
|
MountMtime: time.Now(),
|
|
MountCtime: time.Now(),
|
|
UidGidMapper: uidGidMapper,
|
|
}
|
|
|
|
wfs := &WFS{
|
|
option: option,
|
|
signature: 1,
|
|
inodeToPath: NewInodeToPath(root, 0),
|
|
fhMap: NewFileHandleToInode(),
|
|
dhMap: NewDirectoryHandleToInode(),
|
|
fhLockTable: util.NewLockTable[FileHandleId](),
|
|
hardLinkLockTable: util.NewLockTable[string](),
|
|
}
|
|
wfs.metaCache = meta_cache.NewMetaCache(
|
|
filepath.Join(tb.TempDir(), "meta"),
|
|
uidGidMapper,
|
|
root,
|
|
false,
|
|
func(path util.FullPath) { wfs.inodeToPath.MarkChildrenCached(path) },
|
|
func(path util.FullPath) bool { return wfs.inodeToPath.IsChildrenCached(path) },
|
|
func(meta_cache.EntryInvalidation) {},
|
|
nil,
|
|
)
|
|
tb.Cleanup(wfs.metaCache.Shutdown)
|
|
|
|
now := time.Now()
|
|
ctx := context.Background()
|
|
if err := wfs.metaCache.InsertEntry(ctx, &filer.Entry{
|
|
FullPath: dir,
|
|
Attr: filer.Attr{Mode: os.ModeDir | 0o755, Mtime: now, Crtime: now, Uid: 99, Gid: 100},
|
|
}, 0); err != nil {
|
|
tb.Fatalf("insert dir: %v", err)
|
|
}
|
|
for i := 0; i < n; i++ {
|
|
child := dir.Child(fmt.Sprintf("image-%08d.jpg", i))
|
|
if err := wfs.metaCache.InsertEntry(ctx, &filer.Entry{
|
|
FullPath: child,
|
|
// The filer stamps an inode on every entry it stores, so a listing
|
|
// arrives with one and never has to derive its own.
|
|
Attr: filer.Attr{Mode: 0o644, Mtime: now, Crtime: now, Uid: 99, Gid: 100, FileSize: 4 << 20, Inode: child.AsInode(now.Unix())},
|
|
// A real file has chunks, and building them is most of what decoding
|
|
// an entry costs.
|
|
// No FileId: BeforeEntrySerialization reparses that legacy string
|
|
// over Fid on the way in, which would make every entry's chunk
|
|
// byte-identical instead of varying per file.
|
|
Chunks: []*filer_pb.FileChunk{{
|
|
Size: 4 << 20, ModifiedTsNs: now.UnixNano(),
|
|
ETag: "1a2b3c4d5e6f7890",
|
|
Fid: &filer_pb.FileId{VolumeId: 3, FileKey: uint64(i), Cookie: 0x1637037d},
|
|
}},
|
|
}, 0); err != nil {
|
|
tb.Fatalf("insert entry %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
// Mark the tree cached so the listing is served from the meta cache and no
|
|
// filer client is dialled.
|
|
wfs.inodeToPath.MarkChildrenCached(root)
|
|
wfs.inodeToPath.Lookup(dir, now.Unix(), true, false, 0, true)
|
|
wfs.inodeToPath.MarkChildrenCached(dir)
|
|
|
|
return wfs
|
|
}
|
|
|
|
// walkOnce enumerates the whole directory the way a front end does: repeated
|
|
// rounds against one handle until the listing runs dry, returning whatever
|
|
// references the round took.
|
|
func walkOnce(tb testing.TB, wfs *WFS, dirInode uint64, sink *benchSink, forgets bool) int {
|
|
dhid, _ := wfs.AcquireDirectoryHandle()
|
|
defer wfs.ReleaseDirectoryHandle(dhid)
|
|
|
|
total := 0
|
|
offset := uint64(0)
|
|
for {
|
|
sink.reset()
|
|
status := wfs.doReadDirectory(context.Background(), &fuse.ReadIn{
|
|
InHeader: fuse.InHeader{NodeId: dirInode},
|
|
Fh: uint64(dhid),
|
|
Offset: offset,
|
|
Size: 1 << 20,
|
|
}, sink, sink.plus)
|
|
if status != fuse.OK {
|
|
tb.Fatalf("readdir: %v", status)
|
|
}
|
|
if sink.count == 0 {
|
|
return total
|
|
}
|
|
total += sink.count
|
|
if forgets {
|
|
// HasInode keeps this to the entries that really hold a reference,
|
|
// so a listing that took none is not charged for a bogus Forget.
|
|
for _, ino := range sink.inodes {
|
|
if wfs.inodeToPath.HasInode(ino) {
|
|
wfs.Forget(ino, 1)
|
|
}
|
|
}
|
|
}
|
|
if sink.lastOff <= offset {
|
|
return total
|
|
}
|
|
offset = sink.lastOff
|
|
}
|
|
}
|
|
|
|
var benchCases = []struct {
|
|
name string
|
|
plus bool
|
|
// ref mirrors the sink's TakesLookupRef. The kernel's reports true whatever
|
|
// the mode, exactly as fuseDirEntryList does; it is the mode that decides
|
|
// whether a reference is actually granted.
|
|
ref bool
|
|
// forgets is what the front end does after a round: the kernel returns one
|
|
// FORGET per readdirplus entry and none for a plain readdir, and the WinFsp
|
|
// adapter hands back everything a round gave it.
|
|
forgets bool
|
|
}{
|
|
{"kernel_readdir", false, true, false},
|
|
{"kernel_readdirplus", true, true, true},
|
|
{"winfsp_readdirplus", true, false, true},
|
|
}
|
|
|
|
func BenchmarkReadDirectory(b *testing.B) {
|
|
dir := util.FullPath("/images")
|
|
for _, tc := range benchCases {
|
|
b.Run(tc.name, func(b *testing.B) {
|
|
wfs := newBenchWFS(b, dir, benchDirEntryCount)
|
|
dirInode, _ := wfs.inodeToPath.GetInode(dir)
|
|
sink := &benchSink{plus: tc.plus, takesRef: tc.ref, sinkLimit: 4096}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
if got := walkOnce(b, wfs, dirInode, sink, tc.forgets); got != benchDirEntryCount+2 {
|
|
b.Fatalf("listed %d entries, want %d", got, benchDirEntryCount+2)
|
|
}
|
|
}
|
|
b.StopTimer()
|
|
// What the listing left in the inode table, over the root and the
|
|
// directory itself.
|
|
b.ReportMetric(float64(inodeTableSize(wfs.inodeToPath)), "inodes_left")
|
|
})
|
|
}
|
|
}
|