|
| 1 | +# Incident review: Hull live clinic gateway failure, 6–7 July 2026 |
| 2 | + |
| 3 | +**Severity**: No patient impact. The clinic ran to completion on the manual-images fallback. Gateway (automatic) imaging was unavailable for the first live clinic; full end-to-end success was achieved the same afternoon. |
| 4 | + |
| 5 | +## Summary |
| 6 | +The first production use of the gateway imaging flow failed: worklist items sent from Rubie never reached the hospital gateway. Diagnosis was unusually hard because three independent faults were stacked, all producing the same symptom (a 30-second timeout on every send), and the system’s error reporting could not distinguish them. Each fault was peeled off in turn over ~36 hours; the full pipeline – check-in → worklist → modality → images → upload → Rubie – worked in production for the first time on the afternoon of 7 July. |
| 7 | + |
| 8 | +## Symptoms |
| 9 | +- Every worklist send from prod Rubie failed with a generic “Timeout waiting for response from gateway”; nothing appeared on the modality worklist. |
| 10 | +- All infrastructure looked healthy: relay listener connected (count 1), managed identity and role assignments correct, gateway services running, config verified repeatedly on both sides. |
| 11 | +- Independent probes succeeded where the app failed (a laptop-based relay echo round-tripped in 0.4s), deepening the confusion. |
| 12 | + |
| 13 | +## Root causes |
| 14 | +- **Private DNS blackhole**: the relay hostname resolved, inside the prod VNet, to the relay namespace’s private endpoint, which was silently broken despite showing *Approved* (later attributed to an Azure-side fault or provisioning race at PE creation; resolved weeks later by deleting and recreating the PE, which has worked since). |
| 15 | +- **Runtime incompatibility**: the asyncio websockets client cannot reliably open connections inside gevent-patched gunicorn workers (DNS resolution via asyncio's thread executor degrades to 10s+, exhausting the connection budget). Every environment where sends had ever succeeded bypassed gevent (local dev, shell probes) – the production execution model had never actually been tested. |
| 16 | +- **Silent listener death**: overnight, the gateway's relay connection died without the process noticing or the relay deregistering it – a “zombie” listener that looked connected while receiving nothing. |
| 17 | + |
| 18 | +**Contributing factors**: relay send failures were recorded only in a database field, not logged; a single exception handler conflated “connection never opened” with “gateway never replied”; VM telemetry export was broken throughout, forcing diagnosis via remote commands; there was no admin visibility of gateway actions at the start of the incident. |
| 19 | + |
| 20 | +## What we did |
| 21 | +- Shipped diagnostic capability during the incident: a GatewayActions admin page. |
| 22 | +- Built a standalone relay echo probe to test the relay → gateway path with Rubie removed; its container-side variant exposed the DNS/PE fault. |
| 23 | +- Fixed DNS (interim public CNAME; later superseded by PE recreation), replaced the asyncio client with the synchronous websockets client ([Rubie ADR-009](https://github.com/NHSDigital/dtos-manage-breast-screening/blob/main/docs/adr/ADR-009-Synchronous_websockets_client_for_relay_sends.md)), and adopted a pre-clinic listener restart to clear zombie connections. |
| 24 | +- Invoked the designed fallback: the clinic ran entirely on manual image recording, invisibly to participants. |
| 25 | +- Relay token refresh implemented |
| 26 | + |
| 27 | +## Improvements made since |
| 28 | +- **Error reporting**: every relay failure now names its failing phase in both the admin and logs; unknown actions get structured error replies instead of silent connection closes. |
| 29 | +- **Runbooks**: [clinic preparation](../deployment/runbooks/clinic-preparation.md) checklist; [live-clinic debugging](../deployment/runbooks/live-clinic-debugging.md); onboarding/promotion updates; diagnostic toolkit ships with the gateway release. |
| 30 | +- **Automated end-to-end test participant**: a synthetic test can now be sent through the real production pipeline on demand – Rubie dispatches a test worklist item over the relay; the gateway stores it and invokes its modality emulator in place of the physical machine, which queries the worklist, generates DICOM images, and submits them through the standard PACS-and-upload path back to Rubie, where the round trip is confirmed against a dedicated test-action record (kept separate from clinical data). |
| 31 | +- **Rehearsal practice**: successful remote dummy run completed ahead of the next live clinic. |
| 32 | +- **In flight**: automated per-gateway healthcheck (health proven by traffic, surfaced in admin, wired into deploy pipelines); per-site imaging toggle so switching a site to manual is an audited admin action rather than an emergency deploy or record deletion; gateway listener liveness/token-renewal fixes. |
| 33 | + |
| 34 | +## Learnings |
| 35 | +- Health must be proven by traffic, not state. Listener counts, PE connection status, and service states each individually misled us during this incident. Everything we’ve built since sends a real message and judges the reply. |
| 36 | +- The fallback earned its keep. Manual-images mode meant zero patient impact and removed all time pressure from diagnosis. |
| 37 | +- Institutional knowledge belongs in the repo. Every finding was folded into version-controlled runbooks the same week, several of which were corrected again as evidence improved. |
| 38 | +- We had no production gateway we could actually touch. Every diagnostic step on the gateway side ran through Arc run-commands against a hospital-owned machine – serially queued, minutes per round trip, dependent on hospital IT for RDP and power, and twice found switched off entirely. A team-operated gateway VM in the production environment (Azure-hosted, Arc-onboarded, ring0, connected to the prod relay and API but to no modality) would have let us reproduce and inspect the failures directly under real prod identity and networking. |
0 commit comments