feat: add StreamRelayActor bridging Akka events to gRPC proto channel

This commit is contained in:
Joseph Doherty
2026-03-21 11:48:04 -04:00
parent 9b0a80dcbd
commit d70bbbe739
2 changed files with 266 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
using System.Threading.Channels;
using Akka.Actor;
using Akka.Event;
using Google.Protobuf.WellKnownTypes;
using ScadaLink.Commons.Messages.Streaming;
using ScadaLink.Communication.Grpc;
using AlarmState = ScadaLink.Commons.Types.Enums.AlarmState;
namespace ScadaLink.Communication.Actors;
/// <summary>
/// Lightweight relay actor that bridges Akka domain events (AttributeValueChanged,
/// AlarmStateChanged) to a System.Threading.Channels.Channel of protobuf SiteStreamEvent
/// messages. The gRPC server method reads from the channel's reader side.
/// </summary>
public class StreamRelayActor : ReceiveActor
{
private readonly ILoggingAdapter _log = Context.GetLogger();
private readonly string _correlationId;
private readonly ChannelWriter<SiteStreamEvent> _channelWriter;
public StreamRelayActor(string correlationId, ChannelWriter<SiteStreamEvent> channelWriter)
{
_correlationId = correlationId;
_channelWriter = channelWriter;
Receive<AttributeValueChanged>(HandleAttributeValueChanged);
Receive<AlarmStateChanged>(HandleAlarmStateChanged);
}
private void HandleAttributeValueChanged(AttributeValueChanged msg)
{
var protoEvent = new SiteStreamEvent
{
CorrelationId = _correlationId,
AttributeChanged = new AttributeValueUpdate
{
InstanceUniqueName = msg.InstanceUniqueName,
AttributePath = msg.AttributePath,
AttributeName = msg.AttributeName,
Value = msg.Value?.ToString() ?? "",
Quality = MapQuality(msg.Quality),
Timestamp = Timestamp.FromDateTimeOffset(msg.Timestamp)
}
};
WriteToChannel(protoEvent);
}
private void HandleAlarmStateChanged(AlarmStateChanged msg)
{
var protoEvent = new SiteStreamEvent
{
CorrelationId = _correlationId,
AlarmChanged = new AlarmStateUpdate
{
InstanceUniqueName = msg.InstanceUniqueName,
AlarmName = msg.AlarmName,
State = MapAlarmState(msg.State),
Priority = msg.Priority,
Timestamp = Timestamp.FromDateTimeOffset(msg.Timestamp)
}
};
WriteToChannel(protoEvent);
}
private void WriteToChannel(SiteStreamEvent protoEvent)
{
if (!_channelWriter.TryWrite(protoEvent))
{
_log.Warning("Channel full, dropping event for correlation {0}", _correlationId);
}
}
private static Quality MapQuality(string quality) => quality switch
{
"Good" => Quality.Good,
"Uncertain" => Quality.Uncertain,
"Bad" => Quality.Bad,
_ => Quality.Unspecified
};
private static AlarmStateEnum MapAlarmState(AlarmState state) => state switch
{
AlarmState.Normal => AlarmStateEnum.AlarmStateNormal,
AlarmState.Active => AlarmStateEnum.AlarmStateActive,
_ => AlarmStateEnum.AlarmStateUnspecified
};
}