diff --git a/docs/plans/2026-06-26-delmia-recipe-notifier-design.md b/docs/plans/2026-06-26-delmia-recipe-notifier-design.md index 5f99cd6b..7b4b2f92 100644 --- a/docs/plans/2026-06-26-delmia-recipe-notifier-design.md +++ b/docs/plans/2026-06-26-delmia-recipe-notifier-design.md @@ -105,6 +105,30 @@ response came back). A node that responds at all is authoritative — its answer > to the next node — failover is strictly for unreachable nodes. (Revisit only if operations want > `5xx`/`503` to also fail over.) +## Delivery semantics: at-least-once + +The connect-failure-only failover loop above is by design (no code change here) — but its +consequence is at-least-once delivery of the `DelmiaRecipeDownload` POST, not exactly-once. Two +cases both count as "no response" from the client's point of view, trigger failover to the next +URL, and produce a duplicate POST even though the first node may have already processed the +request: + +1. **Connection reset after the server processed the POST** — the request reaches the site and is + applied, but the response never makes it back to the client (dropped connection, reset). The + client sees a connect failure (row 1 of the table above), rolls over, and re-POSTs to the next + `BaseUrls` entry. +2. **Slow response exceeding `TimeoutSeconds`** — the server is still processing (or has already + finished) when the client's `HttpClient.Timeout` fires. The client treats the timeout the same + as an unreachable node, rolls over, and re-POSTs. + +**Requirement:** the deployed `DelmiaRecipeDownload` inbound method **MUST be idempotent** on the +natural key `(MachineCode, DownloadPath, WorkOrderNumber)`. A duplicate POST for the same key must +return the same `Result`/`ResultText` as the original call, not double-apply side effects (e.g. not +queue the recipe download twice). This is a contract on the inbound method's script, not on the +DelmiaNotifier client — the client cannot distinguish a "duplicate delivered" retry from a +"first delivery" attempt, so idempotency must live on the receiving side. See the timeout row in +the failover table above for the triggering condition. + ## Error handling & logging - **stdout is reserved** for the `YES`/`NO` contract. All diagnostics — per-URL attempt, status code, diff --git a/docs/requirements/Component-InboundAPI.md b/docs/requirements/Component-InboundAPI.md index ae22165b..a89c7ffc 100644 --- a/docs/requirements/Component-InboundAPI.md +++ b/docs/requirements/Component-InboundAPI.md @@ -47,6 +47,13 @@ Each API method definition includes: - **Implementation Script**: C# script that executes when the method is called. Stored **inline** in the method definition. Follows standard C# authoring patterns but has no template inheritance — it is a standalone script tied to this method. - **Timeout**: Configurable per method. Defines the maximum time the method is allowed to execute (including any routed calls to sites) before returning a timeout error to the caller. +> **Authoring note — idempotent methods for at-least-once callers**: some external callers retry +> on their own connect-failure/timeout failover loop and cannot tell "the first attempt actually +> succeeded" from "the first attempt was lost" — e.g. the `DelmiaRecipeDownload` method backing +> `DelmiaNotifier` (see `docs/plans/2026-06-26-delmia-recipe-notifier-design.md`, "Delivery +> semantics: at-least-once"). Implementation scripts for such methods must be idempotent on their +> natural key so a duplicate delivery returns the same result without double-applying side effects. + ### Management - Managed by users with the **Designer** role via the Central UI. - All method definition changes are audit logged. diff --git a/src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/README.md b/src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/README.md index 8d834dce..6f0ea870 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/README.md +++ b/src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/README.md @@ -52,6 +52,15 @@ WWNotifier.exe -m Z28061 -d "C:\recipes\job.nc" -w WO12345 -p PN-7788 -s 0100 -u - All diagnostics (per-URL attempt, status code, which URL answered, exceptions) go to **stderr** and, if `LogPath` is set, an appended log file — never to stdout. +## Delivery semantics + +The `BaseUrls` failover loop is at-least-once, not exactly-once: a connection reset *after* the +target already processed the POST, or a slow response that exceeds `TimeoutSeconds`, both look like +an unreachable node to this client and cause a re-POST to the next URL — even though the first +attempt may already have succeeded. The deployed `DelmiaRecipeDownload` inbound method must be +idempotent on `(MachineCode, DownloadPath, WorkOrderNumber)` so a duplicate delivery is harmless; +see the design doc's "Delivery semantics: at-least-once" section for details. + ## Configuration — `appsettings.json` Placed next to the exe (copied to the output on build/publish):