fix(mqtt): session-rebind subscriptions, re-init teardown, timeout clamp
Findings from the per-module review of Driver.Mqtt at b990adae
(code-reviews/Driver.Mqtt/findings.md records all ten; these are the fixed
ones).
Driver.Mqtt-001 (High) — MqttSubscriptionManager._subscribedTopics is a claim
about the broker session it established filters on, and AttachTo never cleared
it, but the manager OUTLIVES a session rebuild (AdoptOptions keeps it whenever
the ingest identity is unchanged). After a driver-host reconnect
(ShutdownAsync + InitializeAsync, the #527 watchdog's retry) or a
ReinitializeAsync that only changed the endpoint/credentials, the host's
re-subscribe resolved an empty pending set, sent no SUBSCRIBE at all, and left
the driver Connected/Healthy on a session carrying ZERO subscriptions — no
error, no Bad status, values frozen until the new session happened to drop.
AttachTo now forgets them, mirroring SparkplugIngestor.AttachTo's reset.
Driver.Mqtt-002 (High) — InitializeCoreAsync published _connection and
_primaryHost over whatever the driver already held, so a second initialize
stranded a connected, self-supervising broker session (plus a second Sparkplug
primary-host claim) that no later dispose could reach. Reachable without any
teardown of ours via DriverInstanceActor.AdoptConfigDuringInit. Now tears down
first, under the gate both callers already hold.
Driver.Mqtt-003 (Medium) — connectTimeoutSeconds is [Range]-annotated but
unvalidated on the runtime path; 0 makes every deadline fire immediately and a
negative value throws ArgumentOutOfRangeException out of ShutdownAsync and
DisposeAsync. Normalized once, at the seam every options instance passes.
Also: ShutdownAsync keeps the live last-message instant (-004), three usings
left behind by #512 removed (-005), stale remarks corrected (-009, -010), and
docs/drivers/Mqtt.md now distinguishes the two kinds of reconnect.
Tests 708 -> 713. All three regression tests were confirmed to fail against
the un-fixed code, with two controls that pass in either state.
This commit is contained in:
@@ -183,17 +183,35 @@ public sealed class MqttSubscriptionManager
|
||||
/// transport.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The manager does not own the connection's lifetime and never detaches — the driver disposes
|
||||
/// both together. Call once per connection, and <b>before</b> the first
|
||||
/// <see cref="SubscribeAsync"/>: a subscribe issued with no transport attached records its
|
||||
/// intent (and warns) but sends nothing to a broker, and would only be established if the
|
||||
/// connection later dropped and recovered.
|
||||
/// <para>
|
||||
/// The manager does not own the connection's lifetime and never detaches — the driver disposes
|
||||
/// both together. Call once per connection, and <b>before</b> the first
|
||||
/// <see cref="SubscribeAsync"/>: a subscribe issued with no transport attached records its
|
||||
/// intent (and warns) but sends nothing to a broker, and would only be established if the
|
||||
/// connection later dropped and recovered.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Binding to a session forgets the previous session's established filters</b> — the same
|
||||
/// reset <see cref="Sparkplug.SparkplugIngestor.AttachTo"/> performs, and for the same reason:
|
||||
/// this manager <i>outlives</i> a session rebuild. <c>MqttDriver.AdoptOptions</c> only
|
||||
/// replaces it when the ingest identity changed, so a driver-host reconnect (ShutdownAsync +
|
||||
/// InitializeAsync, #527's watchdog path) and a <c>ReinitializeAsync</c> that changes only the
|
||||
/// endpoint/credentials both re-attach <i>this</i> instance to a brand-new broker session.
|
||||
/// <c>_subscribedTopics</c> is a claim about the session that has just gone: carried across,
|
||||
/// the host's post-reconnect re-subscribe resolves an empty pending set, issues no SUBSCRIBE
|
||||
/// at all, and leaves a driver that reports Connected/Healthy and receives nothing until the
|
||||
/// <i>new</i> session happens to drop — the silent-death outcome every other path in this
|
||||
/// type is written to prevent.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="connection">The connection to observe.</param>
|
||||
public void AttachTo(MqttConnection connection)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connection);
|
||||
|
||||
// FIRST, before delivery is wired: a new session holds none of the old one's subscriptions.
|
||||
_subscribedTopics.Clear();
|
||||
|
||||
_transport = connection;
|
||||
connection.MessageReceived += HandleMessage;
|
||||
connection.Reconnected += OnReconnectedAsync;
|
||||
|
||||
Reference in New Issue
Block a user