09a401b881
Nine drivers raise IRediscoverable.OnRediscoveryNeeded when they observe that a remote's tag set may have changed — a Galaxy redeploy, a TwinCAT symbol-version bump, an MTConnect agent restart, a Sparkplug rebirth. Nothing in src/ subscribed, so every raise went into the void. Drivers even wrap the invoke in try/catch for "subscriber threw"; there has never been a subscriber. DriverInstanceActor now attaches the event in PreStart and detaches in PostStop, mirroring AttachAlarmSource/DetachAlarmSource. Attach is per-actor, not per-connect: the raise has no connection affinity and can land while the driver is between connects. The detach is load-bearing rather than tidy — the IDriver instance outlives the actor when the host respawns a child around the same driver object, so a missing -= accumulates one handler per respawn, each holding a dead Self. The signal rides the existing driver-health snapshot to /hosts as a "re-browse" chip. It is ADVISORY: the served address space is deliberately not rebuilt, because v3 authors raw tags explicitly through the /raw browse-commit flow and a runtime graft would materialise nodes no operator approved and no deployment artifact records. Two things worth knowing: - PublishHealthSnapshot dedups on a health fingerprint, and a rediscovery raise on an otherwise-healthy driver changes none of the four fields it hashed. Without adding the timestamp to that tuple the dedup swallows the exact publish carrying the signal. Reverting that one line turns 3 of the 5 new tests red, which is how it was confirmed rather than assumed. - The new test stub implements IDriver from scratch instead of deriving from the shared StubDriver, whose GetHealth() returns DateTime.UtcNow — its fingerprint differs on every call, so the dedup never engages and the dedup test would have passed vacuously either way. The planned discovery-result drift detector was NOT built. Modbus, S7, MQTT, AbLegacy and Sql all echo authored config from DiscoverAsync rather than browsing the device, so a discovered-vs-authored diff is permanently empty for them — it would have been another plausible-looking inert seam, which is the class this audit exists to remove. IRediscoverable is the trustworthy signal because the driver asserts it deliberately. DriverHealthChanged, IDriverHealthPublisher.Publish and HostsDriverRow gain defaulted optional parameters, so no existing call site changed. telemetry.proto gains fields 8/9 and both Phase-5 mappers carry them. Runtime.Tests 512 passed / 31 skipped.
57 lines
2.2 KiB
C#
57 lines
2.2 KiB
C#
using Akka.Actor;
|
|
using Akka.Cluster.Tools.PublishSubscribe;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Telemetry;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
|
|
|
/// <summary>
|
|
/// Forwards <see cref="DriverHealth"/> transitions to the cluster-wide
|
|
/// <c>driver-health</c> DistributedPubSub topic. Consumed by the AdminUI
|
|
/// <c>DriverStatusSignalRBridge</c>.
|
|
/// </summary>
|
|
public sealed class AkkaDriverHealthPublisher : IDriverHealthPublisher
|
|
{
|
|
/// <summary>The DistributedPubSub topic name for driver-health snapshots — single source
|
|
/// of truth on the message contract itself.</summary>
|
|
public const string TopicName = DriverHealthChanged.TopicName;
|
|
|
|
private readonly ActorSystem _system;
|
|
private readonly ITelemetryLocalHub _hub;
|
|
|
|
/// <summary>Initializes a new instance of <see cref="AkkaDriverHealthPublisher"/>.</summary>
|
|
/// <param name="system">The Akka actor system used to resolve the DPS mediator.</param>
|
|
/// <param name="hub">The node-local live-telemetry hub each snapshot is also emitted into (Phase 5).</param>
|
|
public AkkaDriverHealthPublisher(ActorSystem system, ITelemetryLocalHub hub)
|
|
{
|
|
_system = system;
|
|
_hub = hub;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Publish(
|
|
string clusterId,
|
|
string driverInstanceId,
|
|
DriverHealth health,
|
|
int errorCount5Min,
|
|
DateTime? rediscoveryNeededUtc = null,
|
|
string? rediscoveryReason = null)
|
|
{
|
|
var msg = new DriverHealthChanged(
|
|
clusterId,
|
|
driverInstanceId,
|
|
health.State.ToString(),
|
|
health.LastSuccessfulRead,
|
|
health.LastError,
|
|
errorCount5Min,
|
|
DateTime.UtcNow,
|
|
rediscoveryNeededUtc,
|
|
rediscoveryReason);
|
|
DistributedPubSub.Get(_system).Mediator.Tell(new Publish(TopicName, msg));
|
|
// Phase 5: fan the same snapshot into the node-local live-telemetry hub (no-op until a gRPC
|
|
// client subscribes). The DPS publish above is unchanged — the hub is a strictly additive tap.
|
|
_hub.Emit(new TelemetryItem.Health(msg));
|
|
}
|
|
}
|