Skip to content

Commit

Permalink
Fix a fuse issue where the shutdown procedure blocks forever
Browse files Browse the repository at this point in the history
  • Loading branch information
vxgmichel authored May 4, 2021
2 parents 12084f4 + fde1d27 commit 0d523b3
Show file tree
Hide file tree
Showing 2 changed files with 22 additions and 7 deletions.
1 change: 1 addition & 0 deletions newsfragments/1716.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix an issue with fuse mounpoints on linux where the shutdown procedure might block forever
28 changes: 21 additions & 7 deletions parsec/core/mountpoint/fuse_runner.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Parsec Cloud (https://parsec.cloud) Copyright (c) AGPLv3 2016-2021 Scille SAS

from parsec.core.core_events import CoreEvent
import os
import sys
import trio
import errno
Expand Down Expand Up @@ -125,7 +126,7 @@ async def fuse_mountpoint_runner(
# Note that this does not prevent the user from using a certain encoding
# in the context of the parsec app and another encoding in the context of
# an application accessing the mountpoint. In this case, an encoding error
# might be raised while fuspy tries to decode the path. If that happends,
# might be raised while fuspy tries to decode the path. If that happens,
# fuspy will log the error and simply return EINVAL, which is acceptable.
encoding = sys.getfilesystemencoding()

Expand Down Expand Up @@ -228,24 +229,37 @@ async def _stop_fuse_thread(
if fuse_thread_stopped.is_set() or not fuse_thread_started.is_set():
return
logger.info("Stopping fuse thread...", mountpoint=mountpoint_path)

# Fuse exit in macOS
if sys.platform == "darwin":
# The schedule_exit() solution doesn't work on macOS, instead freezes the application for
# 120 seconds before a timeout occurs. The solution used is to call this function (macOS
# equivalent to fusermount) in a subprocess to unmount.
await trio.run_process(["diskutil", "unmount", "force", str(mountpoint_path)])

# Fuse exit in linux
else:
# Schedule an exit in the fuse operations
fuse_operations.schedule_exit()

# Ping fuse by performing an access from a daemon thread
def _ping_fuse_thread_target():
try:
os.stat(mountpoint_path / ".__shutdown_fuse__")
except OSError:
pass

# All kind of stuff can go wrong here if the access is performed while the fuse FS is already shutting down,
# from exotic exceptions to blocking forever. For this reason, we don't use `await trio.Path(...).exists()`
# but simply fire and forget a ping to the fuse FS. Either this ping succeeds in waking the FS up and thus
# starting its shutdown procedure, or it fails because the FS is already shutting down.
threading.Thread(target=_ping_fuse_thread_target, daemon=True).start()

# Loop over attemps at waking up the fuse operations
while True:
# Attempt to wake up the fuse operations
try:
await trio.Path(mountpoint_path / ".__shutdown_fuse__").exists()
# This might fail for many reasons
except OSError:
pass
# Check if the attempt succeeded for 10 ms
if await trio.to_thread.run_sync(fuse_thread_stopped.wait, 0.01):
break

# The thread has now stopped
logger.info("Fuse thread stopped", mountpoint=mountpoint_path)

0 comments on commit 0d523b3

Please sign in to comment.