Skip to content

Commit cd95be7

Browse files
committed
Azure: wait for active publishing before executing
THis commit creates a new method for `AzureService` named `wait_active_publishing` which relies on `ensure_can_publish` to verify whether changes are being made or not and wait until it's possible to proceed or timeout if necessary. The timeout and retry interval are now possible to be set during the class construction as optional parameters. With this the `publish` method will double check before doing any changes: 1. Before loading the product information it will wait for active changes so it can retrieve the "latest" content 2. During the publishing phase, if it's no longer the latest change it will raise from `ensure_can_publish` Refers to SPSTRAT-611 and SPSTRAT-549 Signed-off-by: Jonathan Gangi <[email protected]>
1 parent bc4c05e commit cd95be7

File tree

3 files changed

+101
-6
lines changed

3 files changed

+101
-6
lines changed

cloudpub/ms_azure/service.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66

77
from deepdiff import DeepDiff
88
from requests import HTTPError
9-
from tenacity import retry
10-
from tenacity.retry import retry_if_result
9+
from tenacity import RetryError, Retrying, retry
10+
from tenacity.retry import retry_if_exception_type, retry_if_result
1111
from tenacity.stop import stop_after_attempt, stop_after_delay
1212
from tenacity.wait import wait_chain, wait_fixed
1313

1414
from cloudpub.common import BaseService
15-
from cloudpub.error import ConflictError, InvalidStateError, NotFoundError
15+
from cloudpub.error import ConflictError, InvalidStateError, NotFoundError, Timeout
1616
from cloudpub.models.ms_azure import (
1717
RESOURCE_MAPING,
1818
AzureResource,
@@ -82,18 +82,31 @@ class AzureService(BaseService[AzurePublishingMetadata]):
8282
CONFIGURE_SCHEMA = "https://schema.mp.microsoft.com/schema/configure/{AZURE_API_VERSION}"
8383
DIFF_EXCLUDES = [r"root\['resources'\]\[[0-9]+\]\['url'\]"]
8484

85-
def __init__(self, credentials: Dict[str, str]):
85+
def __init__(
86+
self,
87+
credentials: Dict[str, str],
88+
retry_interval: int = 300,
89+
retry_timeout: int = 3600 * 24 * 7,
90+
):
8691
"""
8792
Create a new AuzureService object.
8893
8994
Args:
9095
credentials (dict)
9196
Dictionary with Azure credentials to authenticate on Product Ingestion API.
97+
retry_interval (int)
98+
The wait time interval in seconds for retrying jobs.
99+
Defaults to 300
100+
retry_timeout (int)
101+
The max time in seconds to attempt retries.
102+
Defaults to 7 days.
92103
"""
93104
self.session = PartnerPortalSession.make_graph_api_session(
94105
auth_keys=credentials, schema_version=self.AZURE_SCHEMA_VERSION
95106
)
96107
self._products: List[ProductSummary] = []
108+
self.retry_interval = retry_interval
109+
self.retry_timeout = retry_timeout
97110

98111
def _configure(self, data: Dict[str, Any]) -> ConfigureStatus:
99112
"""
@@ -490,6 +503,26 @@ def ensure_can_publish(self, product_id: str) -> None:
490503
log.error(msg)
491504
raise ConflictError(msg)
492505

506+
def wait_active_publishing(self, product_id: str) -> None:
507+
"""
508+
Wait when there's an existing submission in progress.
509+
510+
Args:
511+
product_id (str)
512+
The product ID of to verify the submissions state.
513+
"""
514+
r = Retrying(
515+
retry=retry_if_exception_type(ConflictError),
516+
wait=wait_fixed(self.retry_interval),
517+
stop=stop_after_delay(max_delay=self.retry_timeout),
518+
)
519+
log.info("Checking for active changes on %s.", product_id)
520+
521+
try:
522+
r(self.ensure_can_publish, product_id)
523+
except RetryError:
524+
self._raise_error(Timeout, f"Timed out waiting for {product_id} to be unlocked")
525+
493526
def get_plan_tech_config(self, product: Product, plan: PlanSummary) -> VMIPlanTechConfig:
494527
"""
495528
Return the VMIPlanTechConfig resource for the given product/plan.
@@ -815,6 +848,7 @@ def publish(self, metadata: AzurePublishingMetadata) -> None:
815848
plan_name = metadata.destination.split("/")[-1]
816849
product_id = self.get_productid(product_name)
817850
disk_version = None
851+
self.wait_active_publishing(product_id=product_id)
818852
log.info(
819853
"Preparing to associate the image \"%s\" with the plan \"%s\" from product \"%s\"",
820854
metadata.image_path,

tests/ms_azure/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def auth_dict() -> Dict[str, str]:
5656
@mock.patch("cloudpub.ms_azure.service.PartnerPortalSession")
5757
def azure_service(auth_dict: Dict[str, str]) -> AzureService:
5858
"""Return an instance of AzureService with mocked PartnerPortalSession."""
59-
return AzureService(auth_dict)
59+
return AzureService(auth_dict, retry_interval=0, retry_timeout=10)
6060

6161

6262
def job_details(status: str, result: str, errors: List[Dict[str, Any]]) -> Dict[str, Any]:

tests/ms_azure/test_service.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from requests.exceptions import HTTPError
1313

1414
from cloudpub.common import BaseService
15-
from cloudpub.error import ConflictError, InvalidStateError, NotFoundError
15+
from cloudpub.error import ConflictError, InvalidStateError, NotFoundError, Timeout
1616
from cloudpub.models.ms_azure import (
1717
ConfigureStatus,
1818
CustomerLeads,
@@ -816,6 +816,37 @@ def test_ensure_can_publish_raises(
816816
with pytest.raises(RuntimeError, match=err):
817817
azure_service.ensure_can_publish("ffffffff-ffff-ffff-ffff-ffffffffffff")
818818

819+
@mock.patch("cloudpub.ms_azure.AzureService.ensure_can_publish")
820+
def test_wait_active_publishing_success(
821+
self, mock_ensure_publish: mock.MagicMock, azure_service: AzureService
822+
):
823+
# The test will simlulate 3 submissoins in progress to wait for
824+
mock_ensure_publish.side_effect = [
825+
ConflictError("Submission in progress"),
826+
ConflictError("Submission in progress"),
827+
ConflictError("Submission in progress"),
828+
None,
829+
]
830+
831+
# Test
832+
azure_service.wait_active_publishing("fake-product")
833+
mock_ensure_publish.assert_has_calls([mock.call("fake-product") for _ in range(4)])
834+
835+
@mock.patch("cloudpub.ms_azure.AzureService.ensure_can_publish")
836+
def test_wait_active_publishing_timeout(
837+
self, mock_ensure_publish: mock.MagicMock, azure_service: AzureService
838+
) -> None:
839+
mock_ensure_publish.side_effect = [
840+
ConflictError("Submission in progress") for _ in range(15)
841+
]
842+
err = "Timed out waiting for fake-product to be unlocked"
843+
azure_service.retry_interval = 0.1
844+
azure_service.retry_timeout = 0.5
845+
846+
# Test
847+
with pytest.raises(Timeout, match=err):
848+
azure_service.wait_active_publishing("fake-product")
849+
819850
@mock.patch("cloudpub.ms_azure.AzureService.get_submission_state")
820851
@mock.patch("cloudpub.ms_azure.AzureService.submit_to_status")
821852
@mock.patch("cloudpub.ms_azure.AzureService._is_submission_in_preview")
@@ -947,6 +978,7 @@ def test_publish_live_fail_on_retry(
947978
with pytest.raises(RuntimeError, match=expected_err):
948979
azure_service._publish_live(product_obj, "test-product")
949980

981+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
950982
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
951983
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
952984
@mock.patch("cloudpub.ms_azure.AzureService.configure")
@@ -955,6 +987,7 @@ def test_publish_live_fail_conflict(
955987
mock_configure: mock.MagicMock,
956988
mock_get_productid: mock.MagicMock,
957989
mock_compute_targets: mock.MagicMock,
990+
mock_wait_publish: mock.MagicMock,
958991
token: Dict[str, Any],
959992
auth_dict: Dict[str, Any],
960993
configure_success_response: Dict[str, Any],
@@ -1014,7 +1047,9 @@ def test_publish_live_fail_conflict(
10141047

10151048
with pytest.raises(ConflictError, match=err):
10161049
azure_svc.publish(metadata=metadata_azure_obj)
1050+
mock_wait_publish.assert_called_once()
10171051

1052+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
10181053
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
10191054
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
10201055
@mock.patch("cloudpub.ms_azure.AzureService.configure")
@@ -1037,6 +1072,7 @@ def test_publish_overwrite(
10371072
mock_configure: mock.MagicMock,
10381073
mock_get_productid: mock.MagicMock,
10391074
mock_compute_targets: mock.MagicMock,
1075+
mock_wait_publish: mock.MagicMock,
10401076
product_obj: Product,
10411077
plan_summary_obj: PlanSummary,
10421078
metadata_azure_obj: AzurePublishingMetadata,
@@ -1063,6 +1099,7 @@ def test_publish_overwrite(
10631099

10641100
azure_service.publish(metadata_azure_obj)
10651101

1102+
mock_wait_publish.assert_called_once()
10661103
mock_getprpl_name.assert_called_once_with("example-product", "plan-1", 'draft')
10671104
mock_filter.assert_called_once_with(
10681105
product=product_obj, resource="virtual-machine-plan-technical-configuration"
@@ -1079,6 +1116,7 @@ def test_publish_overwrite(
10791116
mock_configure.assert_called_once_with(resources=[technical_config_obj])
10801117
mock_submit.assert_not_called()
10811118

1119+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
10821120
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
10831121
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
10841122
@mock.patch("cloudpub.ms_azure.AzureService.configure")
@@ -1101,6 +1139,7 @@ def test_publish_nodiskversion(
11011139
mock_configure: mock.MagicMock,
11021140
mock_get_productid: mock.MagicMock,
11031141
mock_compute_targets: mock.MagicMock,
1142+
mock_wait_publish: mock.MagicMock,
11041143
product_obj: Product,
11051144
plan_summary_obj: PlanSummary,
11061145
metadata_azure_obj: AzurePublishingMetadata,
@@ -1135,6 +1174,7 @@ def test_publish_nodiskversion(
11351174

11361175
azure_service.publish(metadata_azure_obj)
11371176

1177+
mock_wait_publish.assert_called_once()
11381178
mock_getprpl_name.assert_has_calls(
11391179
[mock.call("example-product", "plan-1", tgt) for tgt in targets]
11401180
)
@@ -1164,6 +1204,7 @@ def test_publish_nodiskversion(
11641204
mock_submit.assert_not_called()
11651205

11661206
@pytest.mark.parametrize("keepdraft", [True, False], ids=["nochannel", "push"])
1207+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
11671208
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
11681209
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
11691210
@mock.patch("cloudpub.ms_azure.AzureService.configure")
@@ -1188,6 +1229,7 @@ def test_publish_saspresent(
11881229
mock_configure: mock.MagicMock,
11891230
mock_get_productid: mock.MagicMock,
11901231
mock_compute_targets: mock.MagicMock,
1232+
mock_wait_publish: mock.MagicMock,
11911233
keepdraft: bool,
11921234
product_obj: Product,
11931235
plan_summary_obj: PlanSummary,
@@ -1211,6 +1253,7 @@ def test_publish_saspresent(
12111253

12121254
azure_service.publish(metadata_azure_obj)
12131255

1256+
mock_wait_publish.assert_called_once()
12141257
mock_getprpl_name.assert_called_once_with("example-product", "plan-1", "preview")
12151258
mock_filter.assert_has_calls(
12161259
[
@@ -1230,6 +1273,7 @@ def test_publish_saspresent(
12301273
mock_configure.assert_not_called()
12311274
mock_submit.assert_not_called()
12321275

1276+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
12331277
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
12341278
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
12351279
@mock.patch("cloudpub.ms_azure.AzureService.configure")
@@ -1250,6 +1294,7 @@ def test_publish_novmimages(
12501294
mock_configure: mock.MagicMock,
12511295
mock_get_productid: mock.MagicMock,
12521296
mock_compute_targets: mock.MagicMock,
1297+
mock_wait_publish: mock.MagicMock,
12531298
product_obj: Product,
12541299
plan_summary_obj: PlanSummary,
12551300
metadata_azure_obj: AzurePublishingMetadata,
@@ -1291,6 +1336,7 @@ def test_publish_novmimages(
12911336

12921337
azure_service.publish(metadata_azure_obj)
12931338

1339+
mock_wait_publish.assert_called_once()
12941340
mock_getprpl_name.assert_has_calls(
12951341
[mock.call("example-product", "plan-1", tgt) for tgt in targets]
12961342
)
@@ -1317,6 +1363,7 @@ def test_publish_novmimages(
13171363
mock_configure.assert_called_once_with(resources=[expected_tech_config])
13181364
mock_submit.assert_not_called()
13191365

1366+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
13201367
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
13211368
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
13221369
@mock.patch("cloudpub.ms_azure.AzureService.configure")
@@ -1337,6 +1384,7 @@ def test_publish_disk_has_images(
13371384
mock_configure: mock.MagicMock,
13381385
mock_get_productid: mock.MagicMock,
13391386
mock_compute_targets: mock.MagicMock,
1387+
mock_wait_publish: mock.MagicMock,
13401388
product_obj: Product,
13411389
plan_summary_obj: PlanSummary,
13421390
metadata_azure_obj: AzurePublishingMetadata,
@@ -1377,6 +1425,7 @@ def test_publish_disk_has_images(
13771425

13781426
azure_service.publish(metadata_azure_obj)
13791427

1428+
mock_wait_publish.assert_called_once()
13801429
mock_getprpl_name.assert_has_calls(
13811430
[mock.call("example-product", "plan-1", tgt) for tgt in targets]
13821431
)
@@ -1447,6 +1496,7 @@ def test_is_submission_in_preview(
14471496
assert res is True
14481497
mock_substt.assert_called_once_with(current.product_id, "live")
14491498

1499+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
14501500
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
14511501
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
14521502
@mock.patch("cloudpub.ms_azure.AzureService.ensure_can_publish")
@@ -1473,6 +1523,7 @@ def test_publish_live_x64_only(
14731523
mock_ensure_publish: mock.MagicMock,
14741524
mock_get_productid: mock.MagicMock,
14751525
mock_compute_targets: mock.MagicMock,
1526+
mock_wait_publish: mock.MagicMock,
14761527
product_obj: Product,
14771528
plan_summary_obj: PlanSummary,
14781529
metadata_azure_obj: AzurePublishingMetadata,
@@ -1521,6 +1572,7 @@ def test_publish_live_x64_only(
15211572
# Test
15221573
azure_service.publish(metadata_azure_obj)
15231574

1575+
mock_wait_publish.assert_called_once()
15241576
mock_getprpl_name.assert_has_calls(
15251577
[mock.call("example-product", "plan-1", tgt) for tgt in targets]
15261578
)
@@ -1555,6 +1607,7 @@ def test_publish_live_x64_only(
15551607
mock_submit.assert_has_calls(submit_calls)
15561608
mock_ensure_publish.assert_called_once_with(product_obj.id)
15571609

1610+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
15581611
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
15591612
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
15601613
@mock.patch("cloudpub.ms_azure.AzureService.ensure_can_publish")
@@ -1581,6 +1634,7 @@ def test_publish_live_arm64_only(
15811634
mock_ensure_publish: mock.MagicMock,
15821635
mock_get_productid: mock.MagicMock,
15831636
mock_compute_targets: mock.MagicMock,
1637+
mock_wait_publish: mock.MagicMock,
15841638
product_obj: Product,
15851639
plan_summary_obj: PlanSummary,
15861640
metadata_azure_obj: AzurePublishingMetadata,
@@ -1630,6 +1684,7 @@ def test_publish_live_arm64_only(
16301684
# Test
16311685
azure_service.publish(metadata_azure_obj)
16321686

1687+
mock_wait_publish.assert_called_once()
16331688
mock_getprpl_name.assert_has_calls(
16341689
[mock.call("example-product", "plan-1", tgt) for tgt in targets]
16351690
)
@@ -1664,6 +1719,7 @@ def test_publish_live_arm64_only(
16641719
mock_submit.assert_has_calls(submit_calls)
16651720
mock_ensure_publish.assert_called_once_with(product_obj.id)
16661721

1722+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
16671723
@mock.patch("cloudpub.ms_azure.AzureService.ensure_can_publish")
16681724
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
16691725
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
@@ -1672,6 +1728,7 @@ def test_publish_live_when_state_is_preview(
16721728
mock_get_productid: mock.MagicMock,
16731729
mock_compute_targets: mock.MagicMock,
16741730
mock_ensure_publish: mock.MagicMock,
1731+
mock_wait_publish: mock.MagicMock,
16751732
token: Dict[str, Any],
16761733
auth_dict: Dict[str, Any],
16771734
configure_running_response: Dict[str, Any],
@@ -1791,8 +1848,10 @@ def test_publish_live_when_state_is_preview(
17911848
'Updating the technical configuration for "example-product/plan-1" on "preview".'
17921849
not in caplog.text
17931850
)
1851+
mock_wait_publish.assert_called_once()
17941852
mock_ensure_publish.assert_called_once()
17951853

1854+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
17961855
@mock.patch("cloudpub.ms_azure.AzureService.ensure_can_publish")
17971856
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
17981857
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
@@ -1803,6 +1862,7 @@ def test_publish_live_modular_push(
18031862
mock_get_productid: mock.MagicMock,
18041863
mock_compute_targets: mock.MagicMock,
18051864
mock_ensure_publish: mock.MagicMock,
1865+
mock_wait_publish: mock.MagicMock,
18061866
token: Dict[str, Any],
18071867
auth_dict: Dict[str, Any],
18081868
configure_success_response: Dict[str, Any],
@@ -1896,6 +1956,7 @@ def test_publish_live_modular_push(
18961956
'Performing a modular push to "preview" for "ffffffff-ffff-ffff-ffff-ffffffffffff"'
18971957
in caplog.text
18981958
)
1959+
mock_wait_publish.assert_called_once()
18991960
mock_ensure_publish.assert_called_once()
19001961

19011962
# Configure request

0 commit comments

Comments
 (0)