Skip to content

Commit 37c8615

Browse files
committed
Azure: rework how conflict detectino works
This commit changes the existing code of `AzureService.ensure_can_publish` to behave as follows: 1. It no longer retries on error but simply raises whenever a conflict is detected 2. It no longer searches only for "live" or "preview" but for all existing submissions 3. It improves the logging mechanism to inform which state the submission was and its target. Rationale: 1. If we simply wait for the publishing to finish as the previous version (using retry) we could end up in a scenario where the changes made by the library are no longer the latest, causing the Graph API to return the error: `The submission cannot be pushed to as its not the latest` With that, we would need to retry the whole `publish` operations so it makes more sense to just raise `ConflictError` and let `pubtools` retry when this error is caught. 2. The previous code were not able to catch a lot of conflicts, resulting in many errors with `An In Progress submission already exists` By looking into each submission target we may caught conflicts early. 3. Logs will now show exactly what was the reason it was already in progress and for which submission target. 4. A retry can/will be implemented on `pubtools-marketplacesvm` on Azure `push` in case a `ConflictError` is detected. 5. Replaces !119 Refers to SPSTRAT-611 and SPSTRAT-549 Signed-off-by: Jonathan Gangi <jgangi@redhat.com>
1 parent 075853a commit 37c8615

File tree

3 files changed

+43
-99
lines changed

3 files changed

+43
-99
lines changed

cloudpub/error.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,9 @@ class NotFoundError(ValueError):
2121
"""Represent a missing resource."""
2222

2323

24+
class ConflictError(RuntimeError):
25+
"""Report a submission conflict error."""
26+
27+
2428
class Timeout(Exception):
2529
"""Represent a missing resource."""

cloudpub/ms_azure/service.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from tenacity.wait import wait_chain, wait_fixed
1313

