refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Akka.Actor;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication.Actors;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Communication;
|
||||
|
||||
/// <summary>
|
||||
/// Manages debug stream sessions by creating DebugStreamBridgeActors that persist
|
||||
/// as subscribers on the site side. Both the Blazor debug view and the SignalR hub
|
||||
/// use this service to start/stop streams.
|
||||
/// </summary>
|
||||
public class DebugStreamService
|
||||
{
|
||||
private readonly CommunicationService _communicationService;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly SiteStreamGrpcClientFactory _grpcClientFactory;
|
||||
private readonly ILogger<DebugStreamService> _logger;
|
||||
private readonly ConcurrentDictionary<string, IActorRef> _sessions = new();
|
||||
private ActorSystem? _actorSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DebugStreamService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="communicationService">The communication service.</param>
|
||||
/// <param name="serviceProvider">The service provider for dependency resolution.</param>
|
||||
/// <param name="grpcClientFactory">The gRPC client factory for creating site stream clients.</param>
|
||||
/// <param name="logger">The logger instance.</param>
|
||||
public DebugStreamService(
|
||||
CommunicationService communicationService,
|
||||
IServiceProvider serviceProvider,
|
||||
SiteStreamGrpcClientFactory grpcClientFactory,
|
||||
ILogger<DebugStreamService> logger)
|
||||
{
|
||||
_communicationService = communicationService;
|
||||
_serviceProvider = serviceProvider;
|
||||
_grpcClientFactory = grpcClientFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the ActorSystem reference. Called during actor system startup (from AkkaHostedService).
|
||||
/// </summary>
|
||||
/// <param name="actorSystem">The actor system to use for creating bridge actors.</param>
|
||||
public void SetActorSystem(ActorSystem actorSystem)
|
||||
{
|
||||
_actorSystem = actorSystem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a debug stream session. Returns the initial snapshot.
|
||||
/// Ongoing events are delivered via the onEvent callback.
|
||||
/// The onTerminated callback fires if the stream is killed (site disconnect, timeout).
|
||||
/// </summary>
|
||||
/// <param name="instanceId">The instance ID to stream debug information for.</param>
|
||||
/// <param name="onEvent">Callback invoked for each event received from the stream.</param>
|
||||
/// <param name="onTerminated">Callback invoked when the stream terminates.</param>
|
||||
/// <param name="ct">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>A debug stream session with the initial snapshot.</returns>
|
||||
public async Task<DebugStreamSession> StartStreamAsync(
|
||||
int instanceId,
|
||||
Action<object> onEvent,
|
||||
Action onTerminated,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var system = _actorSystem
|
||||
?? throw new InvalidOperationException("DebugStreamService not initialized. ActorSystem not set.");
|
||||
|
||||
// Resolve instance → unique name + site
|
||||
string instanceUniqueName;
|
||||
string siteIdentifier;
|
||||
string grpcNodeAAddress;
|
||||
string grpcNodeBAddress;
|
||||
|
||||
using (var scope = _serviceProvider.CreateScope())
|
||||
{
|
||||
var instanceRepo = scope.ServiceProvider.GetRequiredService<ITemplateEngineRepository>();
|
||||
var instance = await instanceRepo.GetInstanceByIdAsync(instanceId)
|
||||
?? throw new InvalidOperationException($"Instance {instanceId} not found.");
|
||||
|
||||
var siteRepo = scope.ServiceProvider.GetRequiredService<ISiteRepository>();
|
||||
var site = await siteRepo.GetSiteByIdAsync(instance.SiteId)
|
||||
?? throw new InvalidOperationException($"Site {instance.SiteId} not found.");
|
||||
|
||||
instanceUniqueName = instance.UniqueName;
|
||||
siteIdentifier = site.SiteIdentifier;
|
||||
grpcNodeAAddress = site.GrpcNodeAAddress
|
||||
?? throw new InvalidOperationException($"Site {siteIdentifier} has no GrpcNodeAAddress configured.");
|
||||
grpcNodeBAddress = site.GrpcNodeBAddress
|
||||
?? throw new InvalidOperationException($"Site {siteIdentifier} has no GrpcNodeBAddress configured.");
|
||||
}
|
||||
|
||||
var sessionId = Guid.NewGuid().ToString("N");
|
||||
|
||||
// Capture the initial snapshot via a TaskCompletionSource
|
||||
var snapshotTcs = new TaskCompletionSource<DebugViewSnapshot>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
Action<object> onEventWrapper = evt =>
|
||||
{
|
||||
if (evt is DebugViewSnapshot snapshot && !snapshotTcs.Task.IsCompleted)
|
||||
{
|
||||
snapshotTcs.TrySetResult(snapshot);
|
||||
}
|
||||
else
|
||||
{
|
||||
onEvent(evt);
|
||||
}
|
||||
};
|
||||
|
||||
Action onTerminatedWrapper = () =>
|
||||
{
|
||||
_sessions.TryRemove(sessionId, out _);
|
||||
snapshotTcs.TrySetException(new InvalidOperationException("Debug stream terminated before snapshot received."));
|
||||
onTerminated();
|
||||
};
|
||||
|
||||
// Create the bridge actor — use type-based Props to avoid expression tree limitations with closures
|
||||
var commActor = _communicationService.GetCommunicationActor();
|
||||
|
||||
var props = Props.Create(typeof(DebugStreamBridgeActor),
|
||||
siteIdentifier,
|
||||
instanceUniqueName,
|
||||
sessionId,
|
||||
commActor,
|
||||
onEventWrapper,
|
||||
onTerminatedWrapper,
|
||||
_grpcClientFactory,
|
||||
grpcNodeAAddress,
|
||||
grpcNodeBAddress);
|
||||
|
||||
var bridgeActor = system.ActorOf(props, $"debug-stream-{sessionId}");
|
||||
|
||||
_sessions[sessionId] = bridgeActor;
|
||||
|
||||
// Wait for the initial snapshot (with timeout)
|
||||
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
timeoutCts.CancelAfter(TimeSpan.FromSeconds(30));
|
||||
|
||||
DebugViewSnapshot snapshot;
|
||||
try
|
||||
{
|
||||
snapshot = await snapshotTcs.Task.WaitAsync(timeoutCts.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Any failure before the snapshot arrives — the 30s timeout, or the stream
|
||||
// terminating early (site disconnect / gRPC failure, surfaced by
|
||||
// onTerminatedWrapper as an InvalidOperationException) — must deterministically
|
||||
// tear down the bridge actor and its site-side subscription. Use the local
|
||||
// actor reference: a racing onTerminatedWrapper may already have removed the
|
||||
// session, which would make StopStream a no-op. StopDebugStream is idempotent
|
||||
// (the actor may already be stopping itself).
|
||||
_sessions.TryRemove(sessionId, out _);
|
||||
bridgeActor.Tell(new StopDebugStream());
|
||||
|
||||
if (ex is OperationCanceledException)
|
||||
throw new TimeoutException(
|
||||
$"Timed out waiting for debug snapshot from {instanceUniqueName} on site {siteIdentifier}.");
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Debug stream for {instanceUniqueName} on site {siteIdentifier} terminated before a snapshot was received.",
|
||||
ex);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Debug stream {SessionId} started for {Instance} on site {Site}",
|
||||
sessionId, instanceUniqueName, siteIdentifier);
|
||||
|
||||
return new DebugStreamSession(sessionId, snapshot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops an active debug stream session.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The session ID of the debug stream to stop.</param>
|
||||
public void StopStream(string sessionId)
|
||||
{
|
||||
if (_sessions.TryRemove(sessionId, out var bridgeActor))
|
||||
{
|
||||
bridgeActor.Tell(new StopDebugStream());
|
||||
_logger.LogInformation("Debug stream {SessionId} stopped", sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record DebugStreamSession(string SessionId, DebugViewSnapshot InitialSnapshot);
|
||||
Reference in New Issue
Block a user