diff --git a/src/shared/listeners/automatic_linkage.py b/src/shared/listeners/automatic_linkage.py index b1e5f829..c3f109b5 100644 --- a/src/shared/listeners/automatic_linkage.py +++ b/src/shared/listeners/automatic_linkage.py @@ -268,35 +268,36 @@ def build_new_links(container: Container) -> bool: outcome = resolve_linkage_candidates(container) - proposal = CVEDerivationClusterProposal.objects.create( - cve=container.cve, - status=( - CVEDerivationClusterProposal.Status.REJECTED + with transaction.atomic(): + proposal = CVEDerivationClusterProposal.objects.create( + cve=container.cve, + status=( + CVEDerivationClusterProposal.Status.REJECTED + if outcome.rejection + else CVEDerivationClusterProposal.Status.PENDING + ), + rejection_reason=outcome.rejection.reason if outcome.rejection else None, + rejection_match_count=outcome.rejection.match_count or None if outcome.rejection - else CVEDerivationClusterProposal.Status.PENDING - ), - rejection_reason=outcome.rejection.reason if outcome.rejection else None, - rejection_match_count=outcome.rejection.match_count or None - if outcome.rejection - else None, - rejection_max_matches_limit=outcome.rejection.max_matches_limit or None - if outcome.rejection - else None, - algorithm_version=CVEDerivationClusterProposal.CURRENT_ALGORITHM_VERSION, - ) - - if outcome.derivations: - links = build_derivation_links(proposal, outcome.derivations) - DerivationClusterProposalLink.objects.bulk_create(links) - pkg_links = build_package_links(proposal, outcome.derivations) - PackageClusterProposalLink.objects.bulk_create(pkg_links) - logger.info( - "Matching suggestion for '%s': %d derivations, %d packages found.", - container.cve, - len(links), - len(pkg_links), + else None, + rejection_max_matches_limit=outcome.rejection.max_matches_limit or None + if outcome.rejection + else None, + algorithm_version=CVEDerivationClusterProposal.CURRENT_ALGORITHM_VERSION, ) + if outcome.derivations: + links = build_derivation_links(proposal, outcome.derivations) + DerivationClusterProposalLink.objects.bulk_create(links) + pkg_links = build_package_links(proposal, outcome.derivations) + PackageClusterProposalLink.objects.bulk_create(pkg_links) + logger.info( + "Matching suggestion for '%s': %d derivations, %d packages found.", + container.cve, + len(links), + len(pkg_links), + ) + return True diff --git a/src/shared/tests/test_linkage.py b/src/shared/tests/test_linkage.py index c19957de..1931512b 100644 --- a/src/shared/tests/test_linkage.py +++ b/src/shared/tests/test_linkage.py @@ -1,5 +1,6 @@ from collections.abc import Callable from datetime import timedelta +from unittest import mock import pytest from django.test import override_settings @@ -549,3 +550,22 @@ def test_package_link_provenance_flags_merged_across_drvs( link.provenance_flags == ProvenanceFlags.PACKAGE_NAME_MATCH | ProvenanceFlags.PRODUCT_MATCH ) + + +def test_build_new_links_is_atomic( + make_container: Callable[..., Container], + make_drv: Callable[..., NixDerivation], +) -> None: + make_drv(pname="foo") + container = make_container(package_name="foo") + + with mock.patch.object( + DerivationClusterProposalLink.objects, + "bulk_create", + side_effect=Exception("simulated DB failure"), + ): + with pytest.raises(Exception, match="simulated DB failure"): + build_new_links(container) + + # A retry must succeed after a failed first attempt + assert build_new_links(container)