fix(communication): drop IsConfiguredPlaceholder rows in StreamRelayActor before gRPC pack
Placeholder AlarmStateChanged rows are a DebugView snapshot-only concept emitted by InstanceActor.BuildAlarmStatesSnapshot; they are never a real alarm transition. Their timestamp may be DateTimeOffset.MinValue (the Protobuf Timestamp lower boundary), which can throw when packed via Timestamp.FromDateTimeOffset. Added early-return guard at the top of HandleAlarmStateChanged before any timestamp pack or channel write. Updated the existing NativeBindingLinkage round-trip test to use a real (non-placeholder) native alarm; added DropsAlarmStateChanged_WhenIsConfiguredPlaceholder to assert placeholders are silently dropped (15/15 pass).
This commit is contained in:
@@ -56,6 +56,15 @@ public class StreamRelayActor : ReceiveActor
|
||||
|
||||
private void HandleAlarmStateChanged(AlarmStateChanged msg)
|
||||
{
|
||||
// Placeholder rows (IsConfiguredPlaceholder) are a Debug View snapshot-only
|
||||
// concept emitted by InstanceActor.BuildAlarmStatesSnapshot — they are never a
|
||||
// real alarm transition and must not be relayed to the live gRPC stream (their
|
||||
// timestamp may be DateTimeOffset.MinValue, the Protobuf Timestamp boundary).
|
||||
if (msg.IsConfiguredPlaceholder)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var protoEvent = new SiteStreamEvent
|
||||
{
|
||||
CorrelationId = _correlationId,
|
||||
|
||||
@@ -116,9 +116,9 @@ public class StreamRelayActorTests : TestKit
|
||||
[Fact]
|
||||
public void RelaysAlarmStateChanged_NativeBindingLinkage_SurvivesFullRoundTrip()
|
||||
{
|
||||
// DV-1: the native source-binding canonical name and the configured-placeholder
|
||||
// flag must pack into AlarmStateUpdate (StreamRelayActor) and unpack back out
|
||||
// (SiteStreamGrpcClient.ConvertToDomainEvent) — the full cross-process round trip.
|
||||
// DV-1: the native source-binding canonical name must pack into AlarmStateUpdate
|
||||
// (StreamRelayActor) and unpack back out (SiteStreamGrpcClient.ConvertToDomainEvent)
|
||||
// — the full cross-process round trip for a real (non-placeholder) native alarm.
|
||||
var channel = Channel.CreateUnbounded<SiteStreamEvent>();
|
||||
var actor = Sys.ActorOf(Props.Create(() =>
|
||||
new StreamRelayActor("corr-native-binding", channel.Writer)));
|
||||
@@ -130,7 +130,7 @@ public class StreamRelayActorTests : TestKit
|
||||
Kind = AlarmKind.NativeOpcUa,
|
||||
SourceReference = "Motor1.MotorAlarms.Hi",
|
||||
NativeSourceCanonicalName = "Motor1.MotorAlarms",
|
||||
IsConfiguredPlaceholder = true,
|
||||
IsConfiguredPlaceholder = false,
|
||||
Condition = new AlarmConditionState(
|
||||
Active: true, Acknowledged: false, Confirmed: null,
|
||||
Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: 900)
|
||||
@@ -150,13 +150,46 @@ public class StreamRelayActorTests : TestKit
|
||||
|
||||
// Packed onto the wire frame by StreamRelayActor.
|
||||
Assert.Equal("Motor1.MotorAlarms", protoEvent.AlarmChanged.NativeSourceCanonicalName);
|
||||
Assert.True(protoEvent.AlarmChanged.IsConfiguredPlaceholder);
|
||||
Assert.False(protoEvent.AlarmChanged.IsConfiguredPlaceholder);
|
||||
|
||||
// Unpacked back into the domain record by SiteStreamGrpcClient.
|
||||
var roundTripped = Assert.IsType<AlarmStateChanged>(
|
||||
SiteStreamGrpcClient.ConvertToDomainEvent(protoEvent));
|
||||
Assert.Equal("Motor1.MotorAlarms", roundTripped.NativeSourceCanonicalName);
|
||||
Assert.True(roundTripped.IsConfiguredPlaceholder);
|
||||
Assert.False(roundTripped.IsConfiguredPlaceholder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropsAlarmStateChanged_WhenIsConfiguredPlaceholder()
|
||||
{
|
||||
// Placeholder rows are a Debug View snapshot-only concept emitted by
|
||||
// InstanceActor.BuildAlarmStatesSnapshot. They are never a real alarm
|
||||
// transition; their timestamp may be DateTimeOffset.MinValue (the Protobuf
|
||||
// Timestamp boundary). The relay must drop them without writing to the channel.
|
||||
var channel = Channel.CreateUnbounded<SiteStreamEvent>();
|
||||
var actor = Sys.ActorOf(Props.Create(() =>
|
||||
new StreamRelayActor("corr-placeholder-drop", channel.Writer)));
|
||||
|
||||
var domainEvent = new AlarmStateChanged(
|
||||
"Site1.Tank01", "Tank1.Levels.Hi", AlarmState.Normal, 500,
|
||||
DateTimeOffset.MinValue)
|
||||
{
|
||||
Kind = AlarmKind.NativeOpcUa,
|
||||
NativeSourceCanonicalName = "Tank1.Levels",
|
||||
IsConfiguredPlaceholder = true,
|
||||
Condition = new AlarmConditionState(
|
||||
Active: false, Acknowledged: false, Confirmed: null,
|
||||
Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: 0)
|
||||
};
|
||||
|
||||
actor.Tell(domainEvent);
|
||||
|
||||
// Allow time for the actor to process the message.
|
||||
Thread.Sleep(500);
|
||||
|
||||
// Nothing must have been written — placeholder is silently dropped.
|
||||
Assert.False(channel.Reader.TryRead(out _),
|
||||
"Placeholder AlarmStateChanged must not be relayed to the gRPC stream");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user