1414
from cloudpub.common import BaseService
15-
from cloudpub.error import InvalidStateError, NotFoundError
15+
from cloudpub.error import ConflictError, InvalidStateError, NotFoundError
1616
from cloudpub.models.ms_azure import (
1717
RESOURCE_MAPING,
1818
AzureResource,
@@ -467,31 +467,28 @@ def submit_to_status(
467467
log.debug("Set the status \"%s\" to submission.", status)
468468
return self.configure(resources=cfg_res)
469469

470-
@retry(
471-
wait=wait_fixed(300),
472-
stop=stop_after_delay(max_delay=60 * 60 * 24 * 7), # Give up after retrying for 7 days,
473-
reraise=True,
474-
)
475470
def ensure_can_publish(self, product_id: str) -> None:
476471
"""
477472
Ensure the offer is not already being published.
478473
479-
It will wait for up to 7 days retrying to make sure it's possible to publish before
480-
giving up and raising.
474+
It will raise Conflict if a publish is already in progress in any submission target.
481475
482476
Args:
483477
product_id (str)
484478
The product ID to check the offer's publishing status
485479
Raises:
486-
RuntimeError: whenever a publishing is already in progress.
480+
ConflictError: whenever a publishing is already in progress for any submission target.
487481
"""
488482
log.info("Ensuring no other publishing jobs are in progress for \"%s\"", product_id)
489-
submission_targets = ["preview", "live"]
490483

491-
for target in submission_targets:
492-
sub = self.get_submission_state(product_id, state=target)
493-
if sub and sub.status and sub.status == "running":
494-
raise RuntimeError(f"The offer {product_id} is already being published to {target}")
484+
for sub in self.get_submissions(product_id):
485+
if sub and sub.status and sub.status != "completed":
486+
msg = (
487+
f"The offer {product_id} is already being published to "
488+
f"{sub.target.targetType}: {sub.status}/{sub.result}"
489+
)
490+
log.error(msg)
491+
raise ConflictError(msg)
495492

496493
def get_plan_tech_config(self, product: Product, plan: PlanSummary) -> VMIPlanTechConfig:
497494
"""

tests/ms_azure/test_service.py

Lines changed: 28 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from httmock import response
1111
from requests import Response
1212
from requests.exceptions import HTTPError
13-
from tenacity.stop import stop_after_attempt
1413

1514
from cloudpub.common import BaseService
1615
from cloudpub.error import InvalidStateError, NotFoundError
@@ -750,85 +749,35 @@ def test_submit_to_status_not_found(
750749

751750
mock_configure.assert_not_called()
752751

753-
@pytest.mark.parametrize("target", ["preview", "live"])
754-
@mock.patch("cloudpub.ms_azure.AzureService.get_submission_state")
752+
@mock.patch("cloudpub.ms_azure.AzureService.get_submissions")
755753
def test_ensure_can_publish_success(
756754
self,
757-
mock_getsubst: mock.MagicMock,
758-
target: str,
755+
mock_getsubs: mock.MagicMock,
759756
azure_service: AzureService,
760757
) -> None:
761-
submission = {
762-
"$schema": "https://product-ingestion.azureedge.net/schema/submission/2022-03-01-preview2", # noqa: E501
763-
"id": "submission/ffffffff-ffff-ffff-ffff-ffffffffffff/0",
764-
"product": "product/ffffffff-ffff-ffff-ffff-ffffffffffff",
765-
"target": {"targetType": target},
766-
"lifecycleState": "generallyAvailable",
767-
"status": "completed",
768-
"result": "succeeded",
769-
"created": "2024-07-04T22:06:16.2895521Z",
770-
}
771-
mock_getsubst.return_value = ProductSubmission.from_json(submission)
772-
azure_service.ensure_can_publish.retry.sleep = mock.MagicMock() # type: ignore
773-
azure_service.ensure_can_publish.retry.stop = stop_after_attempt(1) # type: ignore
774-
775-
azure_service.ensure_can_publish("ffffffff-ffff-ffff-ffff-ffffffffffff")
776-
777-
# All targets are called by the method, it should pass all
778-
mock_getsubst.assert_has_calls(
779-
[
780-
mock.call("ffffffff-ffff-ffff-ffff-ffffffffffff", state="preview"),
781-
mock.call("ffffffff-ffff-ffff-ffff-ffffffffffff", state="live"),
782-
]
783-
)
784-
785-
@pytest.mark.parametrize("target", ["preview", "live"])
786-
@mock.patch("cloudpub.ms_azure.AzureService.get_submission_state")
787-
def test_ensure_can_publish_success_after_retry(
788-
self,
789-
mock_getsubst: mock.MagicMock,
790-
target: str,
791-
azure_service: AzureService,
792-
) -> None:
793-
running = {
794-
"$schema": "https://product-ingestion.azureedge.net/schema/submission/2022-03-01-preview2", # noqa: E501
795-
"id": "submission/ffffffff-ffff-ffff-ffff-ffffffffffff/0",
796-
"product": "product/ffffffff-ffff-ffff-ffff-ffffffffffff",
797-
"target": {"targetType": target},
798-
"lifecycleState": "generallyAvailable",
799-
"status": "running",
800-
"result": "pending",
801-
"created": "2024-07-04T22:06:16.2895521Z",
802-
}
803-
complete = {
804-
"$schema": "https://product-ingestion.azureedge.net/schema/submission/2022-03-01-preview2", # noqa: E501
805-
"id": "submission/ffffffff-ffff-ffff-ffff-ffffffffffff/0",
806-
"product": "product/ffffffff-ffff-ffff-ffff-ffffffffffff",
807-
"target": {"targetType": target},
808-
"lifecycleState": "generallyAvailable",
809-
"status": "completed",
810-
"result": "succeeded",
811-
"created": "2024-07-04T22:06:16.2895521Z",
812-
}
813-
mock_getsubst.side_effect = [
814-
ProductSubmission.from_json(running),
815-
ProductSubmission.from_json(running),
816-
ProductSubmission.from_json(complete),
817-
ProductSubmission.from_json(complete),
758+
submissions = [
759+
{
760+
"$schema": "https://product-ingestion.azureedge.net/schema/submission/2022-03-01-preview2", # noqa: E501
761+
"id": "submission/ffffffff-ffff-ffff-ffff-ffffffffffff/0",
762+
"product": "product/ffffffff-ffff-ffff-ffff-ffffffffffff",
763+
"target": {"targetType": tgt},
764+
"lifecycleState": "generallyAvailable",
765+
"status": "completed",
766+
"result": "succeeded",
767+
"created": "2024-07-04T22:06:16.2895521Z",
768+
}
769+
for tgt in ["draft", "preview", "live"]
818770
]
819-
azure_service.ensure_can_publish.retry.sleep = mock.MagicMock() # type: ignore
820-
azure_service.ensure_can_publish.retry.stop = stop_after_attempt(3) # type: ignore
771+
mock_getsubs.return_value = [ProductSubmission.from_json(s) for s in submissions]
821772

822773
azure_service.ensure_can_publish("ffffffff-ffff-ffff-ffff-ffffffffffff")
823-
824-
# Calls for "live" and "preview" for 2 times before success == 4
825-
assert mock_getsubst.call_count == 4
774+
mock_getsubs.assert_called_once()
826775

827776
@pytest.mark.parametrize("target", ["preview", "live"])
828-
@mock.patch("cloudpub.ms_azure.AzureService.get_submission_state")
777+
@mock.patch("cloudpub.ms_azure.AzureService.get_submissions")
829778
def test_ensure_can_publish_raises(
830779
self,
831-
mock_getsubst: mock.MagicMock,
780+
mock_getsubs: mock.MagicMock,
832781
target: str,
833782
azure_service: AzureService,
834783
) -> None:
@@ -856,17 +805,13 @@ def test_ensure_can_publish_raises(
856805
"result": "pending",
857806
"created": "2024-07-04T22:06:16.2895521Z",
858807
}
859-
if target == "preview":
860-
subs = [ProductSubmission.from_json(sub2), ProductSubmission.from_json(sub1)]
861-
else:
862-
subs = [ProductSubmission.from_json(sub1), ProductSubmission.from_json(sub2)]
863-
mock_getsubst.side_effect = subs
808+
subs = [ProductSubmission.from_json(sub1), ProductSubmission.from_json(sub2)]
809+
mock_getsubs.return_value = subs
864810

865811
err = (
866-
f"The offer ffffffff-ffff-ffff-ffff-ffffffffffff is already being published to {target}"
812+
"The offer ffffffff-ffff-ffff-ffff-ffffffffffff is already being published to "
813+
f"{target}: running/pending"
867814
)
868-
azure_service.ensure_can_publish.retry.sleep = mock.MagicMock() # type: ignore
869-
azure_service.ensure_can_publish.retry.stop = stop_after_attempt(1) # type: ignore
870815

871816
with pytest.raises(RuntimeError, match=err):
872817
azure_service.ensure_can_publish("ffffffff-ffff-ffff-ffff-ffffffffffff")
@@ -1651,12 +1596,14 @@ def test_publish_live_arm64_only(
16511596
mock_submit.assert_has_calls(submit_calls)
16521597
mock_ensure_publish.assert_called_once_with(product_obj.id)
16531598

1599+
@mock.patch("cloudpub.ms_azure.AzureService.ensure_can_publish")
16541600
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
16551601
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
16561602
def test_publish_live_when_state_is_preview(
16571603
self,
16581604
mock_get_productid: mock.MagicMock,
16591605
mock_compute_targets: mock.MagicMock,
1606+
mock_ensure_publish: mock.MagicMock,
16601607
token: Dict[str, Any],
16611608
auth_dict: Dict[str, Any],
16621609
configure_running_response: Dict[str, Any],
@@ -1714,8 +1661,6 @@ def test_publish_live_when_state_is_preview(
17141661
m.get(
17151662
f"{base_url}/submission/{product_id}",
17161663
[
1717-
{"json": submissions_inprog}, # ensure_can_publish call "preview"
1718-
{"json": submissions_inprog}, # ensure_can_publish call "live"
17191664
{"json": submissions_inprog}, # _is_submission_in_preview call
17201665
{"json": submissions_inprog}, # submit_to_status check prev_state call
17211666
{"json": submissions_final}, # submit_to_status validation after configure
@@ -1749,10 +1694,6 @@ def test_publish_live_when_state_is_preview(
17491694
'Requesting the product ID "ffffffff-ffff-ffff-ffff-ffffffffffff" with state "preview".'
17501695
in caplog.text
17511696
)
1752-
assert (
1753-
'Ensuring no other publishing jobs are in progress for "ffffffff-ffff-ffff-ffff-ffffffffffff"' # noqa: E501
1754-
in caplog.text
1755-
)
17561697
assert (
17571698
'Looking up for submission in state "preview" for "ffffffff-ffff-ffff-ffff-ffffffffffff"' # noqa: E501
17581699
in caplog.text
@@ -1782,7 +1723,9 @@ def test_publish_live_when_state_is_preview(
17821723
'Updating the technical configuration for "example-product/plan-1" on "preview".'
17831724
not in caplog.text
17841725
)
1726+
mock_ensure_publish.assert_called_once()
17851727

1728+
@mock.patch("cloudpub.ms_azure.AzureService.ensure_can_publish")
17861729
@mock.patch("cloudpub.ms_azure.AzureService.compute_targets")
17871730
@mock.patch("cloudpub.ms_azure.AzureService.get_productid")
17881731
@mock.patch("cloudpub.ms_azure.AzureService.configure")
@@ -1791,6 +1734,7 @@ def test_publish_live_modular_push(
17911734
mock_configure: mock.MagicMock,
17921735
mock_get_productid: mock.MagicMock,
17931736
mock_compute_targets: mock.MagicMock,
1737+
mock_ensure_publish: mock.MagicMock,
17941738
token: Dict[str, Any],
17951739
auth_dict: Dict[str, Any],
17961740
configure_success_response: Dict[str, Any],
@@ -1861,8 +1805,6 @@ def test_publish_live_modular_push(
18611805
m.get(
18621806
f"{base_url}/submission/{product_id}",
18631807
[
1864-
{"json": {"value": [submission]}}, # ensure_can_publish call "preview"
1865-
{"json": {"value": [submission]}}, # ensure_can_publish call "live"
18661808
{"json": {"value": [submission]}}, # push_preview: call submit_status
18671809
{"json": {"value": [submission_preview]}}, # push_preview: check result
18681810
{"json": {"value": [submission_preview]}}, # push_live: call submit_status
@@ -1886,6 +1828,7 @@ def test_publish_live_modular_push(
18861828
'Performing a modular push to "preview" for "ffffffff-ffff-ffff-ffff-ffffffffffff"'
18871829
in caplog.text
18881830
)
1831+
mock_ensure_publish.assert_called_once()
18891832

18901833
# Configure request
18911834
mock_configure.assert_has_calls(

0 commit comments

Comments
 (0)