Commit 512a573a authored by George Papathanail's avatar George Papathanail
Browse files

fix: persist SRM instance error on failed QoD/TI/EAM operations

When SRM reports a failed operation, the reason can sit only in
instances[].error with the top-level error left null. The QoD, TI and
EAM completion handlers saved only event.error, so operations.error
was null for every such failure.

Add completion_error() next to SRMOperationCompleted: it returns
event.error, or the first instance error when the operation failed
and event.error is missing. partially_completed operations still
keep operations.error null. Use it in all three handle_completed
methods.
parent 104f349b
Loading
Loading
Loading
Loading
Loading
+2 −1
Changes for src/open_exposure_gateway/application/services/edge_application_management_service.py: 2 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -53,6 +53,7 @@ from open_exposure_gateway.domain.edge_application_management import (
    SRMCompletedInstance,
    SRMOperationCompleted,
    Subject,
    completion_error,
)
from open_exposure_gateway.domain.errors import (
    CallbackCredentialExpiredError,
@@ -1075,7 +1076,7 @@ class EdgeApplicationManagementService:
        operation_update: dict[str, Any] = {
            "status": status,
            "result": result,
            "error": event.error,
            "error": completion_error(event),
        }
        if not partial_delivery:
            operation_update["completed_at"] = datetime.fromisoformat(event.completed_at)
+5 −2
Changes for src/open_exposure_gateway/application/services/quality_on_demand_service.py: 5 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -35,7 +35,10 @@ from open_exposure_gateway.core.exceptions import (
    NotImplementedException,
    UnprocessableEntityException,
)
from open_exposure_gateway.domain.edge_application_management import SRMOperationCompleted
from open_exposure_gateway.domain.edge_application_management import (
    SRMOperationCompleted,
    completion_error,
)
from open_exposure_gateway.domain.errors import (
    CallbackCredentialExpiredError,
    CallbackCredentialRevokedError,
@@ -446,7 +449,7 @@ class QualityOnDemandService:
                update={
                    "status": status,
                    "result": result,
                    "error": event.error,
                    "error": completion_error(event),
                    "completed_at": datetime.fromisoformat(event.completed_at),
                }
            )
+5 −2
Changes for src/open_exposure_gateway/application/services/traffic_influence_service.py: 5 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -31,7 +31,10 @@ from open_exposure_gateway.core.exceptions import (
    NotFoundException,
    NotImplementedException,
)
from open_exposure_gateway.domain.edge_application_management import SRMOperationCompleted
from open_exposure_gateway.domain.edge_application_management import (
    SRMOperationCompleted,
    completion_error,
)
from open_exposure_gateway.domain.errors import (
    CallbackCredentialExpiredError,
    CallbackCredentialRevokedError,
@@ -554,7 +557,7 @@ class TrafficInfluenceService:
                update={
                    "status": status,
                    "result": result,
                    "error": event.error,
                    "error": completion_error(event),
                    "completed_at": datetime.fromisoformat(event.completed_at),
                }
            )
+6 −0
Changes for src/open_exposure_gateway/domain/edge_application_management.py: 6 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -316,6 +316,12 @@ class SRMOperationCompleted(BaseModel):
        return self


def completion_error(event: SRMOperationCompleted) -> dict[str, Any] | None:
    if event.error is not None or event.status != "failed":
        return event.error
    return next((i.error for i in event.instances if i.error is not None), None)


class SRMCapabilityEndpoint(BaseModel):
    interface_id: str
    fqdn: str | None = None
+33 −0
Changes for tests/unit/test_eam_service.py: 33 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -2854,6 +2854,39 @@ class TestHandleCompleted:
        assert updated.result is None
        assert updated.error == error

    async def test_failed_operation_saves_instance_error_when_top_level_missing(
        self, service: EdgeApplicationManagementService, operation_repo: FakeOperationRepository
    ) -> None:
        await self._seed_pending_operation(operation_repo)
        instance_error = {
            "type": "about:blank",
            "title": "Zone Capacity Exceeded",
            "status": 503,
            "detail": "no capacity",
        }
        event = SRMOperationCompleted(
            schema_version="1.0",
            operation_id=str(self.OPERATION_ID),
            status="failed",
            instances=[
                SRMCompletedInstance(
                    service_instance_id=str(INSTANCE_ID),
                    zone_id=str(ZONE_ID),
                    status="failed",
                    error=instance_error,
                ),
            ],
            correlation_id="corr-1",
            completed_at="2026-07-04T10:02:35+00:00",
        )

        await service.handle_completed(event)

        updated = await operation_repo.get_by_id(self.OPERATION_ID)
        assert updated is not None
        assert updated.status == OperationStatus.FAILED
        assert updated.error == instance_error

    async def test_updates_operation_to_partially_completed(
        self, service: EdgeApplicationManagementService, operation_repo: FakeOperationRepository
    ) -> None:
Loading