fix(historian-gateway): historize under the historian name, not the mux ref, when HistorianTagname overrides (FU-3)
The continuous-historization recorder conflated two identifiers into one string: the dependency mux fans DependencyValueChanged keyed by the driver FullName (the mux ref), but a value must be historized under the resolved historian name (HistorianTagname override, else FullName). In the common no-override case the two are equal, so it worked; with an override they diverge and the recorder registered mux interest under a key the mux never fans — that tag's values were never captured (and, had they been, would have been written under the mux ref). Carry BOTH identifiers through the seam: a new HistorizedTagRef(MuxRef, HistorianName) record on IHistorizedTagSubscriptionSink. The applier resolves MuxRef = FullName and HistorianName = override-or-FullName. The recorder now keeps a muxRef->historianName map: it registers/filters mux interest by MuxRef but writes the outbox entry (and drains) under HistorianName. The convergence handler re-registers the mux only when the registered key-set changes, so an override-only rename (same FullName) updates the write target without mux churn. Tests: a divergent-override recorder test (interest by mux ref, value written under the override name, never the mux ref) + an override-rename no-churn test; the applier feed tests now assert the full (mux ref, historian name) pairs. Runtime 348/0, OpcUaServer 327/0; 0 warnings. Closes FU-3. Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
This commit is contained in:
@@ -309,37 +309,38 @@ public sealed class AddressSpaceApplier
|
||||
{
|
||||
try
|
||||
{
|
||||
List<string>? added = null;
|
||||
List<string>? removed = null;
|
||||
List<HistorizedTagRef>? added = null;
|
||||
List<HistorizedTagRef>? removed = null;
|
||||
|
||||
// Added historized value variables → new interest.
|
||||
foreach (var tag in plan.AddedEquipmentTags)
|
||||
{
|
||||
if (HistorizedRef(tag) is { } r) (added ??= new List<string>()).Add(r);
|
||||
if (HistorizedRef(tag) is { } r) (added ??= new List<HistorizedTagRef>()).Add(r);
|
||||
}
|
||||
|
||||
// Removed historized value variables → drop interest.
|
||||
foreach (var tag in plan.RemovedEquipmentTags)
|
||||
{
|
||||
if (HistorizedRef(tag) is { } r) (removed ??= new List<string>()).Add(r);
|
||||
if (HistorizedRef(tag) is { } r) (removed ??= new List<HistorizedTagRef>()).Add(r);
|
||||
}
|
||||
|
||||
// Changed tags: the historized ref may have flipped on/off or been renamed (override/FullName
|
||||
// change). Compare previous-vs-current resolved refs — an unchanged ref is a no-op.
|
||||
// change). Compare previous-vs-current resolved ref PAIRS (record equality compares both the
|
||||
// mux ref and the historian name) — an unchanged pair is a no-op.
|
||||
foreach (var d in plan.ChangedEquipmentTags)
|
||||
{
|
||||
var prev = HistorizedRef(d.Previous);
|
||||
var cur = HistorizedRef(d.Current);
|
||||
if (string.Equals(prev, cur, StringComparison.Ordinal)) continue;
|
||||
if (prev is not null) (removed ??= new List<string>()).Add(prev);
|
||||
if (cur is not null) (added ??= new List<string>()).Add(cur);
|
||||
if (prev == cur) continue;
|
||||
if (prev is not null) (removed ??= new List<HistorizedTagRef>()).Add(prev);
|
||||
if (cur is not null) (added ??= new List<HistorizedTagRef>()).Add(cur);
|
||||
}
|
||||
|
||||
if (added is null && removed is null) return;
|
||||
|
||||
_historizedSubscriptions.UpdateHistorizedRefs(
|
||||
added ?? (IReadOnlyList<string>)Array.Empty<string>(),
|
||||
removed ?? (IReadOnlyList<string>)Array.Empty<string>());
|
||||
added ?? (IReadOnlyList<HistorizedTagRef>)Array.Empty<HistorizedTagRef>(),
|
||||
removed ?? (IReadOnlyList<HistorizedTagRef>)Array.Empty<HistorizedTagRef>());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -350,16 +351,21 @@ public sealed class AddressSpaceApplier
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the historized tag ref for <paramref name="tag"/> EXACTLY as the provisioning hook /
|
||||
/// materialiser do: a non-alarm historized value variable's <c>HistorianTagname</c> override,
|
||||
/// else its driver-side <c>FullName</c>. Returns <c>null</c> when the tag is not a historized
|
||||
/// Resolve the historized tag ref for <paramref name="tag"/> as a
|
||||
/// <see cref="HistorizedTagRef"/> carrying BOTH identifiers the recorder needs: the
|
||||
/// <c>MuxRef</c> = the driver-side <c>FullName</c> the dependency mux fans values by, and the
|
||||
/// <c>HistorianName</c> = the value the EnsureTags hook / materialiser write under (a non-alarm
|
||||
/// historized value variable's <c>HistorianTagname</c> override, else its <c>FullName</c>). The
|
||||
/// two diverge ONLY when an override is set. Returns <c>null</c> when the tag is not a historized
|
||||
/// value variable (not historized, or a native-alarm condition node).
|
||||
/// </summary>
|
||||
/// <param name="tag">The equipment tag to resolve a historized ref for.</param>
|
||||
/// <returns>The resolved historian ref, or <c>null</c> when the tag is not a historized value variable.</returns>
|
||||
private static string? HistorizedRef(EquipmentTagPlan tag) =>
|
||||
/// <returns>The resolved historized ref pair, or <c>null</c> when the tag is not a historized value variable.</returns>
|
||||
private static HistorizedTagRef? HistorizedRef(EquipmentTagPlan tag) =>
|
||||
tag.IsHistorized && tag.Alarm is null
|
||||
? (string.IsNullOrWhiteSpace(tag.HistorianTagname) ? tag.FullName : tag.HistorianTagname)
|
||||
? new HistorizedTagRef(
|
||||
tag.FullName,
|
||||
string.IsNullOrWhiteSpace(tag.HistorianTagname) ? tag.FullName : tag.HistorianTagname)
|
||||
: null;
|
||||
|
||||
private void SafeRebuild()
|
||||
|
||||
Reference in New Issue
Block a user