Skip to content

Commit

Permalink
make Statfs and Fsync timeout faster for unmounting (keybase#24433)
Browse files Browse the repository at this point in the history
* make Statfs and Fsync timeout faster for unmounting

Otherwise if service has already exited, these calls will hang forever
causing both `umount` and `diskutil umount force` to block. Eventually
something else kicks in and kills the KBFS process, leaving behind a
mount point. With this fix, there's a greater chance that unmounting
happens before the KBFS process gets killed.

* strib feedback
  • Loading branch information
songgao authored Jan 27, 2021
1 parent cfdcb9f commit b08d45f
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 12 deletions.
19 changes: 14 additions & 5 deletions go/kbfs/libfuse/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,18 +192,27 @@ func (f *File) sync(ctx context.Context) error {

// Fsync implements the fs.NodeFsyncer interface for File.
func (f *File) Fsync(ctx context.Context, req *fuse.FsyncRequest) (err error) {
ctx, maybeUnmounting, cancel := wrapCtxWithShorterTimeoutForUnmount(f.folder.fs.log, ctx, int(req.Pid))
defer cancel()
if maybeUnmounting {
f.folder.fs.log.CInfof(ctx, "Fsync: maybeUnmounting=true")
}

ctx = f.folder.fs.config.MaybeStartTrace(
ctx, "File.Fsync", f.node.GetBasename().String())
defer func() { f.folder.fs.config.MaybeFinishTrace(ctx, err) }()

f.folder.fs.vlog.CLogf(ctx, libkb.VLog1, "File Fsync")
defer func() { err = f.folder.processError(ctx, libkbfs.WriteMode, err) }()

// This fits in situation 1 as described in libkbfs/delayed_cancellation.go
err = libcontext.EnableDelayedCancellationWithGracePeriod(
ctx, f.folder.fs.config.DelayedCancellationGracePeriod())
if err != nil {
return err
if !maybeUnmounting {
// This fits in situation 1 as described in
// libkbfs/delayed_cancellation.go
err = libcontext.EnableDelayedCancellationWithGracePeriod(
ctx, f.folder.fs.config.DelayedCancellationGracePeriod())
if err != nil {
return err
}
}

return f.sync(ctx)
Expand Down
11 changes: 6 additions & 5 deletions go/kbfs/libfuse/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,13 +381,14 @@ func (f *FS) Root() (fs.Node, error) {
return f.root, nil
}

// quotaUsageStaleTolerance is the lifespan of stale usage data that libfuse
// accepts in the Statfs handler. In other words, this causes libkbfs to issue
// a fresh RPC call if cached usage data is older than 10s.
const quotaUsageStaleTolerance = 10 * time.Second

// Statfs implements the fs.FSStatfser interface for FS.
func (f *FS) Statfs(ctx context.Context, req *fuse.StatfsRequest, resp *fuse.StatfsResponse) error {
ctx, maybeUnmounting, cancel := wrapCtxWithShorterTimeoutForUnmount(f.log, ctx, int(req.Pid))
defer cancel()
if maybeUnmounting {
f.log.CInfof(ctx, "Statfs: maybeUnmounting=true")
}

*resp = fuse.StatfsResponse{
Bsize: fuseBlockSize,
Namelen: ^uint32(0),
Expand Down
15 changes: 14 additions & 1 deletion go/kbfs/libfuse/mounter_non_osx.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@

package libfuse

import "bazil.org/fuse"
import (
"context"

"bazil.org/fuse"
"github.com/keybase/client/go/logger"
)

func getPlatformSpecificMountOptions(dir string, platformParams PlatformParams) ([]fuse.MountOption, error) {
options := []fuse.MountOption{}
Expand All @@ -27,3 +32,11 @@ func translatePlatformSpecificError(err error, platformParams PlatformParams) er
func (m *mounter) reinstallMountDirIfPossible() {
// no-op
}

var noop = func() {}

func wrapCtxWithShorterTimeoutForUnmount(
_ logger.Logger, ctx context.Context, _ int) (
newCtx context.Context, maybeUnmounting bool, cancel context.CancelFunc) {
return ctx, false, noop
}
39 changes: 39 additions & 0 deletions go/kbfs/libfuse/mounter_osx.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
package libfuse

import (
"context"
"errors"
"time"

"bazil.org/fuse"
"github.com/keybase/client/go/install/libnativeinstaller"
"github.com/keybase/client/go/logger"
)

var kbfusePath = fuse.OSXFUSEPaths{
Expand Down Expand Up @@ -84,3 +87,39 @@ func (m *mounter) reinstallMountDirIfPossible() {
err = libnativeinstaller.InstallMountDir(m.runMode, m.log)
m.log.Debug("InstallMountDir: err=%v", err)
}

// quotaUsageStaleTolerance is the lifespan of stale usage data that libfuse
// accepts in the Statfs handler. In other words, this causes libkbfs to issue
// a fresh RPC call if cached usage data is older than 10s.
const quotaUsageStaleTolerance = 10 * time.Second

const unmountCallTolerance = time.Second

var unmountingExecPaths = map[string]bool{
"/usr/sbin/diskutil": true,
"/usr/libexec/lsd": true,
"/sbin/umount": true,
}

var noop = func() {}

// wrapCtxWithShorterTimeoutForUnmount wraps ctx witha a timeout of
// unmountCallTolerance if pid is /usr/sbin/diskutil, /usr/libexec/lsd, or
// /sbin/umount. This is useful for calls that usually happen during unmounting
// such as Statfs and Fsync. If we block on those calls, `diskutil umount force
// <mnt>` is blocked as well. So make them timeout after 2s to make unmounting
// work.
func wrapCtxWithShorterTimeoutForUnmount(
log logger.Logger, ctx context.Context, pid int) (
newCtx context.Context, maybeUnmounting bool, cancel context.CancelFunc) {
p, err := pidPath(pid)
if err != nil {
return ctx, false, noop
}
if unmountingExecPaths[p] {
log.CDebugf(ctx, "wrapping context with timeout for %s", p)
newCtx, cancel = context.WithTimeout(ctx, unmountCallTolerance)
return newCtx, true, cancel
}
return ctx, false, noop
}
41 changes: 41 additions & 0 deletions go/kbfs/libfuse/pidpath_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2021 Keybase Inc. All rights reserved.
// Use of this source code is governed by a BSD
// license that can be found in the LICENSE file.
//
// +build darwin

package libfuse

// #include <libproc.h>
// #include <stdlib.h>
// #include <errno.h>
import "C"

import (
"errors"
"strconv"
"unsafe"
)

// pidPath returns the exec path for process pid. Adapted from
// https://ops.tips/blog/macos-pid-absolute-path-and-procfs-exploration/
func pidPath(pid int) (path string, err error) {
const bufSize = C.PROC_PIDPATHINFO_MAXSIZE
buf := C.CString(string(make([]byte, bufSize)))
defer C.free(unsafe.Pointer(buf))

ret, err := C.proc_pidpath(C.int(pid), unsafe.Pointer(buf), bufSize)
if err != nil {
return "", err
}
if ret < 0 {
return "", errors.New(
"error calling proc_pidpath. exit code: " + strconv.Itoa(int(ret)))
}
if ret == 0 {
return "", errors.New("proc_pidpath returned empty buffer")
}

path = C.GoString(buf)
return
}
15 changes: 15 additions & 0 deletions go/kbfs/libfuse/pidpath_others.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2021 Keybase Inc. All rights reserved.
// Use of this source code is governed by a BSD
// license that can be found in the LICENSE file.
//
// +build !darwin

package libfuse

import "github.com/pkg/errors"

var notImplementedErr = errors.New("unimplemented")

func pidPath(_ int) (path string, err error) {
return "", notImplementedErr
}
4 changes: 3 additions & 1 deletion go/mounter/mounter_non_osx.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

package mounter

import "fmt"
import (
"fmt"
)

// IsMounted returns true if directory is mounted (by kbfuse)
func IsMounted(dir string, log Log) (bool, error) {
Expand Down

0 comments on commit b08d45f

Please sign in to comment.