Commit 4f10e248 authored by Han-Wen Nienhuys's avatar Han-Wen Nienhuys

fuse: fix tests for Go 1.9

Go 1.9 uses epoll() for more efficient file I/O. File I/O causes a
call to epoll, and the runtime makes this call take up a GOMAXPROCS
slot.

The FUSE kernel module also supports poll: polling on a file residing
in a FUSE file system causes the kernel to sends a POLL request to the
userspace process.  If the process responds with ENOSYS, the kernel
will stop forwarding poll requests to the FUSE process.

In a test for Go FUSE file systems, it is normal to serve the
filesystem out of the same process that opens files in the file
system. If this happens in Go 1.9, the epoll call can take the only
GOMAXPROCS slot left, leaving the process unable to respond to the
FUSE POLL opcode, deadlocking the process.

This change add support for a magic file "/ .go-fuse-epoll-hack" with
node ID uint64(-1), and on starting up the file system, the library
calls poll() on this file, triggering the POLL opcode before the Go
runtime had a chance to do so.

There are two problem scenarios left:

* File system tests that start I/O before calling WaitMount() still
  risk deadlocking themselves.

* The Linux kernel keeps track of feature support in fuse_conn, which notes

         * The following bitfields are only for optimization purposes
         * and hence races in setting them will not cause malfunction

  if our forced ENOSYS gets lost due to a race condition in the
  kernel, this can still trigger.

