azure: use a custom event loop for authentication (#5958)

* azure: use a custom event loop for authentication

* add test_temp_event_loop

* wrap open() too!

* ignore aiohttp warnings for normal level use-cases

* rename tests/unit/remote/test_azure to tests/unit/fs/test_azure
This commit is contained in:
Batuhan Taskaya
2021-05-05 15:41:55 +03:00
committed by GitHub
parent 0a0d146386
commit a234f53b76
3 changed files with 84 additions and 7 deletions
+42 -5
View File
@@ -1,6 +1,8 @@
import asyncio
import logging
import os
import threading
from contextlib import contextmanager
from funcy import cached_property, wrap_prop
@@ -19,6 +21,34 @@ _DEFAULT_CREDS_STEPS = (
)
@contextmanager
def _temp_event_loop():
"""When trying to initalize azure filesystem instances
with DefaultCredentials, the authentication process requires
to have an access to a separate event loop. The normal calls
are run in a separate loop managed by the fsspec, but the
DefaultCredentials assumes that the callee is managing their
own event loop. This function checks whether is there any
event loop set, and if not it creates a temporary event loop
and set it. After the context is finalized, it restores the
original event loop back (if there is any)."""
try:
original_loop = asyncio.get_event_loop()
except RuntimeError:
original_loop = None
loop = original_loop or asyncio.new_event_loop()
try:
asyncio.set_event_loop(loop)
yield
finally:
if original_loop is None:
loop.close()
asyncio.set_event_loop(original_loop)
class AzureAuthError(DvcException):
pass
@@ -120,11 +150,12 @@ class AzureFileSystem(FSSpecWrapper):
from azure.core.exceptions import AzureError
try:
file_system = AzureBlobFileSystem(**self.fs_args)
if self.bucket not in [
container.rstrip("/") for container in file_system.ls("/")
]:
file_system.mkdir(self.bucket)
with _temp_event_loop():
file_system = AzureBlobFileSystem(**self.fs_args)
if self.bucket not in [
container.rstrip("/") for container in file_system.ls("/")
]:
file_system.mkdir(self.bucket)
except (ValueError, AzureError) as e:
raise AzureAuthError(
f"Authentication to Azure Blob Storage via {self.login_method}"
@@ -133,3 +164,9 @@ class AzureFileSystem(FSSpecWrapper):
) from e
return file_system
def open(
self, path_info, mode="r", **kwargs
): # pylint: disable=arguments-differ
with _temp_event_loop():
return self.fs.open(self._with_bucket(path_info), mode=mode)
+11 -1
View File
@@ -196,7 +196,17 @@ def disable_other_loggers():
def setup(level=logging.INFO):
colorama.init()
logging.getLogger("asyncio").setLevel(logging.CRITICAL)
if level >= logging.DEBUG:
# Unclosed session errors for asyncio/aiohttp are only available
# on the tracing mode for extensive debug purposes. They are really
# noisy, and this is potentially somewhere in the client library
# not managing their own session. Even though it is the best practice
# for them to do so, we can be assured that these errors raised when
# the object is getting deallocated, so no need to take any extensive
# action.
logging.getLogger("asyncio").setLevel(logging.CRITICAL)
logging.getLogger("aiohttp").setLevel(logging.CRITICAL)
addLoggingLevel("TRACE", logging.DEBUG - 5)
logging.config.dictConfig(
@@ -1,4 +1,9 @@
from dvc.fs.azure import AzureFileSystem
import asyncio
from concurrent.futures import ThreadPoolExecutor
import pytest
from dvc.fs.azure import AzureFileSystem, _temp_event_loop
from dvc.path_info import PathInfo
container_name = "container-name"
@@ -37,3 +42,28 @@ def test_info(tmp_dir, azure):
hash_ = fs.info(to_info)["etag"]
assert isinstance(hash_, str)
assert hash_.strip("'").strip('"') == hash_
def test_temp_event_loop():
def procedure():
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.sleep(0))
return "yeey"
def wrapped_procedure():
with _temp_event_loop():
return procedure()
# it should clean the loop after
# exitting the context.
with pytest.raises(RuntimeError):
asyncio.get_event_loop()
with ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(procedure)
with pytest.raises(RuntimeError):
future.result()
future = executor.submit(wrapped_procedure)
assert future.result() == "yeey"