Files
go-fuse/fuse/test/node_parallel_lookup_test.go

144 lines
3.6 KiB
Go

// Copyright 2019 the Go-FUSE Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package test
// verify that several lookup requests can be served in parallel without deadlock.
import (
"context"
"fmt"
"log"
"os"
"testing"
"golang.org/x/sync/errgroup"
"github.com/hanzoai/go-fuse/v2/fuse"
"github.com/hanzoai/go-fuse/v2/fuse/nodefs"
"github.com/hanzoai/go-fuse/v2/internal/testutil"
)
// tRoot implements simple root node which Lookups children in predefined .nodes.
// The Lookup is synchronized with main test driver on .lookupq and .lookupGo.
type tRoot struct {
nodefs.Node
nodes map[string]nodefs.Node // name -> Node
lookupq chan string // main <- fssrv: lookup(name) request received
lookupGo chan struct{} // main -> fssrv: ok to further process lookup requests
}
func (r *tRoot) Lookup(out *fuse.Attr, name string, fctx *fuse.Context) (*nodefs.Inode, fuse.Status) {
node, ok := r.nodes[name]
if !ok {
// e.g. it can be lookup for .Trash automatically issued by volume manager
return nil, fuse.ENOENT
}
r.lookupq <- name // tell main driver that we received lookup(name)
<-r.lookupGo // wait for main to allow us to continue
st := node.GetAttr(out, nil, fctx)
return node.Inode(), st
}
// verifyFileRead verifies that file @path has content == dataOK.
func verifyFileRead(path string, dataOK string) error {
v, err := os.ReadFile(path)
if err != nil {
return err
}
if string(v) != dataOK {
return fmt.Errorf("%s: file read: got %q ; want %q", path, v, dataOK)
}
return nil
}
func TestNodeParallelLookup(t *testing.T) {
dir := t.TempDir()
root := &tRoot{
Node: nodefs.NewDefaultNode(),
nodes: make(map[string]nodefs.Node),
lookupq: make(chan string),
lookupGo: make(chan struct{}),
}
opts := nodefs.NewOptions()
opts.LookupKnownChildren = true
opts.Debug = testutil.VerboseTest()
srv, _, err := nodefs.MountRoot(dir, root, opts)
if err != nil {
t.Fatal(err)
}
root.nodes["hello"] = NewDataNode([]byte("abc"))
root.nodes["world"] = NewDataNode([]byte("def"))
root.Inode().NewChild("hello", false, root.nodes["hello"])
root.Inode().NewChild("world", false, root.nodes["world"])
go srv.Serve()
if err := srv.WaitMount(); err != nil {
t.Fatal("WaitMount", err)
}
defer func() {
err := srv.Unmount()
if err != nil {
t.Fatal(err)
}
}()
// the test will deadlock if the client cannot issue several lookups simultaneously
if srv.KernelSettings().Flags&fuse.CAP_PARALLEL_DIROPS == 0 {
t.Skip("Kernel serializes dir lookups")
}
// spawn 2 threads to access the files in parallel
// this will deadlock if nodefs does not allow simultaneous Lookups to be handled.
// see https://github.com/hanwen/go-fuse/commit/d0fca860 for context.
ctx0, cancel := context.WithCancel(context.Background())
defer cancel()
wg, ctx := errgroup.WithContext(ctx0)
wg.Go(func() error {
return verifyFileRead(dir+"/hello", "abc")
})
wg.Go(func() error {
return verifyFileRead(dir+"/world", "def")
})
// wait till both threads queue into Lookup
expect := map[string]struct{}{ // set of expected lookups
"hello": {},
"world": {},
}
loop:
for len(expect) > 0 {
var lookup string
select {
case <-ctx.Done():
break loop // wg.Wait will return the error
case lookup = <-root.lookupq:
// ok
}
if testutil.VerboseTest() {
log.Printf("I: <- lookup %q", lookup)
}
_, ok := expect[lookup]
if !ok {
t.Fatalf("unexpected lookup: %q ; expect: %q", lookup, expect)
}
delete(expect, lookup)
}
// let both lookups continue
close(root.lookupGo)
err = wg.Wait()
if err != nil {
t.Fatal(err)
}
}