From e32a62656f5e70ca276c0c94065d0eab3b378880 Mon Sep 17 00:00:00 2001 From: Peter Wu <162184229+weirongw23-msft@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:03:17 -0500 Subject: [PATCH 1/2] [Storage] Added retry logic in Blob download's `process_content` for `ServiceResponseError` (#45055) --- sdk/storage/azure-storage-blob/assets.json | 2 +- .../azure/storage/blob/_download.py | 6 +-- .../azure/storage/blob/aio/_download_async.py | 6 +-- .../azure-storage-blob/tests/test_retry.py | 50 ++++++++++++++++++- .../tests/test_retry_async.py | 50 ++++++++++++++++++- 5 files changed, 105 insertions(+), 9 deletions(-) diff --git a/sdk/storage/azure-storage-blob/assets.json b/sdk/storage/azure-storage-blob/assets.json index f94cd70f1a2d..8ce342de1105 100644 --- a/sdk/storage/azure-storage-blob/assets.json +++ b/sdk/storage/azure-storage-blob/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/storage/azure-storage-blob", - "Tag": "python/storage/azure-storage-blob_16c5acad24" + "Tag": "python/storage/azure-storage-blob_89c4f2856e" } diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_download.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_download.py index 6b4482f3c621..a71f579b129f 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_download.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_download.py @@ -15,7 +15,7 @@ overload, Tuple, TypeVar, Union, TYPE_CHECKING ) -from azure.core.exceptions import DecodeError, HttpResponseError, IncompleteReadError +from azure.core.exceptions import DecodeError, HttpResponseError, IncompleteReadError, ServiceResponseError from azure.core.tracing.common import with_current_context from ._shared.request_handlers import validate_and_format_range_headers @@ -235,7 +235,7 @@ def _download_chunk(self, chunk_start: int, chunk_end: int) -> Tuple[bytes, int] try: chunk_data = process_content(response, offset[0], offset[1], self.encryption_options) retry_active = False - except (IncompleteReadError, HttpResponseError, DecodeError) as error: + except (IncompleteReadError, HttpResponseError, DecodeError, ServiceResponseError) as error: retry_total -= 1 if retry_total <= 0: raise HttpResponseError(error, error=error) from error @@ -522,7 +522,7 @@ def _initial_request(self): self._encryption_options ) retry_active = False - except (IncompleteReadError, HttpResponseError, DecodeError) as error: + except (IncompleteReadError, HttpResponseError, DecodeError, ServiceResponseError) as error: retry_total -= 1 if retry_total <= 0: raise HttpResponseError(error, error=error) from error diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_download_async.py b/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_download_async.py index b4355e8ee841..fa8ff67a7645 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_download_async.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_download_async.py @@ -19,7 +19,7 @@ Tuple, TypeVar, Union, TYPE_CHECKING ) -from azure.core.exceptions import DecodeError, HttpResponseError, IncompleteReadError +from azure.core.exceptions import DecodeError, HttpResponseError, IncompleteReadError, ServiceResponseError from .._shared.request_handlers import validate_and_format_range_headers from .._shared.response_handlers import parse_length_from_content_range, process_storage_error @@ -144,7 +144,7 @@ async def _download_chunk(self, chunk_start: int, chunk_end: int) -> Tuple[bytes try: chunk_data = await process_content(response, offset[0], offset[1], self.encryption_options) retry_active = False - except (IncompleteReadError, HttpResponseError, DecodeError) as error: + except (IncompleteReadError, HttpResponseError, DecodeError, ServiceResponseError) as error: retry_total -= 1 if retry_total <= 0: raise HttpResponseError(error, error=error) from error @@ -432,7 +432,7 @@ async def _initial_request(self): self._encryption_options ) retry_active = False - except (IncompleteReadError, HttpResponseError, DecodeError) as error: + except (IncompleteReadError, HttpResponseError, DecodeError, ServiceResponseError) as error: retry_total -= 1 if retry_total <= 0: raise HttpResponseError(error, error=error) from error diff --git a/sdk/storage/azure-storage-blob/tests/test_retry.py b/sdk/storage/azure-storage-blob/tests/test_retry.py index 9d5c131e3609..960bcd7923e1 100644 --- a/sdk/storage/azure-storage-blob/tests/test_retry.py +++ b/sdk/storage/azure-storage-blob/tests/test_retry.py @@ -13,7 +13,8 @@ ClientAuthenticationError, HttpResponseError, ResourceExistsError, - ServiceResponseError + ServiceResponseError, + ServiceResponseTimeoutError ) from azure.core.pipeline.transport import RequestsTransport from azure.storage.blob._shared.authentication import AzureSigningError @@ -639,4 +640,51 @@ def assert_exception_retry_hook(**kwargs): assert retry_counter.count == 3 + @BlobPreparer() + @recorded_by_proxy + def test_retry_on_service_response_error(self, **kwargs): + storage_account_name = kwargs.pop("storage_account_name") + storage_account_key = kwargs.pop("storage_account_key") + + # Arrange + container_name = self.get_resource_name('utcontainer') + blob_name = self.get_resource_name('blob') + service = self._create_storage_service( + BlobServiceClient, storage_account_name, storage_account_key, max_block_size=4) + container = service.create_container(container_name) + data = b'abcd' * 4 + container.upload_blob(blob_name, data, overwrite=True) + + retry = LinearRetry(backoff=1, random_jitter_range=1) + retry_counter = RetryCounter() + retry_service = self._create_storage_service( + BlobServiceClient, + storage_account_name, + storage_account_key, + retry_policy=retry, + max_block_size=4 + ) + blob = retry_service.get_blob_client(container_name, blob_name) + + # Mock the internal response to raise ServiceResponseError on first chunk processing + from azure.storage.blob._download import process_content as real_process_content + + def mock_process_content_with_error(response, start_offset, end_offset, encryption): + retry_counter.simple_count(retry) + conn_error = AzureError("Connection reset by peer") + if retry_counter.count == 1: + raise ServiceResponseError(conn_error, error=conn_error) + elif retry_counter.count == 2: + raise ServiceResponseTimeoutError(conn_error, error=conn_error) + return real_process_content(response, start_offset, end_offset, encryption) + + # Act + try: + with mock.patch('azure.storage.blob._download.process_content', side_effect=mock_process_content_with_error): + downloaded_data = blob.download_blob().readall() + assert downloaded_data == data + assert retry_counter.count >= 3 + finally: + service.delete_container(container_name) + # ------------------------------------------------------------------------------ diff --git a/sdk/storage/azure-storage-blob/tests/test_retry_async.py b/sdk/storage/azure-storage-blob/tests/test_retry_async.py index 0c1c51478618..2285321ced3b 100644 --- a/sdk/storage/azure-storage-blob/tests/test_retry_async.py +++ b/sdk/storage/azure-storage-blob/tests/test_retry_async.py @@ -17,7 +17,8 @@ IncompleteReadError, HttpResponseError, ResourceExistsError, - ServiceResponseError + ServiceResponseError, + ServiceResponseTimeoutError ) from azure.core.pipeline.transport import AioHttpTransport from azure.storage.blob import LocationMode @@ -618,4 +619,51 @@ def assert_exception_retry_hook(**kwargs): assert retry_counter.count == 3 + @BlobPreparer() + @recorded_by_proxy_async + async def test_retry_on_service_response_error(self, **kwargs): + storage_account_name = kwargs.pop("storage_account_name") + storage_account_key = kwargs.pop("storage_account_key") + + # Arrange + container_name = self.get_resource_name('utcontainer') + blob_name = self.get_resource_name('blob') + service = self._create_storage_service( + BlobServiceClient, storage_account_name, storage_account_key, max_block_size=4) + container = await service.create_container(container_name) + data = b'abcd' * 4 + await container.upload_blob(blob_name, data, overwrite=True) + + retry = LinearRetry(backoff=1, random_jitter_range=1) + retry_counter = RetryCounter() + retry_service = self._create_storage_service( + BlobServiceClient, + storage_account_name, + storage_account_key, + retry_policy=retry, + max_block_size=4 + ) + blob = retry_service.get_blob_client(container_name, blob_name) + + # Mock the internal response to raise ServiceResponseError on first chunk processing + from azure.storage.blob.aio._download_async import process_content as real_process_content + + async def mock_process_content_with_error(response, start_offset, end_offset, encryption): + retry_counter.simple_count(retry) + conn_error = AzureError("Connection reset by peer") + if retry_counter.count == 1: + raise ServiceResponseError(conn_error, error=conn_error) + elif retry_counter.count == 2: + raise ServiceResponseTimeoutError(conn_error, error=conn_error) + return await real_process_content(response, start_offset, end_offset, encryption) + + # Act + try: + with mock.patch('azure.storage.blob.aio._download_async.process_content', side_effect=mock_process_content_with_error): + downloaded_data = await (await blob.download_blob()).readall() + assert downloaded_data == data + assert retry_counter.count >= 3 + finally: + await service.delete_container(container_name) + # ------------------------------------------------------------------------------ From 57daa54d29db4ffe6038ea0b0a904d39f6f22af4 Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Thu, 12 Feb 2026 00:36:30 -0500 Subject: [PATCH 2/2] Branch prep --- sdk/storage/azure-storage-blob/CHANGELOG.md | 9 +++++++++ .../azure-storage-blob/azure/storage/blob/_version.py | 2 +- sdk/storage/azure-storage-blob/setup.py | 2 +- sdk/storage/azure-storage-file-datalake/CHANGELOG.md | 5 +++++ .../azure/storage/filedatalake/_version.py | 2 +- sdk/storage/azure-storage-file-datalake/setup.py | 4 ++-- sdk/storage/azure-storage-file-share/CHANGELOG.md | 5 +++++ .../azure/storage/fileshare/_version.py | 2 +- sdk/storage/azure-storage-file-share/setup.py | 2 +- sdk/storage/azure-storage-queue/CHANGELOG.md | 5 +++++ .../azure-storage-queue/azure/storage/queue/_version.py | 2 +- sdk/storage/azure-storage-queue/setup.py | 2 +- 12 files changed, 33 insertions(+), 9 deletions(-) diff --git a/sdk/storage/azure-storage-blob/CHANGELOG.md b/sdk/storage/azure-storage-blob/CHANGELOG.md index a3836f9133c9..4d30154d7a32 100644 --- a/sdk/storage/azure-storage-blob/CHANGELOG.md +++ b/sdk/storage/azure-storage-blob/CHANGELOG.md @@ -1,5 +1,14 @@ # Release History +## 12.29.0 (Unreleased) + +### Features Added +- Stable release of features from 12.29.0b1 + +### Bugs Fixed +- Fixed an issue where `BlobClient`'s `download_blob` did not retry upon +`ServiceReponseError` and `ServiceResponseTimeoutError` exceptions + ## 12.29.0b1 (2026-01-27) ### Features Added diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_version.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_version.py index a462cf624d86..7ee472fd5aac 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_version.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_version.py @@ -4,4 +4,4 @@ # license information. # -------------------------------------------------------------------------- -VERSION = "12.29.0b1" +VERSION = "12.29.0" diff --git a/sdk/storage/azure-storage-blob/setup.py b/sdk/storage/azure-storage-blob/setup.py index 0abb2504e0fb..8a0ac2fffe32 100644 --- a/sdk/storage/azure-storage-blob/setup.py +++ b/sdk/storage/azure-storage-blob/setup.py @@ -56,7 +56,7 @@ url='https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/storage/azure-storage-blob', keywords="azure, azure sdk", classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Programming Language :: Python', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3', diff --git a/sdk/storage/azure-storage-file-datalake/CHANGELOG.md b/sdk/storage/azure-storage-file-datalake/CHANGELOG.md index c394591de45e..abbfe043e234 100644 --- a/sdk/storage/azure-storage-file-datalake/CHANGELOG.md +++ b/sdk/storage/azure-storage-file-datalake/CHANGELOG.md @@ -1,5 +1,10 @@ # Release History +## 12.24.0 (Unreleased) + +### Features Added +- Stable release of features from 12.24.0b1 + ## 12.24.0b1 (2026-01-27) ### Features Added diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_version.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_version.py index f67466f0741b..444bd6f8a740 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_version.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_version.py @@ -4,4 +4,4 @@ # license information. # -------------------------------------------------------------------------- -VERSION = "12.24.0b1" +VERSION = "12.24.0" diff --git a/sdk/storage/azure-storage-file-datalake/setup.py b/sdk/storage/azure-storage-file-datalake/setup.py index 4da70e2ca0ca..e5b5c551bdd5 100644 --- a/sdk/storage/azure-storage-file-datalake/setup.py +++ b/sdk/storage/azure-storage-file-datalake/setup.py @@ -57,7 +57,7 @@ url='https://github.com/Azure/azure-sdk-for-python', keywords="azure, azure sdk", classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Programming Language :: Python', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3', @@ -79,7 +79,7 @@ python_requires=">=3.9", install_requires=[ "azure-core>=1.37.0", - "azure-storage-blob>=12.29.0b1", + "azure-storage-blob>=12.29.0", "typing-extensions>=4.6.0", "isodate>=0.6.1" ], diff --git a/sdk/storage/azure-storage-file-share/CHANGELOG.md b/sdk/storage/azure-storage-file-share/CHANGELOG.md index 948dba1e270d..3d0eff4cd754 100644 --- a/sdk/storage/azure-storage-file-share/CHANGELOG.md +++ b/sdk/storage/azure-storage-file-share/CHANGELOG.md @@ -1,5 +1,10 @@ # Release History +## 12.25.0 (Unreleased) + +### Features Added +- Stable release of features from 12.25.0b1 + ## 12.25.0b1 (2026-01-27) ### Features Added diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_version.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_version.py index 5192aed4c84b..bb6f0bf88595 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_version.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_version.py @@ -4,4 +4,4 @@ # license information. # -------------------------------------------------------------------------- -VERSION = "12.25.0b1" +VERSION = "12.25.0" diff --git a/sdk/storage/azure-storage-file-share/setup.py b/sdk/storage/azure-storage-file-share/setup.py index 2e62c6db6a86..32afb6a76a16 100644 --- a/sdk/storage/azure-storage-file-share/setup.py +++ b/sdk/storage/azure-storage-file-share/setup.py @@ -45,7 +45,7 @@ url='https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/storage/azure-storage-file-share', keywords="azure, azure sdk", classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Programming Language :: Python', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3', diff --git a/sdk/storage/azure-storage-queue/CHANGELOG.md b/sdk/storage/azure-storage-queue/CHANGELOG.md index e6b4a2b5286d..98d1665b2665 100644 --- a/sdk/storage/azure-storage-queue/CHANGELOG.md +++ b/sdk/storage/azure-storage-queue/CHANGELOG.md @@ -1,5 +1,10 @@ # Release History +## 12.16.0 (Unreleased) + +### Features Added +- Stable release of features from 12.16.0b1 + ## 12.16.0b1 (2026-01-27) ### Features Added diff --git a/sdk/storage/azure-storage-queue/azure/storage/queue/_version.py b/sdk/storage/azure-storage-queue/azure/storage/queue/_version.py index 5f501737020d..45ccbc7d81dc 100644 --- a/sdk/storage/azure-storage-queue/azure/storage/queue/_version.py +++ b/sdk/storage/azure-storage-queue/azure/storage/queue/_version.py @@ -4,4 +4,4 @@ # license information. # -------------------------------------------------------------------------- -VERSION = "12.16.0b1" +VERSION = "12.16.0" diff --git a/sdk/storage/azure-storage-queue/setup.py b/sdk/storage/azure-storage-queue/setup.py index ac028b43a7a5..2824ae5c7345 100644 --- a/sdk/storage/azure-storage-queue/setup.py +++ b/sdk/storage/azure-storage-queue/setup.py @@ -45,7 +45,7 @@ url="https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/storage/azure-storage-queue", keywords="azure, azure sdk", classifiers=[ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3",