fix(template-engine): native alarm sources participate in the revision hash — staleness detection restored (deliberate one-time stale flip for affected instances)
TemplateEngine-011: ResolvedNativeAlarmSource bindings were absent from the flattened-config revision hash, so edits to a native alarm source binding (source reference, connection, condition filter, locked state) went undetected by staleness detection. Add a HashableNativeAlarmSource record and fold the sorted, null-when-empty NativeAlarmSources collection into HashableConfiguration in its alphabetical slot. WhenWritingNull keeps every native-source-free config byte-identical to its pre-migration hash (pinned literal test proves it), so the migration is surgical: only native-source-bearing instances flip stale once after upgrade; redeploy clears it — deliberate. Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -18,6 +18,16 @@ namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening;
|
||||
/// by <c>RevisionHashServiceTests.HashableRecords_PropertiesDeclaredAlphabetically</c>.
|
||||
/// Collections are explicitly sorted by <c>CanonicalName</c> before hashing.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// MIGRATION (TemplateEngine-011): native alarm source bindings
|
||||
/// (<see cref="ResolvedNativeAlarmSource"/>) now participate in the hash so
|
||||
/// that binding edits flag the instance stale. The hashable property is
|
||||
/// populated NULL-WHEN-EMPTY and the serializer omits null properties
|
||||
/// (<see cref="JsonIgnoreCondition.WhenWritingNull"/>), so every
|
||||
/// native-source-free configuration keeps its byte-identical pre-migration
|
||||
/// hash — no global staleness storm. Native-source-bearing instances will
|
||||
/// report stale once after upgrade; redeploy clears it — deliberate.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class RevisionHashService
|
||||
{
|
||||
@@ -90,6 +100,19 @@ public class RevisionHashService
|
||||
ExecutionTimeoutSeconds = s.ExecutionTimeoutSeconds
|
||||
})
|
||||
.ToList(),
|
||||
NativeAlarmSources = configuration.NativeAlarmSources is { Count: > 0 }
|
||||
? configuration.NativeAlarmSources
|
||||
.OrderBy(n => n.CanonicalName, StringComparer.Ordinal)
|
||||
.Select(n => new HashableNativeAlarmSource
|
||||
{
|
||||
CanonicalName = n.CanonicalName,
|
||||
ConditionFilter = n.ConditionFilter,
|
||||
ConnectionName = n.ConnectionName,
|
||||
IsLocked = n.IsLocked,
|
||||
SourceReference = n.SourceReference
|
||||
})
|
||||
.ToList()
|
||||
: null,
|
||||
Connections = configuration.Connections is { Count: > 0 }
|
||||
? new SortedDictionary<string, HashableConnection>(
|
||||
configuration.Connections.ToDictionary(
|
||||
@@ -138,6 +161,13 @@ public class RevisionHashService
|
||||
/// </summary>
|
||||
public string InstanceUniqueName { get; init; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Collection of native alarm source bindings in the configuration.
|
||||
/// Null when the configuration carries no native alarm sources so that
|
||||
/// native-source-free configs hash identically to before this field was
|
||||
/// added (see the MIGRATION note on the class summary).
|
||||
/// </summary>
|
||||
public List<HashableNativeAlarmSource>? NativeAlarmSources { get; init; }
|
||||
/// <summary>
|
||||
/// Collection of scripts in the configuration.
|
||||
/// </summary>
|
||||
public List<HashableScript> Scripts { get; init; } = [];
|
||||
@@ -247,6 +277,30 @@ public class RevisionHashService
|
||||
public string Protocol { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed record HashableNativeAlarmSource
|
||||
{
|
||||
/// <summary>
|
||||
/// The path-qualified canonical name of the native alarm source binding.
|
||||
/// </summary>
|
||||
public string CanonicalName { get; init; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Optional condition filter; null = mirror all conditions under the source.
|
||||
/// </summary>
|
||||
public string? ConditionFilter { get; init; }
|
||||
/// <summary>
|
||||
/// The data connection name that owns the alarm feed.
|
||||
/// </summary>
|
||||
public string ConnectionName { get; init; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Whether the native alarm source binding is locked.
|
||||
/// </summary>
|
||||
public bool IsLocked { get; init; }
|
||||
/// <summary>
|
||||
/// The source reference (OPC UA SourceNode/notifier nodeId, or MxAccess object/area).
|
||||
/// </summary>
|
||||
public string SourceReference { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed record HashableScript
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user