From b5e326b078ae7663e6369996b359a8403b31225d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 09:07:45 +0000 Subject: [PATCH 1/6] Initial plan From 49eae0a04d9da7ac49d612431e7200074f87e5a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 09:19:20 +0000 Subject: [PATCH 2/6] Retry sdk_no_wait on provisioning-state bad request --- .../azure/cli/core/tests/test_util.py | 43 ++++++++++++++++++- src/azure-cli-core/azure/cli/core/util.py | 21 ++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/tests/test_util.py b/src/azure-cli-core/azure/cli/core/tests/test_util.py index ae329ed65b7..a2c4de303e8 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_util.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_util.py @@ -17,7 +17,7 @@ (get_file_json, truncate_text, shell_safe_json_parse, b64_to_hex, hash_string, random_string, open_page_in_browser, can_launch_browser, handle_exception, ConfiguredDefaultSetter, send_raw_request, should_disable_connection_verify, parse_proxy_resource_id, get_az_user_agent, get_az_rest_user_agent, - _get_parent_proc_name, is_wsl, run_cmd, run_az_cmd, roughly_parse_command) + _get_parent_proc_name, is_wsl, run_cmd, run_az_cmd, roughly_parse_command, sdk_no_wait) from azure.cli.core.mock import DummyCli @@ -463,6 +463,47 @@ def test_run_az_cmd(self): self.assertIsInstance(output.result, dict, "unexpected cmd execution result") self.assertIn("azure-cli-core", output.result, "unexpected cmd execution result") + @mock.patch('time.sleep', autospec=True) + def test_sdk_no_wait_retries_on_provisioning_bad_request(self, sleep_mock): + class MockHttpError(Exception): + status_code = 400 + message = 'Resource cannot be updated during provisioning' + + operation = mock.Mock(side_effect=[MockHttpError(), MockHttpError(), 'ok']) + result = sdk_no_wait(False, operation) + + self.assertEqual(result, 'ok') + self.assertEqual(operation.call_count, 3) + self.assertEqual(sleep_mock.call_count, 2) + + @mock.patch('time.sleep', autospec=True) + def test_sdk_no_wait_no_wait_does_not_retry(self, sleep_mock): + class MockHttpError(Exception): + status_code = 400 + message = 'Resource cannot be updated during provisioning' + + operation = mock.Mock(side_effect=MockHttpError()) + + with self.assertRaises(MockHttpError): + sdk_no_wait(True, operation) + + self.assertEqual(operation.call_count, 1) + self.assertEqual(sleep_mock.call_count, 0) + + @mock.patch('time.sleep', autospec=True) + def test_sdk_no_wait_non_matching_error_no_retry(self, sleep_mock): + class MockHttpError(Exception): + status_code = 400 + message = 'A different bad request' + + operation = mock.Mock(side_effect=MockHttpError()) + + with self.assertRaises(MockHttpError): + sdk_no_wait(False, operation) + + self.assertEqual(operation.call_count, 1) + self.assertEqual(sleep_mock.call_count, 0) + class TestBase64ToHex(unittest.TestCase): diff --git a/src/azure-cli-core/azure/cli/core/util.py b/src/azure-cli-core/azure/cli/core/util.py index bc572f3efe6..909a112a1b2 100644 --- a/src/azure-cli-core/azure/cli/core/util.py +++ b/src/azure-cli-core/azure/cli/core/util.py @@ -790,7 +790,26 @@ def augment_no_wait_handler_args(no_wait_enabled, handler, handler_args): def sdk_no_wait(no_wait, func, *args, **kwargs): if no_wait: kwargs.update({'polling': False}) - return func(*args, **kwargs) + + retry_attempts = 6 + retry_interval_in_seconds = 10 + for attempt in range(retry_attempts): + try: + return func(*args, **kwargs) + except Exception as ex: # pylint: disable=broad-except + error_msg = getattr(ex, 'message', str(ex)) + status_code = getattr(ex, 'status_code', None) + if not ( + not no_wait and + status_code == 400 and + 'resource cannot be updated during provisioning' in error_msg.lower() and + attempt < retry_attempts - 1 + ): + raise + + logger.warning("Resource is still provisioning. Retrying in %s seconds...", retry_interval_in_seconds) + from time import sleep + sleep(retry_interval_in_seconds) def open_page_in_browser(url): From 3ff3944de3ed873cd9c200d804360efa6614b595 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 09:20:05 +0000 Subject: [PATCH 3/6] Refine provisioning retry logic readability --- src/azure-cli-core/azure/cli/core/util.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/util.py b/src/azure-cli-core/azure/cli/core/util.py index 909a112a1b2..07370fecb2e 100644 --- a/src/azure-cli-core/azure/cli/core/util.py +++ b/src/azure-cli-core/azure/cli/core/util.py @@ -10,6 +10,7 @@ import platform import re import sys +import time from knack.log import get_logger from knack.util import CLIError, to_snake_case, to_camel_case @@ -799,17 +800,17 @@ def sdk_no_wait(no_wait, func, *args, **kwargs): except Exception as ex: # pylint: disable=broad-except error_msg = getattr(ex, 'message', str(ex)) status_code = getattr(ex, 'status_code', None) - if not ( + should_retry = ( not no_wait and status_code == 400 and 'resource cannot be updated during provisioning' in error_msg.lower() and attempt < retry_attempts - 1 - ): + ) + if not should_retry: raise logger.warning("Resource is still provisioning. Retrying in %s seconds...", retry_interval_in_seconds) - from time import sleep - sleep(retry_interval_in_seconds) + time.sleep(retry_interval_in_seconds) def open_page_in_browser(url): From 4df0df5966af171a17e45139b849a88822550e56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 09:22:03 +0000 Subject: [PATCH 4/6] Polish provisioning retry safeguards --- src/azure-cli-core/azure/cli/core/util.py | 43 ++++++++++++++--------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/util.py b/src/azure-cli-core/azure/cli/core/util.py index 07370fecb2e..86a71caa786 100644 --- a/src/azure-cli-core/azure/cli/core/util.py +++ b/src/azure-cli-core/azure/cli/core/util.py @@ -53,6 +53,16 @@ # - https://azcliprod.blob.core.windows.net/cli/vm/aliases.json (VM image aliases) AME_STORAGE_BASE_URL = "https://azcliprod.blob.core.windows.net/cli" +_PROVISIONING_RETRY_ATTEMPTS = 6 +_PROVISIONING_RETRY_INTERVAL_SECONDS = 10 +_PROVISIONING_RETRYABLE_ERROR = 'resource cannot be updated during provisioning' + + +def _is_provisioning_retryable_error(ex): + error_msg = getattr(ex, 'message', str(ex)) + status_code = getattr(ex, 'status_code', None) + return status_code == 400 and _PROVISIONING_RETRYABLE_ERROR in error_msg.lower() + def handle_exception(ex): # pylint: disable=too-many-locals, too-many-statements, too-many-branches # For error code, follow guidelines at https://docs.python.org/2/library/sys.html#sys.exit, @@ -792,25 +802,26 @@ def sdk_no_wait(no_wait, func, *args, **kwargs): if no_wait: kwargs.update({'polling': False}) - retry_attempts = 6 - retry_interval_in_seconds = 10 - for attempt in range(retry_attempts): + try: + return func(*args, **kwargs) + except Exception as ex: # pylint: disable=broad-except + if no_wait or not _is_provisioning_retryable_error(ex): + raise + initial_ex = ex + + retry_ex = None + # The first attempt has already been made above. + for _ in range(_PROVISIONING_RETRY_ATTEMPTS - 1): + logger.warning("Resource is still provisioning. Retrying in %s seconds...", + _PROVISIONING_RETRY_INTERVAL_SECONDS) + time.sleep(_PROVISIONING_RETRY_INTERVAL_SECONDS) try: return func(*args, **kwargs) - except Exception as ex: # pylint: disable=broad-except - error_msg = getattr(ex, 'message', str(ex)) - status_code = getattr(ex, 'status_code', None) - should_retry = ( - not no_wait and - status_code == 400 and - 'resource cannot be updated during provisioning' in error_msg.lower() and - attempt < retry_attempts - 1 - ) - if not should_retry: + except Exception as ex_after_retry: # pylint: disable=broad-except + retry_ex = ex_after_retry + if not _is_provisioning_retryable_error(retry_ex): raise - - logger.warning("Resource is still provisioning. Retrying in %s seconds...", retry_interval_in_seconds) - time.sleep(retry_interval_in_seconds) + raise retry_ex or initial_ex def open_page_in_browser(url): From fa24cb929cff9b5243784c13f4e21c884173c52d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 09:22:56 +0000 Subject: [PATCH 5/6] Clarify sdk_no_wait retry flow --- src/azure-cli-core/azure/cli/core/util.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/util.py b/src/azure-cli-core/azure/cli/core/util.py index 86a71caa786..78dec38eda4 100644 --- a/src/azure-cli-core/azure/cli/core/util.py +++ b/src/azure-cli-core/azure/cli/core/util.py @@ -807,21 +807,17 @@ def sdk_no_wait(no_wait, func, *args, **kwargs): except Exception as ex: # pylint: disable=broad-except if no_wait or not _is_provisioning_retryable_error(ex): raise - initial_ex = ex - retry_ex = None - # The first attempt has already been made above. - for _ in range(_PROVISIONING_RETRY_ATTEMPTS - 1): + # Attempt 1 failed with a retryable provisioning-state error, so retry attempts 2..N. + for retry_attempt in range(2, _PROVISIONING_RETRY_ATTEMPTS + 1): logger.warning("Resource is still provisioning. Retrying in %s seconds...", _PROVISIONING_RETRY_INTERVAL_SECONDS) time.sleep(_PROVISIONING_RETRY_INTERVAL_SECONDS) try: return func(*args, **kwargs) - except Exception as ex_after_retry: # pylint: disable=broad-except - retry_ex = ex_after_retry - if not _is_provisioning_retryable_error(retry_ex): + except Exception as retry_ex: # pylint: disable=broad-except + if not _is_provisioning_retryable_error(retry_ex) or retry_attempt == _PROVISIONING_RETRY_ATTEMPTS: raise - raise retry_ex or initial_ex def open_page_in_browser(url): From 38065e5b3b8a96720b6e5514e6f4e442cb6b6d52 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 09:23:44 +0000 Subject: [PATCH 6/6] Tidy sdk_no_wait attempt naming --- src/azure-cli-core/azure/cli/core/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/util.py b/src/azure-cli-core/azure/cli/core/util.py index 78dec38eda4..67a814d7b1b 100644 --- a/src/azure-cli-core/azure/cli/core/util.py +++ b/src/azure-cli-core/azure/cli/core/util.py @@ -809,14 +809,14 @@ def sdk_no_wait(no_wait, func, *args, **kwargs): raise # Attempt 1 failed with a retryable provisioning-state error, so retry attempts 2..N. - for retry_attempt in range(2, _PROVISIONING_RETRY_ATTEMPTS + 1): + for attempt_number in range(2, _PROVISIONING_RETRY_ATTEMPTS + 1): logger.warning("Resource is still provisioning. Retrying in %s seconds...", _PROVISIONING_RETRY_INTERVAL_SECONDS) time.sleep(_PROVISIONING_RETRY_INTERVAL_SECONDS) try: return func(*args, **kwargs) except Exception as retry_ex: # pylint: disable=broad-except - if not _is_provisioning_retryable_error(retry_ex) or retry_attempt == _PROVISIONING_RETRY_ATTEMPTS: + if not _is_provisioning_retryable_error(retry_ex) or attempt_number >= _PROVISIONING_RETRY_ATTEMPTS: raise