Skip to content

fix(common): isolate every event multicast across the codebase#188

Open
ottobolyos wants to merge 30 commits into
TrakHound:masterfrom
ottobolyos:fix/event-multicast-isolation
Open

fix(common): isolate every event multicast across the codebase#188
ottobolyos wants to merge 30 commits into
TrakHound:masterfrom
ottobolyos:fix/event-multicast-isolation

Conversation

@ottobolyos
Copy link
Copy Markdown
Contributor

@ottobolyos ottobolyos commented Jun 3, 2026

Summary

PR #185 introduced per-delegate isolation for the DeviceReceived event so a throwing subscriber no longer cuts off downstream handlers, with swallowed faults routed through InternalError. This PR closes the same bug class atomically across the entire codebase: the HTTP client surface (outer client and every sub-client), the HTTP server surface, both MQTT clients, the SHDR adapter and client, the device finder, and the agent / broker.

Changes

Shared helper

libraries/MTConnect.NET-Common/MulticastIsolation.cs — adds three overloads of MulticastIsolation.Raise:

  • Raise<T>(EventHandler<T>, object, T, EventHandler<Exception>) — generic EventHandler<T> events.
  • Raise(EventHandler, object, EventArgs, EventHandler<Exception>) — non-generic EventHandler events.
  • Raise<TDelegate>(TDelegate, Action<TDelegate>, EventHandler<Exception>, object) — any custom delegate shape; the caller supplies the invocation lambda so no per-signature overload is required.

All three overloads share the same contract: a throwing subscriber cannot starve later subscribers; subscriber faults are routed through the caller-supplied internalError sink, which is itself iterated per-delegate so a throwing fault reporter cannot starve later fault reporters; secondary faults from the internalError handler are terminal and swallowed.

HTTP clients

libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs, MTConnectHttpProbeClient.cs, MTConnectHttpCurrentClient.cs, MTConnectHttpSampleClient.cs, MTConnectHttpAssetClient.cs, MTConnectHttpClientStream.cs — every ?.Invoke raise site replaced with the appropriate MulticastIsolation.Raise call across the outer client and every sub-client.

MQTT clients

  • libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttClient.cs — 16 raise sites: ClientStarting, ClientStopping, ClientStarted, ClientStopped, ConnectionError, InternalError, DeviceReceived, ProbeReceived, ResponseReceived (×2), CurrentReceived, ObservationReceived (×2), SampleReceived, AssetsReceived, AssetReceived.
  • libraries/MTConnect.NET-MQTT/Clients/MTConnectMqttExpandedClient.cs — 9 raise sites: ClientStarting, ClientStopping, ClientStarted, ClientStopped, ConnectionError, InternalError, ObservationReceived (×2), ConnectionStatusChanged (×2), DeviceReceived, AssetReceived.

SHDR adapter and client

  • libraries/MTConnect.NET-SHDR/Adapters/ShdrAdapter.cs — 8 raise sites: AgentConnected, AgentDisconnected, PingReceived, PongSent, ConnectionError, LineSent (×2), SendError (×2).
  • libraries/MTConnect.NET-SHDR/Shdr/ShdrClient.cs — 20 raise sites: Connected, PingSent (×2), ConnectionError (×2), Disconnected (×2), Listening, PongReceived, ProtocolReceived (×11).

Device finder

  • libraries/MTConnect.NET-DeviceFinder/MTConnectDeviceFinder.cs — 8 raise sites: PingSent, PingReceived, SearchCompleted, PortOpened, PortClosed, ProbeSent, DeviceFound, ProbeSuccessful.
  • libraries/MTConnect.NET-DeviceFinder/PingQueue.cs — 3 raise sites: PingSent, PingReceived, Completed.

These classes declare every event with a custom delegate shape (DeviceHandler, PingSentHandler, RequestStatusHandler, etc.); each site uses the new Raise<TDelegate>(handler, invoke) overload with the per-subscriber invocation passed inline.

Agent and broker

  • libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs — 15 raise sites total: the EventHandler<T> events DeviceAdded, ObservationReceived, ObservationAdded (×4), AssetAdded, plus the custom-delegate validation events InvalidComponentAdded, InvalidCompositionAdded, InvalidDataItemAdded, InvalidObservationAdded (×3), InvalidDeviceAdded, InvalidAssetAdded (×2).
  • libraries/MTConnect.NET-Common/Agents/MTConnectAgentBroker.cs — 35 raise sites total: the EventHandler event StreamsResponseSent (×12), plus the custom-delegate request / response events DevicesRequestReceived (×2), DevicesResponseSent (×2), StreamsRequestReceived (×12), AssetsRequestReceived, DeviceAssetsRequestReceived, AssetsResponseSent (×2), ErrorResponseSent (×2).

HTTP server

  • libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpResponseHandler.cs — 5 raise sites: ClientConnected, ResponseSent, ClientDisconnected (×3), ClientException (×2).
  • libraries/MTConnect.NET-HTTP/Servers/MTConnectHttpServerStream.cs — 5 raise sites: StreamStarted, HeartbeatReceived, DocumentReceived, StreamException, StreamStopped.

