worker(alarms): synthetic GUID + degraded/source_provider on emitted transitions
This commit is contained in:
@@ -64,6 +64,41 @@ public sealed class AlarmDispatcherTests
|
|||||||
Assert.Equal(ts, body.TransitionTimestamp.ToDateTime());
|
Assert.Equal(ts, body.TransitionTimestamp.ToDateTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that a transition enqueued via the subtag fallback
|
||||||
|
/// (<c>degraded: true</c>) is marked <see cref="OnAlarmTransitionEvent.Degraded"/>
|
||||||
|
/// with <see cref="AlarmProviderMode.Subtag"/>, while the default path
|
||||||
|
/// stays on the alarmmgr parity contract.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void EnqueueTransition_WhenDegraded_MarksDegradedAndSubtagProvider()
|
||||||
|
{
|
||||||
|
MxAccessEventQueue queue = new MxAccessEventQueue();
|
||||||
|
MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink(queue, new MxAccessEventMapper());
|
||||||
|
sink.Attach(new object(), SessionId);
|
||||||
|
|
||||||
|
DateTime ts = new DateTime(2026, 5, 1, 17, 26, 14, 709, DateTimeKind.Utc);
|
||||||
|
sink.EnqueueTransition(
|
||||||
|
alarmFullReference: "Galaxy!TestArea.TestMachine_001.TestAlarm001",
|
||||||
|
sourceObjectReference: "TestMachine_001.TestAlarm001",
|
||||||
|
alarmTypeName: "DSC",
|
||||||
|
transitionKind: AlarmTransitionKind.Raise,
|
||||||
|
severity: 500,
|
||||||
|
originalRaiseTimestampUtc: null,
|
||||||
|
transitionTimestampUtc: ts,
|
||||||
|
operatorUser: string.Empty,
|
||||||
|
operatorComment: string.Empty,
|
||||||
|
category: "TestArea",
|
||||||
|
description: string.Empty,
|
||||||
|
degraded: true);
|
||||||
|
|
||||||
|
Assert.Equal(1, queue.Count);
|
||||||
|
Assert.True(queue.TryDequeue(out WorkerEvent? workerEvent));
|
||||||
|
OnAlarmTransitionEvent body = workerEvent!.Event.OnAlarmTransition;
|
||||||
|
Assert.True(body.Degraded);
|
||||||
|
Assert.Equal(AlarmProviderMode.Subtag, body.SourceProvider);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Verifies that unchanged alarm states do not emit transitions.</summary>
|
/// <summary>Verifies that unchanged alarm states do not emit transitions.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OnTransition_WithConsecutiveUnchangedState_DoesNotEmitTransition()
|
public void OnTransition_WithConsecutiveUnchangedState_DoesNotEmitTransition()
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unit tests for <see cref="SyntheticAlarmGuid"/>: the subtag-fallback
|
||||||
|
/// path derives a deterministic GUID from the alarm reference, so identical
|
||||||
|
/// references must collide and distinct references must not.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SyntheticAlarmGuidTests
|
||||||
|
{
|
||||||
|
/// <summary>Verifies the same reference yields the same GUID.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void SameReference_SameGuid() =>
|
||||||
|
Assert.Equal(SyntheticAlarmGuid.ForReference("A.B.C"), SyntheticAlarmGuid.ForReference("A.B.C"));
|
||||||
|
|
||||||
|
/// <summary>Verifies distinct references yield distinct GUIDs.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void DifferentReference_DifferentGuid() =>
|
||||||
|
Assert.NotEqual(SyntheticAlarmGuid.ForReference("A.B.C"), SyntheticAlarmGuid.ForReference("A.B.D"));
|
||||||
|
|
||||||
|
/// <summary>Verifies a reference produces a non-empty GUID.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Reference_ProducesNonEmptyGuid() =>
|
||||||
|
Assert.NotEqual(Guid.Empty, SyntheticAlarmGuid.ForReference("A.B.C"));
|
||||||
|
}
|
||||||
@@ -86,6 +86,12 @@ public sealed class MxAccessAlarmEventSink : IMxAccessEventSink
|
|||||||
/// <param name="operatorComment">The operator's comment, if any.</param>
|
/// <param name="operatorComment">The operator's comment, if any.</param>
|
||||||
/// <param name="category">The alarm category.</param>
|
/// <param name="category">The alarm category.</param>
|
||||||
/// <param name="description">The alarm description.</param>
|
/// <param name="description">The alarm description.</param>
|
||||||
|
/// <param name="degraded">
|
||||||
|
/// <see langword="true"/> when the transition was synthesized by the
|
||||||
|
/// subtag-provider fallback rather than the native alarmmgr path.
|
||||||
|
/// Defaults to <see langword="false"/> so existing alarmmgr callers
|
||||||
|
/// compile unchanged and stay on the parity (alarmmgr) path.
|
||||||
|
/// </param>
|
||||||
internal void EnqueueTransition(
|
internal void EnqueueTransition(
|
||||||
string alarmFullReference,
|
string alarmFullReference,
|
||||||
string sourceObjectReference,
|
string sourceObjectReference,
|
||||||
@@ -97,10 +103,16 @@ public sealed class MxAccessAlarmEventSink : IMxAccessEventSink
|
|||||||
string operatorUser,
|
string operatorUser,
|
||||||
string operatorComment,
|
string operatorComment,
|
||||||
string category,
|
string category,
|
||||||
string description)
|
string description,
|
||||||
|
bool degraded = false)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Degraded transitions come from the subtag fallback; the native
|
||||||
|
// alarmmgr (wnwrap) path stays degraded=false / ALARMMGR for parity.
|
||||||
|
AlarmProviderMode sourceProvider = degraded
|
||||||
|
? AlarmProviderMode.Subtag
|
||||||
|
: AlarmProviderMode.Alarmmgr;
|
||||||
MxEvent mxEvent = eventMapper.CreateOnAlarmTransition(
|
MxEvent mxEvent = eventMapper.CreateOnAlarmTransition(
|
||||||
sessionId,
|
sessionId,
|
||||||
alarmFullReference,
|
alarmFullReference,
|
||||||
@@ -114,7 +126,9 @@ public sealed class MxAccessAlarmEventSink : IMxAccessEventSink
|
|||||||
operatorComment,
|
operatorComment,
|
||||||
category,
|
category,
|
||||||
description,
|
description,
|
||||||
statuses: null);
|
statuses: null,
|
||||||
|
degraded: degraded,
|
||||||
|
sourceProvider: sourceProvider);
|
||||||
eventQueue.Enqueue(mxEvent);
|
eventQueue.Enqueue(mxEvent);
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
|
|||||||
@@ -124,6 +124,15 @@ public sealed class MxAccessEventMapper
|
|||||||
/// <param name="category">Alarm taxonomy bucket from the Galaxy template.</param>
|
/// <param name="category">Alarm taxonomy bucket from the Galaxy template.</param>
|
||||||
/// <param name="description">Human-readable alarm description.</param>
|
/// <param name="description">Human-readable alarm description.</param>
|
||||||
/// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
|
/// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
|
||||||
|
/// <param name="degraded">
|
||||||
|
/// <see langword="true"/> when this transition was synthesized by the
|
||||||
|
/// subtag-provider fallback rather than the native alarmmgr path.
|
||||||
|
/// Defaults to <see langword="false"/> to preserve alarmmgr parity.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="sourceProvider">
|
||||||
|
/// The alarm provider that sourced this transition. Defaults to
|
||||||
|
/// <see cref="AlarmProviderMode.Alarmmgr"/> for the native path.
|
||||||
|
/// </param>
|
||||||
public MxEvent CreateOnAlarmTransition(
|
public MxEvent CreateOnAlarmTransition(
|
||||||
string sessionId,
|
string sessionId,
|
||||||
string alarmFullReference,
|
string alarmFullReference,
|
||||||
@@ -137,7 +146,9 @@ public sealed class MxAccessEventMapper
|
|||||||
string operatorComment,
|
string operatorComment,
|
||||||
string category,
|
string category,
|
||||||
string description,
|
string description,
|
||||||
Array? statuses)
|
Array? statuses,
|
||||||
|
bool degraded = false,
|
||||||
|
AlarmProviderMode sourceProvider = AlarmProviderMode.Alarmmgr)
|
||||||
{
|
{
|
||||||
MxEvent mxEvent = CreateBaseEvent(
|
MxEvent mxEvent = CreateBaseEvent(
|
||||||
MxEventFamily.OnAlarmTransition,
|
MxEventFamily.OnAlarmTransition,
|
||||||
@@ -159,6 +170,8 @@ public sealed class MxAccessEventMapper
|
|||||||
OperatorComment = operatorComment ?? string.Empty,
|
OperatorComment = operatorComment ?? string.Empty,
|
||||||
Category = category ?? string.Empty,
|
Category = category ?? string.Empty,
|
||||||
Description = description ?? string.Empty,
|
Description = description ?? string.Empty,
|
||||||
|
Degraded = degraded,
|
||||||
|
SourceProvider = sourceProvider,
|
||||||
};
|
};
|
||||||
if (originalRaiseTimestampUtc is { } orts)
|
if (originalRaiseTimestampUtc is { } orts)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,4 +37,12 @@ public sealed class MxAlarmSnapshotRecord
|
|||||||
public string OperatorName { get; set; } = string.Empty;
|
public string OperatorName { get; set; } = string.Empty;
|
||||||
/// <summary>Gets or sets the alarm comment.</summary>
|
/// <summary>Gets or sets the alarm comment.</summary>
|
||||||
public string AlarmComment { get; set; } = string.Empty;
|
public string AlarmComment { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether this record was synthesized
|
||||||
|
/// by the subtag-provider fallback rather than emitted by the native
|
||||||
|
/// alarmmgr (wnwrap) path. Default <see langword="false"/> preserves
|
||||||
|
/// parity for the alarmmgr path; the subtag fallback sets it to
|
||||||
|
/// <see langword="true"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool Degraded { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Derives a deterministic synthetic <see cref="Guid"/> from an alarm
|
||||||
|
/// reference for the subtag-provider fallback path, which has no native
|
||||||
|
/// MxAccess alarm GUID. Hashing the reference yields a stable identity so
|
||||||
|
/// repeated transitions for the same alarm reference correlate downstream
|
||||||
|
/// (acknowledge, snapshot, OPC UA mapping) without an alarmmgr-supplied GUID.
|
||||||
|
/// </summary>
|
||||||
|
public static class SyntheticAlarmGuid
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Produces a stable <see cref="Guid"/> for the given alarm reference.
|
||||||
|
/// The same reference always maps to the same GUID; distinct references
|
||||||
|
/// map to distinct GUIDs with overwhelming probability.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reference">
|
||||||
|
/// The fully-qualified alarm reference (for example
|
||||||
|
/// <c>"Galaxy!Area.Tag.HiHi"</c>). Treated as UTF-8 bytes.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>A deterministic, non-empty GUID derived from the reference.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Thrown when <paramref name="reference"/> is <see langword="null"/>.
|
||||||
|
/// </exception>
|
||||||
|
public static Guid ForReference(string reference)
|
||||||
|
{
|
||||||
|
if (reference is null) throw new ArgumentNullException(nameof(reference));
|
||||||
|
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(reference);
|
||||||
|
|
||||||
|
// MD5 is used purely for a stable, non-cryptographic identity mapping
|
||||||
|
// (reference -> 16-byte GUID), never for security. Its 128-bit output
|
||||||
|
// fits a GUID exactly, which is why it is preferred here.
|
||||||
|
using MD5 md5 = MD5.Create();
|
||||||
|
byte[] hash = md5.ComputeHash(bytes);
|
||||||
|
return new Guid(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user