Fixes golang/go#21014 and #165
parent f7e021d6
...@@ -129,12 +129,10 @@ KNOWN PROBLEMS ...@@ -129,12 +129,10 @@ KNOWN PROBLEMS
Grep source code for TODO. Major topics: Grep source code for TODO. Major topics:
* Support for umask in Create
* Missing support for network FS file locking: FUSE_GETLK, FUSE_SETLK, * Missing support for network FS file locking: FUSE_GETLK, FUSE_SETLK,
FUSE_SETLKW FUSE_SETLKW
* Missing support for FUSE_INTERRUPT, CUSE, BMAP, POLL, IOCTL * Missing support for FUSE_INTERRUPT, CUSE, BMAP, IOCTL
* In the path API, renames are racy; See also: * In the path API, renames are racy; See also:
......
...@@ -129,6 +129,22 @@ func doOpen(server *Server, req *request) { ...@@ -129,6 +129,22 @@ func doOpen(server *Server, req *request) {
func doCreate(server *Server, req *request) { func doCreate(server *Server, req *request) {
out := (*CreateOut)(req.outData) out := (*CreateOut)(req.outData)
if req.filenames[0] == pollHackName && req.inHeader.NodeId == FUSE_ROOT_ID {
out.EntryOut = EntryOut{
NodeId: pollHackInode,
Attr: Attr{
Ino: pollHackInode,
Mode: S_IFREG | 0644,
Nlink: 1,
},
}
out.OpenOut = OpenOut{
Fh: pollHackInode,
}
req.status = OK
return
}
status := server.fileSystem.Create((*CreateIn)(req.inData), req.filenames[0], out) status := server.fileSystem.Create((*CreateIn)(req.inData), req.filenames[0], out)
req.status = status req.status = status
} }
...@@ -246,6 +262,9 @@ func doGetAttr(server *Server, req *request) { ...@@ -246,6 +262,9 @@ func doGetAttr(server *Server, req *request) {
// doForget - forget one NodeId // doForget - forget one NodeId
func doForget(server *Server, req *request) { func doForget(server *Server, req *request) {
if req.inHeader.NodeId == pollHackInode {
return
}
if !server.opts.RememberInodes { if !server.opts.RememberInodes {
server.fileSystem.Forget(req.inHeader.NodeId, (*ForgetIn)(req.inData).Nlookup) server.fileSystem.Forget(req.inHeader.NodeId, (*ForgetIn)(req.inData).Nlookup)
} }
...@@ -272,6 +291,9 @@ func doBatchForget(server *Server, req *request) { ...@@ -272,6 +291,9 @@ func doBatchForget(server *Server, req *request) {
if server.opts.Debug { if server.opts.Debug {
log.Printf("doBatchForget: forgetting %d of %d: NodeId: %d, Nlookup: %d", i+1, len(forgets), f.NodeId, f.Nlookup) log.Printf("doBatchForget: forgetting %d of %d: NodeId: %d, Nlookup: %d", i+1, len(forgets), f.NodeId, f.Nlookup)
} }
if f.NodeId == pollHackInode {
continue
}
server.fileSystem.Forget(f.NodeId, f.Nlookup) server.fileSystem.Forget(f.NodeId, f.Nlookup)
} }
} }
......
...@@ -15,6 +15,8 @@ import ( ...@@ -15,6 +15,8 @@ import (
"syscall" "syscall"
"time" "time"
"unsafe" "unsafe"
"golang.org/x/sys/unix"
) )
const ( const (
...@@ -55,6 +57,9 @@ type Server struct { ...@@ -55,6 +57,9 @@ type Server struct {
ready chan error ready chan error
} }
const pollHackName = ".go-fuse-epoll-hack"
const pollHackInode = ^uint64(0)
// SetDebug is deprecated. Use MountOptions.Debug instead. // SetDebug is deprecated. Use MountOptions.Debug instead.
func (ms *Server) SetDebug(dbg bool) { func (ms *Server) SetDebug(dbg bool) {
// This will typically trigger the race detector. // This will typically trigger the race detector.
...@@ -161,6 +166,7 @@ func NewServer(fs RawFileSystem, mountPoint string, opts *MountOptions) (*Server ...@@ -161,6 +166,7 @@ func NewServer(fs RawFileSystem, mountPoint string, opts *MountOptions) (*Server
// FUSE device: on unmount, sometime some reads do not // FUSE device: on unmount, sometime some reads do not
// error-out, meaning that unmount will hang. // error-out, meaning that unmount will hang.
singleReader: runtime.GOOS == "darwin", singleReader: runtime.GOOS == "darwin",
ready: make(chan error, 1),
} }
ms.reqPool.New = func() interface{} { return new(request) } ms.reqPool.New = func() interface{} { return new(request) }
ms.readPool.New = func() interface{} { return make([]byte, o.MaxWrite+pageSize) } ms.readPool.New = func() interface{} { return make([]byte, o.MaxWrite+pageSize) }
...@@ -173,7 +179,6 @@ func NewServer(fs RawFileSystem, mountPoint string, opts *MountOptions) (*Server ...@@ -173,7 +179,6 @@ func NewServer(fs RawFileSystem, mountPoint string, opts *MountOptions) (*Server
} }
mountPoint = filepath.Clean(filepath.Join(cwd, mountPoint)) mountPoint = filepath.Clean(filepath.Join(cwd, mountPoint))
} }
ms.ready = make(chan error, 1)
fd, err := mount(mountPoint, &o, ms.ready) fd, err := mount(mountPoint, &o, ms.ready)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -389,7 +394,13 @@ func (ms *Server) handleRequest(req *request) Status { ...@@ -389,7 +394,13 @@ func (ms *Server) handleRequest(req *request) Status {
log.Println(req.InputDebug()) log.Println(req.InputDebug())
} }
if req.status.Ok() && req.handler.Func == nil { if req.inHeader.Opcode == _OP_POLL {
req.status = ENOSYS
} else if req.inHeader.NodeId == pollHackInode {
// We want to avoid switching off features through our
// poll hack, so don't use ENOSYS
req.status = EIO
} else if req.status.Ok() && req.handler.Func == nil {
log.Printf("Unimplemented opcode %v", operationName(req.inHeader.Opcode)) log.Printf("Unimplemented opcode %v", operationName(req.inHeader.Opcode))
req.status = ENOSYS req.status = ENOSYS
} }
...@@ -582,5 +593,30 @@ func init() { ...@@ -582,5 +593,30 @@ func init() {
// Currently, this call only necessary on OSX. // Currently, this call only necessary on OSX.
func (ms *Server) WaitMount() error { func (ms *Server) WaitMount() error {
err := <-ms.ready err := <-ms.ready
if err != nil {
return err return err
}
return pollHack(ms.mountPoint)
}
// Go 1.9 introduces polling for file I/O. The implementation causes
// the runtime's epoll to take up the last GOMAXPROCS slot, and if
// that happens, we won't have any threads left to service FUSE's
// _OP_POLL request. Prevent this by forcing _OP_POLL to happen, so we
// can say ENOSYS and prevent further _OP_POLL requests.
func pollHack(mountPoint string) error {
fd, err := syscall.Creat(filepath.Join(mountPoint, pollHackName), syscall.O_CREAT)
if err != nil {
return err
}
pollData := []unix.PollFd{{
Fd: int32(fd),
Events: unix.POLLIN | unix.POLLPRI | unix.POLLOUT,
}}
// Trigger _OP_POLL, so we can say ENOSYS. We don't care about
// the return value.
unix.Poll(pollData, 0)
syscall.Close(fd)
return nil
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment