Skip to content

Commit 9d5a1f0

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 <jgangi@redhat.com>
1 parent bc4c05e commit 9d5a1f0

File tree

3 files changed

+100
-6
lines changed

3 files changed

+100
-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 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 ConflictError:
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: 61 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,36 @@ 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+
# The test shuould timeout after 10 attempts
840+
mock_ensure_publish.side_effect = [
841+
ConflictError("Submission in progress") for _ in range(11)
842+
]
843+
err = "Timed out waiting for fake-product to be unlocked"
844+
845+
# Test
846+
with pytest.raises(Timeout, match=err):
847+
azure_service.wait_active_publishing("fake-product")
848+
819849
@mock.patch("cloudpub.ms_azure.AzureService.get_submission_state")
820850
@mock.patch("cloudpub.ms_azure.AzureService.submit_to_status")
821851
@mock.patch("cloudpub.ms_azure.AzureService._is_submission_in_preview")
@@ -947,6 +977,7 @@ def test_publish_live_fail_on_retry(
947977
with pytest.raises(RuntimeError, match=expected_err):
948978
azure_service._publish_live(product_obj, "test-product")
949979

980+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
950981
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
951982
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
952983
@mock.patch("cloudpub.ms_azure.AzureService.configure")
@@ -955,6 +986,7 @@ def test_publish_live_fail_conflict(
955986
mock_configure: mock.MagicMock,
956987
mock_get_productid: mock.MagicMock,
957988
mock_compute_targets: mock.MagicMock,
989+
mock_wait_publish: mock.MagicMock,
958990
token: Dict[str, Any],
959991
auth_dict: Dict[str, Any],
960992
configure_success_response: Dict[str, Any],
@@ -1014,7 +1046,9 @@ def test_publish_live_fail_conflict(
10141046

10151047
with pytest.raises(ConflictError, match=err):
10161048
azure_svc.publish(metadata=metadata_azure_obj)
1049+
mock_wait_publish.assert_called_once()
10171050

1051+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
10181052
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
10191053
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
10201054
@mock.patch("cloudpub.ms_azure.AzureService.configure")
@@ -1037,6 +1071,7 @@ def test_publish_overwrite(
10371071
mock_configure: mock.MagicMock,
10381072
mock_get_productid: mock.MagicMock,
10391073
mock_compute_targets: mock.MagicMock,
1074+
mock_wait_publish: mock.MagicMock,
10401075
product_obj: Product,
10411076
plan_summary_obj: PlanSummary,
10421077
metadata_azure_obj: AzurePublishingMetadata,
@@ -1063,6 +1098,7 @@ def test_publish_overwrite(
10631098

10641099
azure_service.publish(metadata_azure_obj)
10651100

1101+
mock_wait_publish.assert_called_once()
10661102
mock_getprpl_name.assert_called_once_with("example-product", "plan-1", 'draft')
10671103
mock_filter.assert_called_once_with(
10681104
product=product_obj, resource="virtual-machine-plan-technical-configuration"
@@ -1079,6 +1115,7 @@ def test_publish_overwrite(
10791115
mock_configure.assert_called_once_with(resources=[technical_config_obj])
10801116
mock_submit.assert_not_called()
10811117

1118+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
10821119
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
10831120
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
10841121
@mock.patch("cloudpub.ms_azure.AzureService.configure")
@@ -1101,6 +1138,7 @@ def test_publish_nodiskversion(
11011138
mock_configure: mock.MagicMock,
11021139
mock_get_productid: mock.MagicMock,
11031140
mock_compute_targets: mock.MagicMock,
1141+
mock_wait_publish: mock.MagicMock,
11041142
product_obj: Product,
11051143
plan_summary_obj: PlanSummary,
11061144
metadata_azure_obj: AzurePublishingMetadata,
@@ -1135,6 +1173,7 @@ def test_publish_nodiskversion(
11351173

11361174
azure_service.publish(metadata_azure_obj)
11371175

1176+
mock_wait_publish.assert_called_once()
11381177
mock_getprpl_name.assert_has_calls(
11391178
[mock.call("example-product", "plan-1", tgt) for tgt in targets]
11401179
)
@@ -1164,6 +1203,7 @@ def test_publish_nodiskversion(
11641203
mock_submit.assert_not_called()
11651204

11661205
@pytest.mark.parametrize("keepdraft", [True, False], ids=["nochannel", "push"])
1206+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
11671207
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
11681208
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
11691209
@mock.patch("cloudpub.ms_azure.AzureService.configure")
@@ -1188,6 +1228,7 @@ def test_publish_saspresent(
11881228
mock_configure: mock.MagicMock,
11891229
mock_get_productid: mock.MagicMock,
11901230
mock_compute_targets: mock.MagicMock,
1231+
mock_wait_publish: mock.MagicMock,
11911232
keepdraft: bool,
11921233
product_obj: Product,
11931234
plan_summary_obj: PlanSummary,
@@ -1211,6 +1252,7 @@ def test_publish_saspresent(
12111252

12121253
azure_service.publish(metadata_azure_obj)
12131254

1255+
mock_wait_publish.assert_called_once()
12141256
mock_getprpl_name.assert_called_once_with("example-product", "plan-1", "preview")
12151257
mock_filter.assert_has_calls(
12161258
[
@@ -1230,6 +1272,7 @@ def test_publish_saspresent(
12301272
mock_configure.assert_not_called()
12311273
mock_submit.assert_not_called()
12321274

1275+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
12331276
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
12341277
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
12351278
@mock.patch("cloudpub.ms_azure.AzureService.configure")
@@ -1250,6 +1293,7 @@ def test_publish_novmimages(
12501293
mock_configure: mock.MagicMock,
12511294
mock_get_productid: mock.MagicMock,
12521295
mock_compute_targets: mock.MagicMock,
1296+
mock_wait_publish: mock.MagicMock,
12531297
product_obj: Product,
12541298
plan_summary_obj: PlanSummary,
12551299
metadata_azure_obj: AzurePublishingMetadata,
@@ -1291,6 +1335,7 @@ def test_publish_novmimages(
12911335

12921336
azure_service.publish(metadata_azure_obj)
12931337

1338+
mock_wait_publish.assert_called_once()
12941339
mock_getprpl_name.assert_has_calls(
12951340
[mock.call("example-product", "plan-1", tgt) for tgt in targets]
12961341
)
@@ -1317,6 +1362,7 @@ def test_publish_novmimages(
13171362
mock_configure.assert_called_once_with(resources=[expected_tech_config])
13181363
mock_submit.assert_not_called()
13191364

1365+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
13201366
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
13211367
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
13221368
@mock.patch("cloudpub.ms_azure.AzureService.configure")
@@ -1337,6 +1383,7 @@ def test_publish_disk_has_images(
13371383
mock_configure: mock.MagicMock,
13381384
mock_get_productid: mock.MagicMock,
13391385
mock_compute_targets: mock.MagicMock,
1386+
mock_wait_publish: mock.MagicMock,
13401387
product_obj: Product,
13411388
plan_summary_obj: PlanSummary,
13421389
metadata_azure_obj: AzurePublishingMetadata,
@@ -1377,6 +1424,7 @@ def test_publish_disk_has_images(
13771424

13781425
azure_service.publish(metadata_azure_obj)
13791426

1427+
mock_wait_publish.assert_called_once()
13801428
mock_getprpl_name.assert_has_calls(
13811429
[mock.call("example-product", "plan-1", tgt) for tgt in targets]
13821430
)
@@ -1447,6 +1495,7 @@ def test_is_submission_in_preview(
14471495
assert res is True
14481496
mock_substt.assert_called_once_with(current.product_id, "live")
14491497

1498+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
14501499
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
14511500
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
14521501
@mock.patch("cloudpub.ms_azure.AzureService.ensure_can_publish")
@@ -1473,6 +1522,7 @@ def test_publish_live_x64_only(
14731522
mock_ensure_publish: mock.MagicMock,
14741523
mock_get_productid: mock.MagicMock,
14751524
mock_compute_targets: mock.MagicMock,
1525+
mock_wait_publish: mock.MagicMock,
14761526
product_obj: Product,
14771527
plan_summary_obj: PlanSummary,
14781528
metadata_azure_obj: AzurePublishingMetadata,
@@ -1521,6 +1571,7 @@ def test_publish_live_x64_only(
15211571
# Test
15221572
azure_service.publish(metadata_azure_obj)
15231573

1574+
mock_wait_publish.assert_called_once()
15241575
mock_getprpl_name.assert_has_calls(
15251576
[mock.call("example-product", "plan-1", tgt) for tgt in targets]
15261577
)
@@ -1555,6 +1606,7 @@ def test_publish_live_x64_only(
15551606
mock_submit.assert_has_calls(submit_calls)
15561607
mock_ensure_publish.assert_called_once_with(product_obj.id)
15571608

1609+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
15581610
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
15591611
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
15601612
@mock.patch("cloudpub.ms_azure.AzureService.ensure_can_publish")
@@ -1581,6 +1633,7 @@ def test_publish_live_arm64_only(
15811633
mock_ensure_publish: mock.MagicMock,
15821634
mock_get_productid: mock.MagicMock,
15831635
mock_compute_targets: mock.MagicMock,
1636+
mock_wait_publish: mock.MagicMock,
15841637
product_obj: Product,
15851638
plan_summary_obj: PlanSummary,
15861639
metadata_azure_obj: AzurePublishingMetadata,
@@ -1630,6 +1683,7 @@ def test_publish_live_arm64_only(
16301683
# Test
16311684
azure_service.publish(metadata_azure_obj)
16321685

1686+
mock_wait_publish.assert_called_once()
16331687
mock_getprpl_name.assert_has_calls(
16341688
[mock.call("example-product", "plan-1", tgt) for tgt in targets]
16351689
)
@@ -1664,6 +1718,7 @@ def test_publish_live_arm64_only(
16641718
mock_submit.assert_has_calls(submit_calls)
16651719
mock_ensure_publish.assert_called_once_with(product_obj.id)
16661720

1721+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
16671722
@mock.patch("cloudpub.ms_azure.AzureService.ensure_can_publish")
16681723
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
16691724
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
@@ -1672,6 +1727,7 @@ def test_publish_live_when_state_is_preview(
16721727
mock_get_productid: mock.MagicMock,
16731728
mock_compute_targets: mock.MagicMock,
16741729
mock_ensure_publish: mock.MagicMock,
1730+
mock_wait_publish: mock.MagicMock,
16751731
token: Dict[str, Any],
16761732
auth_dict: Dict[str, Any],
16771733
configure_running_response: Dict[str, Any],
@@ -1791,8 +1847,10 @@ def test_publish_live_when_state_is_preview(
17911847
'Updating the technical configuration for "example-product/plan-1" on "preview".'
17921848
not in caplog.text
17931849
)
1850+
mock_wait_publish.assert_called_once()
17941851
mock_ensure_publish.assert_called_once()
17951852

1853+
@mock.patch("cloudpub.ms_azure.AzureService.wait_active_publishing")
17961854
@mock.patch("cloudpub.ms_azure.AzureService.ensure_can_publish")
17971855
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
17981856
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
@@ -1803,6 +1861,7 @@ def test_publish_live_modular_push(
18031861
mock_get_productid: mock.MagicMock,
18041862
mock_compute_targets: mock.MagicMock,
18051863
mock_ensure_publish: mock.MagicMock,
1864+
mock_wait_publish: mock.MagicMock,
18061865
token: Dict[str, Any],
18071866
auth_dict: Dict[str, Any],
18081867
configure_success_response: Dict[str, Any],
@@ -1896,6 +1955,7 @@ def test_publish_live_modular_push(
18961955
'Performing a modular push to "preview" for "ffffffff-ffff-ffff-ffff-ffffffffffff"'
18971956
in caplog.text
18981957
)
1958+
mock_wait_publish.assert_called_once()
18991959
mock_ensure_publish.assert_called_once()
19001960

19011961
# Configure request

0 commit comments

Comments
 (0)