fix(mqtt): review minors - QoS 0 is not answer evidence, caller cancels stay cancels, rebirth wait cap, bound/trie pins (R06 G2 MQTT review minors 2 3 4 6 7 8)
2: only a PUBACKed (QoS >= 1) publish advances LastSuccessfulRead; a QoS 0 Sparkplug command completing means only that the bytes left the local socket. 3: a no-verdict SUBSCRIBE caused by the caller's own cancellation now throws OperationCanceledException (refs still Bad + pending), so the breaker no longer counts a pipeline timeout as a failure. 4: the rebirth wait is the window capped at 15 s, below the AdminUI 20 s per-call timeout. 6: a 10-topic test pins the 8-in-flight publish bound. 7: partial-level wildcards and $share filters are pinned never to confirm. 8: the intended init-vs-shutdown supersede is commented at the re-check. Refs #744 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -149,6 +149,13 @@ internal sealed class MqttBrowseSession : IRebirthCapableBrowseSession
|
||||
/// </summary>
|
||||
private static readonly UTF8Encoding Utf8Strict = new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
|
||||
|
||||
/// <summary>
|
||||
/// Ceiling on the rebirth request's wait, below the AdminUI's 20 s <c>BrowserSessionService.PerCallTimeout</c>
|
||||
/// (review minor 4): a configured window above it could never take effect there, and a rebirth aimed at an
|
||||
/// offline edge node must not hold the operator's button for the whole window.
|
||||
/// </summary>
|
||||
internal static readonly TimeSpan MaxRebirthWait = TimeSpan.FromSeconds(15);
|
||||
|
||||
/// <summary>The quiet spell that ends a rebirth request's wait once its births have started arriving.</summary>
|
||||
private static readonly TimeSpan RebirthQuietInterval = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
@@ -225,6 +232,9 @@ internal sealed class MqttBrowseSession : IRebirthCapableBrowseSession
|
||||
/// <summary>The Sparkplug birth-observation window this session was opened with (R06:M-4).</summary>
|
||||
internal TimeSpan BirthWindow { get; }
|
||||
|
||||
/// <summary>How long a rebirth request waits for its births: the window, capped at <see cref="MaxRebirthWait"/>.</summary>
|
||||
internal TimeSpan RebirthWait => BirthWindow < MaxRebirthWait ? BirthWindow : MaxRebirthWait;
|
||||
|
||||
/// <summary>How many NBIRTH/DBIRTH messages this session has observed — the settle loop's signal.</summary>
|
||||
internal long BirthCount => Interlocked.Read(ref _birthCount);
|
||||
|
||||
@@ -987,7 +997,7 @@ internal sealed class MqttBrowseSession : IRebirthCapableBrowseSession
|
||||
try
|
||||
{
|
||||
await SettleBirthsAsync(
|
||||
() => BirthCount, baseline, awaitFirstBirth: true, RebirthQuietInterval, BirthWindow, cancellationToken)
|
||||
() => BirthCount, baseline, awaitFirstBirth: true, RebirthQuietInterval, RebirthWait, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
|
||||
@@ -806,9 +806,11 @@ public sealed class MqttDriver
|
||||
.ConfigureAwait(false);
|
||||
results[index] = new WriteResult(0u);
|
||||
|
||||
// A PUBACK (or a sent QoS 0 command) is the broker answering — evidence the session works,
|
||||
// so it feeds LastSuccessfulRead alongside inbound messages.
|
||||
Interlocked.Exchange(ref _lastPublishUtcTicks, DateTime.UtcNow.Ticks);
|
||||
// A PUBACK is the broker answering — evidence the session works, so it feeds
|
||||
// LastSuccessfulRead alongside inbound messages. A completed QoS 0 publish is NOT: it only
|
||||
// means the bytes reached the local socket buffer, and frequent Sparkplug commands would
|
||||
// otherwise keep the watchdog's clock fresh over a half-open session (review minor 2).
|
||||
if (encoded.Qos >= 1) Interlocked.Exchange(ref _lastPublishUtcTicks, DateTime.UtcNow.Ticks);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -1118,7 +1120,10 @@ public sealed class MqttDriver
|
||||
await StartPrimaryHostAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Re-checked after publishing: a shutdown whose gate wait expired between the check above and
|
||||
// here captured whatever it found and left these fields for us. We still hold the gate, so no
|
||||
// here captured whatever it found and left these fields for us. Intended consequence (review
|
||||
// minor 8): a perfectly healthy init that is just completing when a ShutdownAsync arrives also
|
||||
// releases itself and throws OperationCanceledException — the queued shutdown would tear it down
|
||||
// anyway, and the actor serialises init/shutdown, so only an abandoned init can meet this. We still hold the gate, so no
|
||||
// newer initialize can have published anything — everything in them is ours to release.
|
||||
if (!IsCurrentLifecycle(generation))
|
||||
{
|
||||
|
||||
@@ -454,6 +454,10 @@ public sealed class MqttSubscriptionManager
|
||||
|
||||
if (outcome.TransportFailure is { } failure)
|
||||
{
|
||||
// The refs are already Bad and pending either way. But a cancel of the CALLER's token (a pipeline
|
||||
// timeout, host shutdown) is a cancellation, not a broker fault: surfacing it as
|
||||
// InvalidOperationException made the resilience breaker count it as a failure (review minor 3).
|
||||
var callerCancelled = cancellationToken.IsCancellationRequested;
|
||||
// No verdict at all (transport throw / SUBSCRIBE deadline): THROW, so the caller holds no handle
|
||||
// and DriverInstanceActor's health-tick reconcile re-subscribes. Returning a handle here was the
|
||||
// silent-death case — the actor thought the subscription live and never retried. The refs already
|
||||
@@ -464,6 +468,15 @@ public sealed class MqttSubscriptionManager
|
||||
_handleByRawPath.TryRemove(new KeyValuePair<string, ISubscriptionHandle>(def.Name, handle));
|
||||
}
|
||||
|
||||
if (callerCancelled)
|
||||
{
|
||||
throw new OperationCanceledException(
|
||||
$"MQTT driver '{_driverId}': SUBSCRIBE for {pending.Count} tag(s) was cancelled by the caller; "
|
||||
+ "the topics stay pending and will be retried.",
|
||||
failure,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"MQTT driver '{_driverId}': SUBSCRIBE for {pending.Count} tag(s) got no broker verdict "
|
||||
+ $"({failure.GetType().Name}); the subscription will be retried.",
|
||||
|
||||
Reference in New Issue
Block a user