feat: replace debug view polling with real-time SignalR streaming
The debug view polled every 2s by re-subscribing for full snapshots. Now a persistent DebugStreamBridgeActor on central subscribes once and receives incremental Akka stream events from the site, forwarding them to the Blazor component via callbacks and to the CLI via a new SignalR hub at /hubs/debug-stream. Adds `debug stream` CLI command with auto-reconnect.
This commit is contained in:
100
src/ScadaLink.Communication/Actors/DebugStreamBridgeActor.cs
Normal file
100
src/ScadaLink.Communication/Actors/DebugStreamBridgeActor.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using Akka.Actor;
|
||||
using Akka.Event;
|
||||
using ScadaLink.Commons.Messages.DebugView;
|
||||
using ScadaLink.Commons.Messages.Streaming;
|
||||
|
||||
namespace ScadaLink.Communication.Actors;
|
||||
|
||||
/// <summary>
|
||||
/// Persistent actor (one per active debug session) on the central side.
|
||||
/// Sends SubscribeDebugViewRequest to the site via CentralCommunicationActor (with THIS actor
|
||||
/// as the Sender), so the site's InstanceActor registers this actor as the debug subscriber.
|
||||
/// Stream events flow back via Akka remoting and are forwarded to the consumer via callbacks.
|
||||
/// </summary>
|
||||
public class DebugStreamBridgeActor : ReceiveActor
|
||||
{
|
||||
private readonly ILoggingAdapter _log = Context.GetLogger();
|
||||
private readonly string _siteIdentifier;
|
||||
private readonly string _instanceUniqueName;
|
||||
private readonly string _correlationId;
|
||||
private readonly IActorRef _centralCommunicationActor;
|
||||
private readonly Action<object> _onEvent;
|
||||
private readonly Action _onTerminated;
|
||||
|
||||
public DebugStreamBridgeActor(
|
||||
string siteIdentifier,
|
||||
string instanceUniqueName,
|
||||
string correlationId,
|
||||
IActorRef centralCommunicationActor,
|
||||
Action<object> onEvent,
|
||||
Action onTerminated)
|
||||
{
|
||||
_siteIdentifier = siteIdentifier;
|
||||
_instanceUniqueName = instanceUniqueName;
|
||||
_correlationId = correlationId;
|
||||
_centralCommunicationActor = centralCommunicationActor;
|
||||
_onEvent = onEvent;
|
||||
_onTerminated = onTerminated;
|
||||
|
||||
// Initial snapshot response from the site
|
||||
Receive<DebugViewSnapshot>(snapshot =>
|
||||
{
|
||||
_log.Info("Received initial snapshot for {0} ({1} attrs, {2} alarms)",
|
||||
_instanceUniqueName, snapshot.AttributeValues.Count, snapshot.AlarmStates.Count);
|
||||
_onEvent(snapshot);
|
||||
});
|
||||
|
||||
// Ongoing stream events from the site's InstanceActor
|
||||
Receive<AttributeValueChanged>(changed => _onEvent(changed));
|
||||
Receive<AlarmStateChanged>(changed => _onEvent(changed));
|
||||
|
||||
// Consumer requests stop
|
||||
Receive<StopDebugStream>(_ =>
|
||||
{
|
||||
_log.Info("Stopping debug stream for {0}", _instanceUniqueName);
|
||||
SendUnsubscribe();
|
||||
Context.Stop(Self);
|
||||
});
|
||||
|
||||
// Site disconnected — CentralCommunicationActor notifies us
|
||||
Receive<DebugStreamTerminated>(msg =>
|
||||
{
|
||||
_log.Warning("Debug stream terminated for {0} (site {1} disconnected)", _instanceUniqueName, msg.SiteId);
|
||||
_onTerminated();
|
||||
Context.Stop(Self);
|
||||
});
|
||||
|
||||
// Orphan safety net — if nobody stops us within 5 minutes, self-terminate
|
||||
Context.SetReceiveTimeout(TimeSpan.FromMinutes(5));
|
||||
Receive<ReceiveTimeout>(_ =>
|
||||
{
|
||||
_log.Warning("Debug stream for {0} timed out (orphaned session), stopping", _instanceUniqueName);
|
||||
SendUnsubscribe();
|
||||
_onTerminated();
|
||||
Context.Stop(Self);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void PreStart()
|
||||
{
|
||||
_log.Info("Starting debug stream bridge for {0} on site {1}", _instanceUniqueName, _siteIdentifier);
|
||||
|
||||
// Send subscribe request via CentralCommunicationActor.
|
||||
// THIS actor is the Sender, so the site's InstanceActor registers us as the subscriber.
|
||||
var request = new SubscribeDebugViewRequest(_instanceUniqueName, _correlationId);
|
||||
var envelope = new SiteEnvelope(_siteIdentifier, request);
|
||||
_centralCommunicationActor.Tell(envelope, Self);
|
||||
}
|
||||
|
||||
private void SendUnsubscribe()
|
||||
{
|
||||
var request = new UnsubscribeDebugViewRequest(_instanceUniqueName, _correlationId);
|
||||
var envelope = new SiteEnvelope(_siteIdentifier, request);
|
||||
_centralCommunicationActor.Tell(envelope, Self);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message sent to a DebugStreamBridgeActor to stop the debug stream session.
|
||||
/// </summary>
|
||||
public record StopDebugStream;
|
||||
Reference in New Issue
Block a user