mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 07:18:25 +01:00
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:
+42
-5
@@ -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
@@ -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"
|
||||
Reference in New Issue
Block a user