fix(debug-stream): M2.18 review nits — thread-safe test mock + AlarmKey null-guard + rename stale test (#26)
- MockSiteStreamGrpcClient.SubscribeCalls and UnsubscribedCorrelationIds switched from bare List<T> to lock-guarded backing fields with snapshot accessors, eliminating the actor-thread/test-thread data race (matches the existing lock(events) pattern for ReceivedEvents) - AttributeKey and AlarmKey null-guard each component with ?? string.Empty so a null SourceReference/AlarmName/etc. cannot silently collide with an empty-string component in the dedup dictionary - On_Snapshot_Opens_GrpcStream renamed to On_Snapshot_Does_Not_Open_Additional_GrpcStream; assertion updated to confirm exactly one subscribe (the PreStart stream-first open) with no second subscribe after snapshot delivery - _stopped ordering in InstanceNotFound path moved after CleanupGrpc() for consistency with DebugStreamTerminated and ReceiveTimeout handlers
This commit is contained in:
@@ -147,11 +147,13 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers
|
||||
{
|
||||
_log.Warning("Instance {0} is not deployed on site; terminating debug stream",
|
||||
_instanceUniqueName);
|
||||
_stopped = true;
|
||||
// M2.18: the stream-first subscription opened in PreStart is for a
|
||||
// non-deployed instance — cancel it (and any buffered gap events are
|
||||
// discarded with the actor). No pass-through.
|
||||
// _stopped is set AFTER CleanupGrpc() to match the ordering in the
|
||||
// DebugStreamTerminated and ReceiveTimeout handlers (cosmetic consistency).
|
||||
CleanupGrpc();
|
||||
_stopped = true;
|
||||
_preSnapshotBuffer.Clear();
|
||||
_onEvent(snapshot); // resolves the snapshot TCS with InstanceNotFound=true
|
||||
// Note: after Context.Stop(Self) below the actor is dead. DebugStreamService
|
||||
@@ -358,17 +360,29 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers
|
||||
/// </summary>
|
||||
private const char KeyDelimiter = '\u0000';
|
||||
|
||||
/// <summary>Per-entity dedup key for an attribute change.</summary>
|
||||
/// <summary>
|
||||
/// Per-entity dedup key for an attribute change. Each nullable component is guarded
|
||||
/// with <c>?? string.Empty</c> so a null can never silently collide with another
|
||||
/// key via <see cref="string.Concat"/> (e.g. two entries with null AttributePath
|
||||
/// would otherwise share a key with any entry whose AttributePath is the empty string).
|
||||
/// </summary>
|
||||
private static string AttributeKey(AttributeValueChanged a) =>
|
||||
string.Concat(a.InstanceUniqueName, KeyDelimiter, a.AttributePath, KeyDelimiter, a.AttributeName);
|
||||
string.Concat(
|
||||
a.InstanceUniqueName ?? string.Empty, KeyDelimiter,
|
||||
a.AttributePath ?? string.Empty, KeyDelimiter,
|
||||
a.AttributeName ?? string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Per-entity dedup key for an alarm change. Includes <see cref="AlarmStateChanged.SourceReference"/>
|
||||
/// so native per-condition alarms (which share an AlarmName but differ by source
|
||||
/// reference) are not conflated; empty for computed alarms.
|
||||
/// reference) are not conflated; empty for computed alarms. Each nullable component is
|
||||
/// guarded with <c>?? string.Empty</c> to prevent silent key collisions.
|
||||
/// </summary>
|
||||
private static string AlarmKey(AlarmStateChanged al) =>
|
||||
string.Concat(al.InstanceUniqueName, KeyDelimiter, al.AlarmName, KeyDelimiter, al.SourceReference);
|
||||
string.Concat(
|
||||
al.InstanceUniqueName ?? string.Empty, KeyDelimiter,
|
||||
al.AlarmName ?? string.Empty, KeyDelimiter,
|
||||
al.SourceReference ?? string.Empty);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void PreStart()
|
||||
|
||||
Reference in New Issue
Block a user