fix: propagate SPRT discovery bind/broadcast failures instead of swallowing them

SprtDiscoveryService caught SocketException on the UDP 9100 bind and the
broadcast send and returned an empty list either way. That defeated the
CompositeDiscoveryService fix that rethrows only when every child discovery
service fails: on a real network fault the composite would see one child
throw (ENPC) and one child silently succeed empty (SPRT), conclude discovery
worked and found nothing, and let PrinterDiscoveryBackgroundTask expire and
deregister live Epson printers after the retention window - discarding their
queued jobs.

Move the policy decision to the composite, which already has it right: the
child now reports bind/broadcast failure honestly by rethrowing, and
CompositeDiscoveryService absorbs a single child's failure while still
rethrowing when every child fails. The per-datagram receive-loop catch is
unchanged - a single bad receive still shouldn't abort the pass.

Rewrote SprtDiscoveryServiceTests' bind-failure test to assert the throw, and
added an end-to-end CompositeDiscoveryServiceTests case with a real
SprtDiscoveryService failing to bind alongside a stub that succeeds, proving
the composite still degrades gracefully rather than losing the pass. Updated
the POS895 design doc to match the new contract.

Claude-Session: https://claude.ai/code/session_01E6N3P4nSZLtp4BC2LXVref
This commit is contained in:
EugeneTes
2026-09-03 13:01:58 +02:00
parent a4e96f3041
commit fd19cd9358
4 changed files with 55 additions and 10 deletions

View File

@@ -58,13 +58,21 @@ listens for the timeout, and maps each 39-byte reply to a `DiscoveredPrinter`:
Replies that do not parse as a 17-character dashed MAC are logged and skipped,
never thrown.
**Binding UDP 9100 can fail** when another process holds it. On `SocketException`
the service logs a warning and returns an empty list, so ENPC discovery still
runs. Discovery degrades; it does not crash.
**Binding UDP 9100 can fail** when another process holds it, and the broadcast
send can fail too. On `SocketException` the service logs a warning and
**rethrows** - it does not swallow the fault into an empty list. A silent empty
list is indistinguishable from "no SPRT printers on this network", and
`CompositeDiscoveryService` is the layer that needs to tell those apart, not
this service.
**`CompositeDiscoveryService`** - takes `IEnumerable<IDiscoveryService>`, runs
them concurrently on the shared timeout, merges results by `IPAddress` with
first-wins, and forwards each child's `onPrinterDiscovered` callback as it fires.
It absorbs a single child's failure (logs it, keeps the other children's
results) but rethrows when every child fails, since that means discovery
itself is broken rather than that the network simply has no printers on it.
Discovery still degrades gracefully overall - it just does so at the composite
layer, which is the only layer that can see all children and make that call.
**Wiring** - `PrintServerBootstrapper.cs:77` changes from
`Bind<IDiscoveryService>().To<DiscoveryService>()` to the composite over both