feat(mesh-phase5): dark-switch central telemetry ingest (Dps bridges | Grpc dialer)

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
Joseph Doherty
2026-07-23 16:48:34 -04:00
parent 84fa2e1e43
commit d15c613ef3
2 changed files with 200 additions and 16 deletions
@@ -1,7 +1,14 @@
using Akka.Actor;
using Akka.Hosting;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.OtOpcUa.AdminUI.Telemetry;
using ZB.MOM.WW.OtOpcUa.Cluster;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.ControlPlane.Telemetry;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
@@ -12,6 +19,7 @@ public static class HubServiceCollectionExtensions
public const string ScriptLogSignalRBridgeName = "script-log-signalr-bridge";
public const string DriverStatusSignalRBridgeName = "driver-status-signalr-bridge";
public const string DriverResilienceStatusBridgeName = "driver-resilience-status-bridge";
public const string TelemetryDialSupervisorName = "telemetry-dial-supervisor";
/// <summary>
/// Registers the in-process live-push services the AdminUI's Blazor Server panels read
@@ -54,30 +62,69 @@ public static class HubServiceCollectionExtensions
{
builder.WithActors((system, registry, resolver) =>
{
// Fleet-status always stays on DPS in both modes (deferred / out of Phase 5 scope) — it is
// never gated by the telemetry dark switch.
var fleetHub = resolver.GetService<IHubContext<FleetStatusHub>>();
var fleetBridge = system.ActorOf(FleetStatusSignalRBridge.Props(fleetHub), FleetStatusSignalRBridgeName);
registry.Register<FleetStatusSignalRBridgeKey>(fleetBridge);
var alertHub = resolver.GetService<IHubContext<AlertHub>>();
// The four telemetry sinks are registered identically in both modes; only the upstream that
// feeds them swaps (DPS bridges vs. the gRPC dial supervisor). Resolve them once, above the
// branch, so both paths feed the SAME singletons the Blazor panels read.
var alertBroadcaster = resolver.GetService<IInProcessBroadcaster<Commons.Messages.Alerts.AlarmTransitionEvent>>();
var alertBridge = system.ActorOf(AlertSignalRBridge.Props(alertHub, alertBroadcaster), AlertSignalRBridgeName);
registry.Register<AlertSignalRBridgeKey>(alertBridge);
var scriptLogHub = resolver.GetService<IHubContext<ScriptLogHub>>();
var scriptLogBroadcaster = resolver.GetService<IInProcessBroadcaster<Commons.Messages.Logging.ScriptLogEntry>>();
var scriptLogBridge = system.ActorOf(ScriptLogSignalRBridge.Props(scriptLogHub, scriptLogBroadcaster), ScriptLogSignalRBridgeName);
registry.Register<ScriptLogSignalRBridgeKey>(scriptLogBridge);
var driverStatusHub = resolver.GetService<IHubContext<DriverStatusHub>>();
var driverStatusStore = resolver.GetService<IDriverStatusSnapshotStore>();
var driverStatusBridge = system.ActorOf(DriverStatusSignalRBridge.Props(driverStatusHub, driverStatusStore), DriverStatusSignalRBridgeName);
registry.Register<DriverStatusSignalRBridgeKey>(driverStatusBridge);
// Resilience-status bridge: DPS topic -> in-process store (no SignalR hub — the panel reads
// the store directly, and resilience has no browser-JS consumer).
var resilienceStore = resolver.GetService<IDriverResilienceStatusStore>();
var resilienceBridge = system.ActorOf(DriverResilienceStatusBridge.Props(resilienceStore), DriverResilienceStatusBridgeName);
registry.Register<DriverResilienceStatusBridgeKey>(resilienceBridge);
// Phase 5 dark switch. Absent options ⇒ Dps (today's behaviour); case-insensitive.
var telemetryMode = resolver.GetService<IOptions<TelemetryDialOptions>>()?.Value.Mode
?? TelemetryDialOptions.ModeDps;
if (string.Equals(telemetryMode, TelemetryDialOptions.ModeGrpc, StringComparison.OrdinalIgnoreCase))
{
// Grpc: central dials each enabled node's dedicated telemetry stream, feeding the SAME
// four sinks the DPS bridges feed. No DPS telemetry bridges are spawned.
var options = resolver.GetService<IOptions<TelemetryDialOptions>>()!.Value;
var dbFactory = resolver.GetService<IDbContextFactory<OtOpcUaConfigDbContext>>();
var loggerFactory = resolver.GetService<ILoggerFactory>();
var nodeSource = TelemetryNodeSource.Create(
dbFactory!, loggerFactory!.CreateLogger(typeof(TelemetryNodeSource).FullName!));
var dialLoop = TelemetryNodeSource.CreateDialLoop(
options.ApiKey, loggerFactory.CreateLogger<TelemetryStreamClient>());
var supervisor = system.ActorOf(
TelemetryDialSupervisor.Props(
nodeSource,
dialLoop,
alertBroadcaster,
scriptLogBroadcaster,
driverStatusStore,
resilienceStore,
options),
TelemetryDialSupervisorName);
registry.Register<TelemetryDialSupervisorKey>(supervisor);
}
else
{
// Dps (default): the four DPS bridges subscribe their mesh-wide topics and feed the sinks.
var alertHub = resolver.GetService<IHubContext<AlertHub>>();
var alertBridge = system.ActorOf(AlertSignalRBridge.Props(alertHub, alertBroadcaster), AlertSignalRBridgeName);
registry.Register<AlertSignalRBridgeKey>(alertBridge);
var scriptLogHub = resolver.GetService<IHubContext<ScriptLogHub>>();
var scriptLogBridge = system.ActorOf(ScriptLogSignalRBridge.Props(scriptLogHub, scriptLogBroadcaster), ScriptLogSignalRBridgeName);
registry.Register<ScriptLogSignalRBridgeKey>(scriptLogBridge);
var driverStatusHub = resolver.GetService<IHubContext<DriverStatusHub>>();
var driverStatusBridge = system.ActorOf(DriverStatusSignalRBridge.Props(driverStatusHub, driverStatusStore), DriverStatusSignalRBridgeName);
registry.Register<DriverStatusSignalRBridgeKey>(driverStatusBridge);
// Resilience-status bridge: DPS topic -> in-process store (no SignalR hub — the panel reads
// the store directly, and resilience has no browser-JS consumer).
var resilienceBridge = system.ActorOf(DriverResilienceStatusBridge.Props(resilienceStore), DriverResilienceStatusBridgeName);
registry.Register<DriverResilienceStatusBridgeKey>(resilienceBridge);
}
});
return builder;
}
@@ -89,3 +136,6 @@ public sealed class AlertSignalRBridgeKey { }
public sealed class ScriptLogSignalRBridgeKey { }
public sealed class DriverStatusSignalRBridgeKey { }
public sealed class DriverResilienceStatusBridgeKey { }
/// <summary>Marker key for <see cref="ActorRegistry"/> lookup of the Grpc-mode telemetry dial supervisor.</summary>
public sealed class TelemetryDialSupervisorKey { }