fix(common): isolate every event multicast across the codebase#188
Conversation
09277af to
f7dd36e
Compare
f6bc213 to
4ccfc69
Compare
4ccfc69 to
82c1bc9
Compare
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).
…m-delegate events
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).
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.
3378cfd to
6a32532
Compare
|
I'm wondering if it might be better to make the MulticastIsolation.Raise() method into an extension method. May also want to make the Example: public static void Raise<T>(this EventHandler<T> handler, object sender, T arg, EventHandler<Exception> internalError = null)
public static void Raise(this EventHandler handler, object sender, EventArgs arg, EventHandler<Exception> internalError = null)Example Usage: ObservationAdded.Raise(this, observation); |
Make the typed EventHandler and EventHandler<T> overloads extension methods
and default internalError to null, so call sites read as
MyEvent.Raise(this, args);
MyEvent.Raise(this, args, InternalError);
instead of
MulticastIsolation.Raise(MyEvent, this, args, InternalError);
The generic-delegate overload keeps its static-call form: extension-method
syntax does not compose cleanly with a where TDelegate : Delegate constraint
at the call site.
Switch every typed MulticastIsolation.Raise(...) call in the HTTP client surface to the new extension-method form (event.Raise(this, args, sink)). No behavioural change.
Switch every typed MulticastIsolation.Raise(...) call in the HTTP server surface to the new extension-method form (event.Raise(this, args, sink)). No behavioural change.
Switch every typed MulticastIsolation.Raise(...) call in the MQTT client surface to the new extension-method form (event.Raise(sender, args, sink)). No behavioural change.
Switch every typed MulticastIsolation.Raise(...) call in the SHDR adapter and client surfaces to the new extension-method form (event.Raise(this, args, sink)). No behavioural change.
Switch typed MulticastIsolation.Raise(...) calls in MTConnectAgent and MTConnectAgentBroker to the new extension-method form (event.Raise(this, args, sink)). Custom-delegate events keep the generic-delegate static call because extension-method syntax does not compose with the helper's where TDelegate : Delegate constraint. No behavioural change.
Update every typed-overload call site in the multicast-isolation tests to match the new extension-method form (event.Raise(this, args, sink)). Custom-delegate tests keep the generic-delegate static call. The XML cref references in the test class summaries still resolve because the method signature (parameter types) is unchanged by the this modifier.
|
Good call — extension-method syntax reads much cleaner at the call sites. Applied across the typed |
Summary
PR #185 introduced per-delegate isolation for the
DeviceReceivedevent so a throwing subscriber no longer cuts off downstream handlers, with swallowed faults routed throughInternalError. 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 theMulticastIsolationstatic class with threeRaiseshapes:Raise<T>(this EventHandler<T>, object, T, EventHandler<Exception> = null)— extension method onEventHandler<T>events.Raise(this EventHandler, object, EventArgs, EventHandler<Exception> = null)— extension method on non-genericEventHandlerevents.Raise<TDelegate>(TDelegate, Action<TDelegate>, EventHandler<Exception> = null, object = null) where TDelegate : Delegate— static call for any custom delegate shape; the caller supplies the per-subscriber invocation lambda so no per-signature overload is required. Stays a static call because extension-method syntax does not compose cleanly with thewhere TDelegate : Delegateconstraint at the call site.All three shapes share the same contract: a throwing subscriber cannot starve later subscribers; subscriber faults are routed through the caller-supplied
internalErrorsink, which is itself iterated per-delegate so a throwing fault reporter cannot starve later fault reporters; secondary faults from theinternalErrorhandler are terminal and swallowed.Call sites for the typed overloads read as:
HTTP clients
libraries/MTConnect.NET-HTTP/Clients/MTConnectHttpClient.cs,MTConnectHttpProbeClient.cs,MTConnectHttpCurrentClient.cs,MTConnectHttpSampleClient.cs,MTConnectHttpAssetClient.cs,MTConnectHttpClientStream.cs— every?.Invokeraise site replaced with the appropriateRaiseextension-method 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 generic-delegateMulticastIsolation.Raise(handler, invoke)static call with the per-subscriber invocation passed inline.Agent and broker
libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs— 15 raise sites total: theEventHandler<T>eventsDeviceAdded,ObservationReceived,ObservationAdded(×4),AssetAddeduse the extension-method form; the custom-delegate validation eventsInvalidComponentAdded,InvalidCompositionAdded,InvalidDataItemAdded,InvalidObservationAdded(×3),InvalidDeviceAdded,InvalidAssetAdded(×2) use the generic-delegate static call.libraries/MTConnect.NET-Common/Agents/MTConnectAgentBroker.cs— 35 raise sites total: theEventHandlereventStreamsResponseSent(×12) uses the extension-method form; the custom-delegate request / response eventsDevicesRequestReceived(×2),DevicesResponseSent(×2),StreamsRequestReceived(×12),AssetsRequestReceived,DeviceAssetsRequestReceived,AssetsResponseSent(×2),ErrorResponseSent(×2) use the generic-delegate static call.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 shapes, 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 onMTConnectDeviceFinderandPingQueue.tests/MTConnect.NET-Common-Tests/Agents/AgentMulticastIsolationTests.cs— helper contract tests for every event onMTConnectAgentandMTConnectAgentBroker, including the validation and request / response custom delegates.tests/MTConnect.NET-SHDR-Tests/ShdrMulticastIsolationTests.cs— helper contract tests forEventHandler<string>,EventHandler<AdapterEventArgs<string>>,EventHandler<AdapterEventArgs<Exception>>, andEventHandler<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 →Devicescache →DeviceReceivedflow.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).