fix(mqtt): initial push of cached snapshots on (re)subscribe — the push-model forceRaise (05/STAB-18 shape b)
This commit is contained in:
@@ -89,6 +89,13 @@ public sealed class MqttSubscriptionManager
|
||||
/// <summary>The broker refused, or never answered, the SUBSCRIBE for this reference's topic.</summary>
|
||||
private const uint StatusBadCommunicationError = 0x80050000u;
|
||||
|
||||
/// <summary>
|
||||
/// No value has ever been observed for this reference — <see cref="LastValueCache.Read"/>'s
|
||||
/// answer for a RawPath nothing has published under yet. The initial push skips it: pushing
|
||||
/// this would claim an observation the driver has not made.
|
||||
/// </summary>
|
||||
private const uint StatusBadWaitingForInitialData = 0x80320000u;
|
||||
|
||||
private readonly int _defaultQos;
|
||||
private readonly string _driverId;
|
||||
private readonly ILogger? _logger;
|
||||
@@ -356,6 +363,13 @@ public sealed class MqttSubscriptionManager
|
||||
_handleByRawPath[def.Name] = handle;
|
||||
}
|
||||
|
||||
// Initial push (05/STAB-18 shape b): every ref this driver has already observed is republished
|
||||
// onto the new handle before any broker traffic. A poll driver gets this free — PollGroupEngine
|
||||
// runs the first poll of a new subscription with forceRaise: true — and CalculationDriver does
|
||||
// it explicitly; MQTT did not, so a node reseeded BadWaitingForInitialData by an address-space
|
||||
// rebuild stayed there until the plant published again, which for a quiet topic is never.
|
||||
PushObservedSnapshots(defs);
|
||||
|
||||
// One SUBSCRIBE per distinct topic, and only for topics not already live: re-SUBSCRIBEing an
|
||||
// established filter is legal but pointless, and it re-triggers the retained seed.
|
||||
var pending = defs.Where(d => !_subscribedTopics.ContainsKey(d.Topic)).ToList();
|
||||
@@ -756,6 +770,12 @@ public sealed class MqttSubscriptionManager
|
||||
// timestamp — and only the notification is skipped. "Cached but not raised" is already a valid
|
||||
// state on this path (see Raise: an authored-but-unsubscribed tag does exactly that).
|
||||
//
|
||||
// What makes this suppression SAFE is the pair of edge behaviours around it (05/STAB-18):
|
||||
// PushObservedSnapshots re-delivers the cached snapshot on every (re)subscribe, and
|
||||
// OnConnectionLost stales every observed ref to BadNoCommunication — so the reconnect's
|
||||
// retained replay of an unchanged payload differs in STATUS and is not suppressed. Without
|
||||
// those, this comparison is what pinned a comms-lost tag Bad until the plant value changed.
|
||||
//
|
||||
// Sparkplug bypasses this entirely. SparkplugIngestor.cs updates Values and raises directly
|
||||
// (its own Values.Update()/Raise() calls, not through this Deliver method), so Sparkplug B
|
||||
// ingestion gets no unchanged-value suppression — deliberately out of scope here.
|
||||
@@ -788,6 +808,29 @@ public sealed class MqttSubscriptionManager
|
||||
return !Equals(previous, current);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Republishes the cached snapshot of every already-observed reference in
|
||||
/// <paramref name="defs"/> — the push-model equivalent of <c>PollGroupEngine</c>'s
|
||||
/// <c>forceRaise: true</c> on the first poll of a new subscription (05/STAB-18 shape b).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A never-observed reference is skipped: its cached status is
|
||||
/// <see cref="StatusBadWaitingForInitialData"/>, and republishing that would assert an
|
||||
/// observation this driver has not made. The cache is deliberately NOT re-<c>Update</c>d here —
|
||||
/// nothing new was observed, only re-delivered, so the stored timestamps stay honest and the
|
||||
/// steady-state suppression still compares against the real last observation.
|
||||
/// </remarks>
|
||||
/// <param name="defs">The definitions the new handle covers.</param>
|
||||
private void PushObservedSnapshots(IReadOnlyList<MqttTagDefinition> defs)
|
||||
{
|
||||
for (var i = 0; i < defs.Count; i++)
|
||||
{
|
||||
var snapshot = Values.Read(defs[i].Name);
|
||||
if (snapshot.StatusCode == StatusBadWaitingForInitialData) continue;
|
||||
Raise(defs[i].Name, snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises <see cref="OnDataChange"/> for a reference, if a live subscription covers it.
|
||||
/// A throwing subscriber is contained so it cannot starve the rest of the fan-out — or, worse,
|
||||
|
||||
Reference in New Issue
Block a user