Commit d8b26aa5 authored by George Papathanail's avatar George Papathanail
Browse files

fix: guard app_deployment rollup against stale redelivered completions

parent 0f5a986f
Loading
Loading
Loading
Loading
+10 −6
Original line number Diff line number Diff line
@@ -128,6 +128,11 @@ def _is_final(app_instance: AppInstance) -> bool:
    return app_instance.state == AppInstanceState.TERMINATED


def _deployment_is_final(app_deployment: AppDeployment) -> bool:
    """Whether no completion may move this deployment rollup any further."""
    return app_deployment.state == AppDeploymentState.TERMINATED


def _log_stale_completion(app_instance: AppInstance, operation_id: UUID) -> None:
    logger.info(
        "stale_completion_ignored_for_final_app_instance",
@@ -867,14 +872,13 @@ class EdgeApplicationManagementService:
        # Optional cache rollup, multi-zone deployments only (persistence-model.md).
        # A single-zone /appinstances operation_id has no matching row, so this
        # is a soft no-op for the existing path -- same optional-dependency
        # pattern as the callback repos below. DELETE /deployments doesn't
        # exist yet, so app_deployments.state can only be INSTANTIATING here;
        # once it does, this will need the same _is_final-style guard the
        # instances above already have, to avoid reviving a terminated
        # deployment on a stale redelivery.
        # pattern as the callback repos below. DELETE /deployments doesn't exist
        # yet, so app_deployments.state can only be INSTANTIATING here, but the
        # _is_final guard mirrors the instances above so a stale redelivery
        # can't revive a terminated deployment once DELETE lands.
        if self._app_deployment_repo is not None:
            app_deployment = await self._app_deployment_repo.get_by_operation_id(operation_id)
            if app_deployment is not None:
            if app_deployment is not None and not _deployment_is_final(app_deployment):
                await self._app_deployment_repo.save(
                    app_deployment.model_copy(
                        update={"state": _APP_DEPLOYMENT_COMPLETION_STATE_MAP[status]}
+47 −0
Original line number Diff line number Diff line
@@ -2399,6 +2399,53 @@ class TestHandleCompleted:
        assert updated is not None
        assert updated.state == AppDeploymentState.FAILED

    async def _seed_terminated_app_deployment(
        self, app_deployment_repo: FakeAppDeploymentRepository, app_deployment_id: UUID
    ) -> None:
        await app_deployment_repo.save(
            AppDeployment(
                app_deployment_id=app_deployment_id,
                operation_id=self.OPERATION_ID,
                app_registration_id=uuid4(),
                app_deployment_name="video_analytics_eu",
                edge_cloud_zones=[ZONE_ID],
                state=AppDeploymentState.TERMINATED,
            )
        )

    async def test_late_deploy_completion_does_not_revive_terminated_deployment(
        self,
        service: EdgeApplicationManagementService,
        operation_repo: FakeOperationRepository,
        app_instance_repo: FakeAppInstanceRepository,
        app_deployment_repo: FakeAppDeploymentRepository,
    ) -> None:
        """A redelivered deploy completion must not roll a deployment that has
        since reached TERMINATED back to a live state -- the same staleness
        guard the per-instance rollups already carry."""
        deployment_id = uuid4()
        await self._seed_pending_operation(operation_repo, OperationType.DEPLOY)
        await self._seed_instantiating_app_instance(app_instance_repo, INSTANCE_ID)
        await self._seed_terminated_app_deployment(app_deployment_repo, deployment_id)
        event = SRMOperationCompleted(
            schema_version="1.0",
            operation_id=str(self.OPERATION_ID),
            status="completed",
            instances=[
                SRMCompletedInstance(
                    service_instance_id=str(INSTANCE_ID), zone_id=str(ZONE_ID), status="completed"
                )
            ],
            correlation_id="corr-1",
            completed_at="2026-07-04T10:02:35+00:00",
        )

        await service.handle_completed(event)

        unchanged = await app_deployment_repo.get_by_id(deployment_id)
        assert unchanged is not None
        assert unchanged.state == AppDeploymentState.TERMINATED

    async def test_single_zone_operation_leaves_app_deployments_untouched(
        self,
        service: EdgeApplicationManagementService,