using Akka;
using Akka.Actor;
using Akka.Streams;
using Akka.Streams.Dsl;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Streaming;
///
/// Site-Wide Akka Stream — manages a broadcast stream for attribute value
/// and alarm state changes. Instance Actors publish events via fire-and-forget Tell.
/// A BroadcastHub fans events out to per-subscriber graphs, each filtered by
/// instance name and bounded by a drop-oldest buffer.
///
/// Filterable by instance name for debug view.
/// Implements ISiteStreamSubscriber so the gRPC server can subscribe actors
/// to instance events without referencing SiteRuntime directly.
///
public class SiteStreamManager : ISiteStreamSubscriber
{
private ActorSystem? _system;
private IMaterializer? _materializer;
private readonly int _bufferSize;
private readonly ILogger _logger;
private readonly object _lock = new();
private IActorRef? _sourceActor;
private Source? _hubSource;
private readonly Dictionary _subscriptions = new();
/// Initializes the stream manager with configuration and logger; the Akka stream is not started until is called.
/// Site runtime options providing the stream buffer size.
/// Logger instance.
public SiteStreamManager(
SiteRuntimeOptions options,
ILogger logger)
{
_bufferSize = options.StreamBufferSize;
_logger = logger;
}
///
/// Initializes the broadcast stream. Must be called after ActorSystem is ready.
/// The ActorSystem is passed here rather than via the constructor so that
/// SiteStreamManager can be created by DI before the actor system exists.
///
/// The running Akka used to materialize the broadcast stream.
public void Initialize(ActorSystem system)
{
_system = system;
_materializer = _system.Materializer();
var (sourceActor, hubSource) = Source.ActorRef(
_bufferSize,
OverflowStrategy.DropHead)
.ToMaterialized(
BroadcastHub.Sink(bufferSize: 256),
Keep.Both)
.Run(_materializer);
_sourceActor = sourceActor;
_hubSource = hubSource;
_logger.LogInformation(
"SiteStreamManager initialized with publish buffer size {BufferSize}", _bufferSize);
}
///
/// Publishes an attribute value change to the broadcast hub.
/// Fire-and-forget — never blocks the calling actor.
///
/// The attribute value change event to publish.
public void PublishAttributeValueChanged(AttributeValueChanged changed)
{
_sourceActor?.Tell(changed);
}
///
/// Publishes an alarm state change to the broadcast hub.
/// Fire-and-forget — never blocks the calling actor.
///
/// The alarm state change event to publish.
public void PublishAlarmStateChanged(AlarmStateChanged changed)
{
_sourceActor?.Tell(changed);
}
///
public string Subscribe(string instanceName, IActorRef subscriber)
{
if (_hubSource is null || _materializer is null)
throw new InvalidOperationException("SiteStreamManager.Initialize must be called before Subscribe");
var subscriptionId = Guid.NewGuid().ToString();
var capturedInstance = instanceName;
var capturedSubscriber = subscriber;
var killSwitch = _hubSource
.Where(ev => ev.InstanceUniqueName == capturedInstance)
.Buffer(_bufferSize, OverflowStrategy.DropHead)
.ViaMaterialized(KillSwitches.Single(), Keep.Right)
.To(Sink.ForEach(ev => capturedSubscriber.Tell(ev)))
.Run(_materializer);
lock (_lock)
{
_subscriptions[subscriptionId] = new SubscriptionInfo(
instanceName, subscriber, killSwitch, DateTimeOffset.UtcNow);
}
_logger.LogDebug(
"Subscriber {SubscriptionId} registered for instance {Instance}",
subscriptionId, instanceName);
return subscriptionId;
}
///
/// Unsubscribe from instance events. Shuts down the per-subscriber
/// stream graph via its KillSwitch.
///
/// The subscription ID returned by .
/// true if the subscription was found and removed; false if it was already gone.
public bool Unsubscribe(string subscriptionId)
{
SubscriptionInfo? info;
lock (_lock)
{
if (!_subscriptions.Remove(subscriptionId, out info))
return false;
}
info.KillSwitch.Shutdown();
_logger.LogDebug("Subscriber {SubscriptionId} removed", subscriptionId);
return true;
}
///
public void RemoveSubscriber(IActorRef subscriber)
{
List toShutdown;
lock (_lock)
{
var matched = _subscriptions
.Where(kvp => kvp.Value.Subscriber.Equals(subscriber))
.ToList();
foreach (var kvp in matched)
_subscriptions.Remove(kvp.Key);
toShutdown = matched.Select(kvp => kvp.Value).ToList();
}
foreach (var info in toShutdown)
info.KillSwitch.Shutdown();
if (toShutdown.Count > 0)
{
_logger.LogDebug(
"Removed {Count} subscriptions for disconnected subscriber", toShutdown.Count);
}
}
///
/// Returns the count of active subscriptions (for diagnostics/testing).
///
public int SubscriptionCount
{
get { lock (_lock) { return _subscriptions.Count; } }
}
private record SubscriptionInfo(
string InstanceName,
IActorRef Subscriber,
IKillSwitch KillSwitch,
DateTimeOffset SubscribedAt);
}