fix(mtconnect): on a changed alarm set forget only the conditions that left (#641)

The driver cleared its whole "last told" record whenever the subscribed alarm
set changed, assuming the rebuild that accompanies an alarm-bearing delta had
reset every condition node. The server now carries each surviving native
condition's state (acknowledgement included) across the rebuild, so re-raising a
survivor would un-acknowledge it. Only entries that left the set are dropped; a
condition that comes back later is told again onto its fresh node.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 17:06:57 -04:00
parent 11f13f6326
commit 3fe6cbb905
3 changed files with 30 additions and 31 deletions
@@ -50,18 +50,13 @@ public sealed partial class MTConnectDriver : IAlarmSource
/// <summary>
/// ConditionId (RawPath) → what the server was last told. Guarded by <see cref="_alarmLock"/>. Survives a
/// re-subscribe with the SAME reference set (a reconnect) and lifecycle restarts — the server's condition
/// nodes outlive both, and re-raising there would un-acknowledge. Cleared WHOLE when the subscribed set
/// changes: only an alarm-bearing raw-tag delta changes it, that delta is a full address-space rebuild,
/// and a rebuild recreates every condition node inactive — so everything must be told again.
/// nodes outlive both, and re-raising there would un-acknowledge. When the subscribed set changes (an
/// alarm-bearing raw-tag delta, i.e. a full rebuild) only the entries that LEFT the set are dropped: the
/// server carries every surviving condition's state across the rebuild (Gitea #641), so a survivor needs
/// no re-tell, while a condition that comes back later was materialised fresh and is told again.
/// </summary>
private readonly Dictionary<string, MTConnectToldCondition> _deliveredConditions = new(StringComparer.Ordinal);
/// <summary>
/// The reference set of the last <see cref="SubscribeAlarmsAsync"/>, kept across an unsubscribe so a
/// release-then-resubscribe of an identical set is still recognised. Guarded by <see cref="_alarmLock"/>.
/// </summary>
private HashSet<string>? _lastAlarmReferences;
/// <summary>The CONDITION tracker for the current session; replaced alongside the observation index.</summary>
private MTConnectConditionTracker _conditions = new();
@@ -108,16 +103,15 @@ public sealed partial class MTConnectDriver : IAlarmSource
lock (_alarmLock)
{
// A CHANGED set means an alarm-bearing delta, which the server applies as a full rebuild: every
// condition node — including ones still in the set — was recreated inactive and has been told
// nothing. Re-tell all of them. The SAME set is a reconnect: keep the record, or every still-active
// alarm would be re-raised and lose its acknowledgement.
if (_lastAlarmReferences is null || !_lastAlarmReferences.SetEquals(references))
// Forget only the conditions that LEFT the set (Gitea #641). A changed set is an alarm-bearing delta,
// which the server applies as a full rebuild — but the server carries every surviving condition's
// state (acknowledgement included) across that rebuild, so re-raising a survivor would only
// un-acknowledge it. A condition that left and later comes back was materialised fresh and has no
// record, so it is told again. The SAME set (a reconnect) prunes nothing.
foreach (var conditionId in _deliveredConditions.Keys.Where(k => !references.Contains(k)).ToList())
{
_deliveredConditions.Clear();
_deliveredConditions.Remove(conditionId);
}
_lastAlarmReferences = references;
}
EmitConditionTransitions(touchedDataItemIds: null, resync: true);