Test coverage

  • tests/MTConnect.NET-Common-Tests/MulticastIsolationTests.cs — helper unit tests for all three overloads, including the generic-delegate path.
  • tests/MTConnect.NET-Common-Tests/MqttClientMulticastIsolationTests.cs — helper contract tests for the delegate shapes used by both MQTT clients.
  • tests/MTConnect.NET-Common-Tests/DeviceFinderMulticastIsolationTests.cs — helper contract tests for every custom-delegate shape declared on MTConnectDeviceFinder and PingQueue.
  • tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs — helper contract tests for every event on MTConnectAgent and MTConnectAgentBroker, including the validation and request / response custom delegates.
  • tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs — helper contract tests for EventHandler<string>, EventHandler<AdapterEventArgs<string>>, EventHandler<AdapterEventArgs<Exception>>, and EventHandler<Exception> shapes.
  • tests/MTConnect.NET-HTTP-Tests/Clients/MulticastIsolationTests.cs, NonGenericMulticastIsolationTests.cs, SubClientMulticastIsolationTests.cs — positive (multi-subscriber fan-out) and negative (InternalError-itself-throws) tests per raise site on the HTTP client surface.
  • tests/MTConnect.NET-HTTP-Tests/Integration/DevicesAndDeviceReceivedIntegrationTests.cs — end-to-end test for the probe → Devices cache → DeviceReceived flow.
  • tests/MTConnect.NET-HTTP-Tests/Servers/HttpServerMulticastIsolationTests.cs — helper contract tests for the HTTP server delegate shapes.

Verification

  • dotnet build MTConnect.NET.sln — green (0 warnings, 0 errors).
  • dotnet test MTConnect.NET.sln --filter "Category!=E2E&Category!=RequiresDocker&Category!=XsdLoadStrict" — green (4831 / 4831).
  • dotnet test tests/MTConnect.NET-Common-Tests --filter "FullyQualifiedName~MulticastIsolation" — green (80 / 80).

@ottobolyos ottobolyos force-pushed the fix/event-multicast-isolation branch from 09277af to f7dd36e Compare June 3, 2026 05:55
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Jun 3, 2026
@ottobolyos ottobolyos changed the title fix(http): isolate every event multicast across the HTTP client fix(common): isolate every event multicast across the codebase Jun 3, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Jun 3, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Jun 3, 2026
@ottobolyos ottobolyos force-pushed the fix/event-multicast-isolation branch from f6bc213 to 4ccfc69 Compare June 3, 2026 15:12
ottobolyos and others added 21 commits June 4, 2026 06:24
Each HTTP sub-client class -- MTConnectHttpProbeClient,
MTConnectHttpCurrentClient, MTConnectHttpSampleClient,
MTConnectHttpAssetClient, and MTConnectHttpClientStream -- now iterates
the multicast invocation list at every event raise site so one throwing
subscriber cannot starve later subscribers. Faults are forwarded through
the class's own InternalError event; a faulting InternalError handler is
also swallowed so the originating fan-out keeps going. Mirrors the
existing per-class helper pattern already applied to the outer
MTConnectHttpClient.

The sub-classes do not share a base type, so a per-class private
RaiseEvent helper keeps each class's InternalError routing
self-contained and adds no inheritance pressure. MTConnectHttpClientStream
gets both the generic RaiseEvent<T> and a non-generic RaiseEvent
sibling for the lifecycle events (Starting, Started, Stopping, Stopped).
Bare Raise{T} in the class-level summaries of
MqttClientMulticastIsolationTests, HttpServerMulticastIsolationTests,
and ShdrMulticastIsolationTests is ambiguous between the
EventHandler{T} overload (phase 1) and the generic-delegate overload
(phase 2b). docfx metadata with TreatWarningsAsErrors=true promotes
the resulting CS0419 to a build error, breaking the "Prepare generated
docs" CI job.

Replace each bare Raise{T} cref with the fully-qualified parameter
list that matches the overload the test class exercises. Also
disambiguate the Raise(EventHandler, ...) cross-reference in
MulticastIsolation.cs line 48 (non-generic overload summary).
@ottobolyos ottobolyos force-pushed the fix/event-multicast-isolation branch from 4ccfc69 to 82c1bc9 Compare June 4, 2026 04:24
@ottobolyos ottobolyos marked this pull request as ready for review June 4, 2026 04:31
@ottobolyos ottobolyos marked this pull request as draft June 4, 2026 05:03
The two `SampleReceived...WhenOneThrows` tests pushed an
UNAVAILABLE -> AVAILABLE pair once after the seed Current arrived and
then waited 30 s for the streaming sample loop to deliver. On the
Windows CI runner the pushed observations can land between two streaming
GETs: the prior request closes for the heartbeat round trip and the next
one resumes past the pushed sequences, leaving the assertion timing out
even though the multicast-isolation behavior is correct.

Hoist the push-and-wait into a `WaitForSampleReceivedWithTransitions`
helper that repushes every second until either the recorded handler
fires or the EventWaitTimeoutMs budget elapses. Each repush is a fresh
UNAVAILABLE -> AVAILABLE pair, so a new observation lands in the buffer
regardless of where the streaming loop is in its reconnect cycle.

The helper preserves the original semantics: a single repush in the
happy path returns within ~1 s, and the overall wait still tops out at
EventWaitTimeoutMs so a genuinely broken multicast still fails the test.
@ottobolyos ottobolyos marked this pull request as ready for review June 4, 2026 05:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants