From 75a6636a2c5c514b7aaae7a33164a84fdb92d68c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 18 Mar 2026 00:20:02 -0400 Subject: [PATCH] fix: wire DCL connection state changes into ISiteHealthCollector DataConnectionActor now calls UpdateConnectionHealth() on state transitions (Connecting/Connected/Reconnecting) and UpdateTagResolution() on connection establishment. DataConnectionManagerActor calls RemoveConnection() on actor removal. Health reports now include data connection statuses when instances are deployed with bindings. --- .../Actors/DataConnectionActor.cs | 8 ++++++++ .../Actors/DataConnectionManagerActor.cs | 9 +++++++-- .../ScadaLink.DataConnectionLayer.csproj | 1 + src/ScadaLink.Host/Actors/AkkaHostedService.cs | 3 ++- .../DataConnectionActorTests.cs | 5 ++++- .../DataConnectionManagerActorTests.cs | 9 ++++++--- 6 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs b/src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs index 02451d9..49b222a 100644 --- a/src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs +++ b/src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs @@ -3,6 +3,7 @@ using Akka.Event; using ScadaLink.Commons.Interfaces.Protocol; using ScadaLink.Commons.Messages.DataConnection; using ScadaLink.Commons.Types.Enums; +using ScadaLink.HealthMonitoring; namespace ScadaLink.DataConnectionLayer.Actors; @@ -28,6 +29,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers private readonly string _connectionName; private readonly IDataConnection _adapter; private readonly DataConnectionOptions _options; + private readonly ISiteHealthCollector _healthCollector; public IStash Stash { get; set; } = null!; public ITimerScheduler Timers { get; set; } = null!; @@ -64,11 +66,13 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers string connectionName, IDataConnection adapter, DataConnectionOptions options, + ISiteHealthCollector healthCollector, IDictionary? connectionDetails = null) { _connectionName = connectionName; _adapter = adapter; _options = options; + _healthCollector = healthCollector; _connectionDetails = connectionDetails ?? new Dictionary(); } @@ -96,6 +100,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers private void BecomeConnecting() { _log.Info("[{0}] Entering Connecting state", _connectionName); + _healthCollector.UpdateConnectionHealth(_connectionName, ConnectionHealth.Connecting); Become(Connecting); Self.Tell(new AttemptConnect()); } @@ -129,6 +134,8 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers private void BecomeConnected() { _log.Info("[{0}] Entering Connected state", _connectionName); + _healthCollector.UpdateConnectionHealth(_connectionName, ConnectionHealth.Connected); + _healthCollector.UpdateTagResolution(_connectionName, _totalSubscribed, _resolvedTags); Become(Connected); Stash.UnstashAll(); } @@ -166,6 +173,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers private void BecomeReconnecting() { _log.Warning("[{0}] Entering Reconnecting state", _connectionName); + _healthCollector.UpdateConnectionHealth(_connectionName, ConnectionHealth.Disconnected); Become(Reconnecting); // WP-9: Push bad quality for all subscribed tags on disconnect diff --git a/src/ScadaLink.DataConnectionLayer/Actors/DataConnectionManagerActor.cs b/src/ScadaLink.DataConnectionLayer/Actors/DataConnectionManagerActor.cs index d7b03d8..eacb5de 100644 --- a/src/ScadaLink.DataConnectionLayer/Actors/DataConnectionManagerActor.cs +++ b/src/ScadaLink.DataConnectionLayer/Actors/DataConnectionManagerActor.cs @@ -2,6 +2,7 @@ using Akka.Actor; using Akka.Event; using ScadaLink.Commons.Interfaces.Protocol; using ScadaLink.Commons.Messages.DataConnection; +using ScadaLink.HealthMonitoring; namespace ScadaLink.DataConnectionLayer.Actors; @@ -15,14 +16,17 @@ public class DataConnectionManagerActor : ReceiveActor private readonly ILoggingAdapter _log = Context.GetLogger(); private readonly IDataConnectionFactory _factory; private readonly DataConnectionOptions _options; + private readonly ISiteHealthCollector _healthCollector; private readonly Dictionary _connectionActors = new(); public DataConnectionManagerActor( IDataConnectionFactory factory, - DataConnectionOptions options) + DataConnectionOptions options, + ISiteHealthCollector healthCollector) { _factory = factory; _options = options; + _healthCollector = healthCollector; Receive(HandleCreateConnection); Receive(HandleRoute); @@ -44,7 +48,7 @@ public class DataConnectionManagerActor : ReceiveActor var adapter = _factory.Create(command.ProtocolType, command.ConnectionDetails); var props = Props.Create(() => new DataConnectionActor( - command.ConnectionName, adapter, _options, command.ConnectionDetails)); + command.ConnectionName, adapter, _options, _healthCollector, command.ConnectionDetails)); // Sanitize name for Akka actor path (replace spaces and invalid chars) var actorName = new string(command.ConnectionName @@ -97,6 +101,7 @@ public class DataConnectionManagerActor : ReceiveActor { Context.Stop(actor); _connectionActors.Remove(command.ConnectionName); + _healthCollector.RemoveConnection(command.ConnectionName); _log.Info("Removed DataConnectionActor for {0}", command.ConnectionName); } } diff --git a/src/ScadaLink.DataConnectionLayer/ScadaLink.DataConnectionLayer.csproj b/src/ScadaLink.DataConnectionLayer/ScadaLink.DataConnectionLayer.csproj index f91800d..d823592 100644 --- a/src/ScadaLink.DataConnectionLayer/ScadaLink.DataConnectionLayer.csproj +++ b/src/ScadaLink.DataConnectionLayer/ScadaLink.DataConnectionLayer.csproj @@ -20,6 +20,7 @@ + diff --git a/src/ScadaLink.Host/Actors/AkkaHostedService.cs b/src/ScadaLink.Host/Actors/AkkaHostedService.cs index 4a20a91..93fa8bb 100644 --- a/src/ScadaLink.Host/Actors/AkkaHostedService.cs +++ b/src/ScadaLink.Host/Actors/AkkaHostedService.cs @@ -219,9 +219,10 @@ akka {{ IActorRef? dclManager = null; if (dclFactory != null) { + var healthCollector = _serviceProvider.GetRequiredService(); dclManager = _actorSystem!.ActorOf( Props.Create(() => new ScadaLink.DataConnectionLayer.Actors.DataConnectionManagerActor( - dclFactory, dclOptions)), + dclFactory, dclOptions, healthCollector)), "dcl-manager"); _logger.LogInformation("Data Connection Layer manager actor created"); } diff --git a/tests/ScadaLink.DataConnectionLayer.Tests/DataConnectionActorTests.cs b/tests/ScadaLink.DataConnectionLayer.Tests/DataConnectionActorTests.cs index f4a6b82..89e7c26 100644 --- a/tests/ScadaLink.DataConnectionLayer.Tests/DataConnectionActorTests.cs +++ b/tests/ScadaLink.DataConnectionLayer.Tests/DataConnectionActorTests.cs @@ -5,6 +5,7 @@ using ScadaLink.Commons.Interfaces.Protocol; using ScadaLink.Commons.Messages.DataConnection; using ScadaLink.Commons.Types.Enums; using ScadaLink.DataConnectionLayer.Actors; +using ScadaLink.HealthMonitoring; namespace ScadaLink.DataConnectionLayer.Tests; @@ -21,11 +22,13 @@ public class DataConnectionActorTests : TestKit { private readonly IDataConnection _mockAdapter; private readonly DataConnectionOptions _options; + private readonly ISiteHealthCollector _mockHealthCollector; public DataConnectionActorTests() : base(@"akka.loglevel = DEBUG") { _mockAdapter = Substitute.For(); + _mockHealthCollector = Substitute.For(); _options = new DataConnectionOptions { ReconnectInterval = TimeSpan.FromMilliseconds(100), @@ -37,7 +40,7 @@ public class DataConnectionActorTests : TestKit private IActorRef CreateConnectionActor(string name = "test-conn") { return Sys.ActorOf(Props.Create(() => - new DataConnectionActor(name, _mockAdapter, _options)), name); + new DataConnectionActor(name, _mockAdapter, _options, _mockHealthCollector)), name); } [Fact] diff --git a/tests/ScadaLink.DataConnectionLayer.Tests/DataConnectionManagerActorTests.cs b/tests/ScadaLink.DataConnectionLayer.Tests/DataConnectionManagerActorTests.cs index 82b0684..5621882 100644 --- a/tests/ScadaLink.DataConnectionLayer.Tests/DataConnectionManagerActorTests.cs +++ b/tests/ScadaLink.DataConnectionLayer.Tests/DataConnectionManagerActorTests.cs @@ -4,6 +4,7 @@ using NSubstitute; using ScadaLink.Commons.Interfaces.Protocol; using ScadaLink.Commons.Messages.DataConnection; using ScadaLink.DataConnectionLayer.Actors; +using ScadaLink.HealthMonitoring; namespace ScadaLink.DataConnectionLayer.Tests; @@ -14,11 +15,13 @@ public class DataConnectionManagerActorTests : TestKit { private readonly IDataConnectionFactory _mockFactory; private readonly DataConnectionOptions _options; + private readonly ISiteHealthCollector _mockHealthCollector; public DataConnectionManagerActorTests() : base(@"akka.loglevel = DEBUG") { _mockFactory = Substitute.For(); + _mockHealthCollector = Substitute.For(); _options = new DataConnectionOptions { ReconnectInterval = TimeSpan.FromMilliseconds(100), @@ -30,7 +33,7 @@ public class DataConnectionManagerActorTests : TestKit public void WriteToUnknownConnection_ReturnsError() { var manager = Sys.ActorOf(Props.Create(() => - new DataConnectionManagerActor(_mockFactory, _options))); + new DataConnectionManagerActor(_mockFactory, _options, _mockHealthCollector))); manager.Tell(new WriteTagRequest( "corr1", "nonexistent", "tag1", 42, DateTimeOffset.UtcNow)); @@ -44,7 +47,7 @@ public class DataConnectionManagerActorTests : TestKit public void SubscribeToUnknownConnection_ReturnsError() { var manager = Sys.ActorOf(Props.Create(() => - new DataConnectionManagerActor(_mockFactory, _options))); + new DataConnectionManagerActor(_mockFactory, _options, _mockHealthCollector))); manager.Tell(new SubscribeTagsRequest( "corr1", "inst1", "nonexistent", ["tag1"], DateTimeOffset.UtcNow)); @@ -64,7 +67,7 @@ public class DataConnectionManagerActorTests : TestKit .Returns(mockAdapter); var manager = Sys.ActorOf(Props.Create(() => - new DataConnectionManagerActor(_mockFactory, _options))); + new DataConnectionManagerActor(_mockFactory, _options, _mockHealthCollector))); manager.Tell(new CreateConnectionCommand( "conn1", "OpcUa", new Dictionary()));