Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Changelog

Fixes

- Make async loop lock initialization thread-safe (#1783)
- FTP: preserve filenames containing whitespace in _mlsd2 (#2043)
- Prevent attribute error for 'forced' before flushing cache (#2042)
- Reflect async _walk correctly (#2040)
Expand Down
23 changes: 5 additions & 18 deletions fsspec/asyn.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,22 @@
private = re.compile("_[^_]")
iothread = [None] # dedicated fsspec IO thread
loop = [None] # global event loop for any non-async instance
_lock = None # global lock placeholder
_lock = threading.Lock()
get_running_loop = asyncio.get_running_loop


def get_lock():
"""Allocate or return a threading lock.

The lock is allocated on first use to allow setting one lock per forked process.
"""
global _lock
if not _lock:
_lock = threading.Lock()
"""Return the process-local threading lock."""
return _lock


def reset_lock():
"""Reset the global lock.

This should be called only on the init of a forked process to reset the lock to
None, enabling the new forked process to get a new lock.
"""
"""Reset the global loop and lock after forking."""
global _lock

iothread[0] = None
loop[0] = None
_lock = None
_lock = threading.Lock()


async def _runner(event, coro, result, timeout=None):
Expand Down Expand Up @@ -155,10 +145,7 @@ def get_loop():


def reset_after_fork():
global lock
loop[0] = None
iothread[0] = None
lock = None
reset_lock()


if hasattr(os, "register_at_fork"):
Expand Down
24 changes: 24 additions & 0 deletions fsspec/tests/test_async.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import asyncio
import inspect
import io
import threading
import time
from concurrent.futures import ThreadPoolExecutor
from types import SimpleNamespace

import pytest

Expand All @@ -10,6 +13,27 @@
from fsspec.asyn import _run_coros_in_chunks


def test_get_lock_is_thread_safe(monkeypatch):
barrier = threading.Barrier(2)
real_threading = fsspec.asyn.threading

def make_lock():
candidate = threading.Lock()
barrier.wait()
return candidate

fsspec.asyn.reset_lock()
try:
monkeypatch.setattr(fsspec.asyn, "threading", SimpleNamespace(Lock=make_lock))
with ThreadPoolExecutor(max_workers=2) as executor:
locks = list(executor.map(lambda _: fsspec.asyn.get_lock(), range(2)))
finally:
monkeypatch.setattr(fsspec.asyn, "threading", real_threading)
fsspec.asyn.reset_lock()

assert locks[0] is locks[1]


def test_sync_methods():
inst = fsspec.asyn.AsyncFileSystem()
assert inspect.iscoroutinefunction(inst._info)
Expand Down