Skip to content
Closed
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
2 changes: 2 additions & 0 deletions .vscode/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@
"spinup",
"cibuildwheel",
"aoai",
"aiotest",
"aiotests",
"pyprojects",
"certifi",
"cffi",
Expand Down
6 changes: 3 additions & 3 deletions sdk/core/azure-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# Release History

## 1.35.0 (Unreleased)
## 1.35.0 (2025-06-05)

### Features Added

- Added a `start_time` keyword argument to the `start_span` and `start_as_current_span` methods in the `OpenTelemetryTracer` class. This allows users to specify a custom start time for created spans. #41106

### Breaking Changes

### Bugs Fixed

- Reduce risk of hanging while closing aiohttp transport if server does not follow best practices. Fix #41363

### Other Changes

- A timeout error when using the `aiohttp` transport (the default for async SDKs) will now be raised as a `azure.core.exceptions.ServiceResponseTimeoutError`, a subtype of the previously raised `ServiceResponseError`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,10 @@ async def open(self):
async def close(self):
"""Closes the connection."""
if self._session_owner and self.session:
await self.session.close()
try:
await asyncio.wait_for(self.session.close(), timeout=0.0001) # close immediately
except (asyncio.TimeoutError, TimeoutError):
pass
self.session = None

def _build_ssl_config(self, cert, verify):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for
# license information.
# -------------------------------------------------------------------------
import sys
import pytest
import logging
from azure.storage.blob.aio import BlobServiceClient


@pytest.mark.skipif(
sys.version_info < (3, 11), reason="ssl_shutdown_timeout in aiohttp only takes effect on Python 3.11+"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to skip 3.9/3.10? Ideally, we see no warnings logged for those versions, too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see errors when using aiohttp 3.12.7 with py 3.9.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What errors? Things should still work with Python 3.9+ since we are only adding a timeout to asyncio.wait_for and not using ssl_shutdown_timeout.

Also, let's @pytest.mark.live_test_only this test. Ideally, we use the storage account that's provisioned in test-resources.bicep.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aiohttp hangs in Python 3.9 (compared to a 30-second delay in 3.11). In 3.9, we have to forcefully stop aiohttp, and an error from aiohttp appeared during my test on Python 3.9.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't able to reproduce an error locally with 3.9/3.10. Did you see this on CI?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this on py3.9 w/ timeout in azure-core.aiohttp added:

Exception ignored in: <function _ProactorBasePipeTransport.del at 0x000002255D531670>
Traceback (most recent call last):
File "C:\Users\xiangyan\AppData\Local\Programs\Python\Python39\lib\asyncio\proactor_events.py", line 116, in del
self.close()
File "C:\Users\xiangyan\AppData\Local\Programs\Python\Python39\lib\asyncio\proactor_events.py", line 108, in close
self._loop.call_soon(self._call_connection_lost, None)
File "C:\Users\xiangyan\AppData\Local\Programs\Python\Python39\lib\asyncio\base_events.py", line 751, in call_soon
self._check_closed()
File "C:\Users\xiangyan\AppData\Local\Programs\Python\Python39\lib\asyncio\base_events.py", line 515, in _check_closed
raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I think I'm not seeing it because I'm running on WSL. Looks like that ^ only shows up in windows.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that runtime exception show up in windows/py3.9 even without the timeout changes in this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. aiohttp 3.12.7 does not solve the problem on py 3.9. (I did not test py 3.10)

)
@pytest.mark.live_test_only
@pytest.mark.asyncio
async def test_download_blob_aiohttp(caplog):
logger = logging.getLogger(__name__)
AZURE_STORAGE_CONTAINER_NAME = "aiotests"
account_url = "https://aiotests.blob.core.windows.net"
blob_service_client = BlobServiceClient(account_url=account_url)
read_path = "aiotest.txt"

with caplog.at_level(logging.INFO):
async with blob_service_client:
blob_client = blob_service_client.get_blob_client(container=AZURE_STORAGE_CONTAINER_NAME, blob=read_path)

async with blob_client:
stream = await blob_client.download_blob()
data = await stream.readall()
logger.info(f"Blob size: {len(data)}")

assert all("Error" not in message for message in caplog.messages)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for
# license information.
# -------------------------------------------------------------------------
import asyncio
import time
import pytest
import types
from unittest.mock import Mock, patch

from azure.core.pipeline.transport import (
AioHttpTransport,
)


class MockClientSession:
"""Mock aiohttp.ClientSession with a close method that sleeps for 30 seconds."""

def __init__(self):
self._closed = False

async def __aenter__(self):
return self

async def __aexit__(self, *args, **kwargs):
pass

async def close(self):
"""Simulate a slow closing session."""
self._closed = True
await asyncio.sleep(30) # Simulate a very slow close operation

def request(self, *args, **kwargs):
return asyncio.Future() # Just needs to be awaitable


@pytest.mark.asyncio
async def test_aiohttp_transport_close_timeout():
"""Test that AioHttpTransport.close() returns within 1 second even if session.close() takes 30 seconds."""

# Create transport with our mock session
mock_session = MockClientSession()
transport = AioHttpTransport(session=mock_session, session_owner=True)

# Open the transport to initialize the session
await transport.open()

# Time the close operation
start_time = time.time()
await transport.close()
end_time = time.time()

# Verify close returned in a reasonable time (should be around 0.0001 seconds due to timeout)
assert end_time - start_time < 0.1, f"Transport close took {end_time - start_time} seconds, should be < 0.1 seconds"

# Ensure transport's session was set to None
assert transport.session is None
Loading