From bf0d630a8feaaf05c79eed2edc58c022c96c5a87 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 7 Jul 2026 11:56:57 +0000 Subject: [PATCH] =?UTF-8?q?An=20attach-mode=20partial=20failure=20persists?= =?UTF-8?q?=20successes=20then=20records=20the=20failure=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- applications/modelling_e2e/handler.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/applications/modelling_e2e/handler.py b/applications/modelling_e2e/handler.py index 9215a078c..b580a37d4 100644 --- a/applications/modelling_e2e/handler.py +++ b/applications/modelling_e2e/handler.py @@ -65,7 +65,7 @@ from domain.modelling.plan import Plan from domain.property.property import Property, PropertyIdentity from domain.property_baseline.calculator_rebaseliner import CalculatorRebaseliner from domain.sap10_calculator.calculator import Sap10Calculator -from domain.tasks.subtasks import SubTask +from domain.tasks.subtasks import SubTask, SubTaskFailure from domain.tasks.tasks import Source from harness.console import run_modelling from orchestration.task_orchestrator import TaskOrchestrator @@ -687,12 +687,21 @@ def handler( ) logger.info(f"property={pid} queued for write") + failed_properties: list[dict[str, Any]] = [] if trigger.subtask_id is not None: # Attach mode (ADR-0055): the whole batch runs under the # distributor's pre-created sub_task — the @task_handler wrapper is # already inside it — so no per-property children are created. + # Failures stay isolated per property (siblings continue) and are + # recorded on the sub_task after the surviving writes flush. for pid in property_ids: - _model_property(pid) + try: + _model_property(pid) + except Exception as exc: # noqa: BLE001 — recorded below + logger.exception(f"property={pid} failed") + failed_properties.append( + {"property_id": pid, "error": str(exc)} + ) else: # Fan the batch out into one child SubTask per property and run # them in a single batched pass: create all children, model each @@ -732,5 +741,18 @@ def handler( f"{[s['certificate_number'] for s in skipped_certs]}" ) + # Raised only after the surviving writes flushed: the failure is a + # record on the sub_task, never an SQS retry (ADR-0055). Recovery is a + # deliberate re-send of the sub_task's own inputs. + if failed_properties: + raise SubTaskFailure( + f"{len(failed_properties)} of {len(property_ids)} " + "properties failed", + details={ + "succeeded": len(property_ids) - len(failed_properties), + "failed": failed_properties, + }, + ) + finally: read_session.close()