feat(mqtt): wire will message delivery on abnormal disconnect

Add DeliverWill to MqttPacketHandlers — extracts and clears the will,
then delivers via ProcessInboundClientMsg. Call from CloseConnection
when client is MQTT. Clean DISCONNECT already clears the will (Task 3),
so DeliverWill is a no-op after DISCONNECT. 3 new tests: abnormal
close delivers will, DISCONNECT suppresses will, no-will is a no-op.
This commit is contained in:
Joseph Doherty
2026-03-01 16:10:53 -05:00
parent b465d095e3
commit 710f443eda
4 changed files with 96 additions and 1 deletions

View File

@@ -1330,6 +1330,10 @@ public sealed partial class ClientConnection
ClearPingTimer();
}
// Deliver MQTT will message on abnormal disconnect.
if (IsMqtt())
MqttPacketHandlers.DeliverWill(this);
// Close the underlying network connection.
try { _nc?.Close(); } catch { /* ignore */ }
_nc = null;

View File

@@ -238,6 +238,33 @@ internal static class MqttPacketHandlers
c.CloseConnection(ClosedState.ClientClosed);
}
// =========================================================================
// Will message delivery
// =========================================================================
/// <summary>
/// Delivers the will message if one is set and the connection was not cleanly
/// disconnected (i.e., DISCONNECT was not received, so will is still non-null).
/// Called from CloseConnection(). Mirrors Go <c>mqttHandleWill()</c>.
/// </summary>
public static void DeliverWill(ClientConnection c)
{
MqttWill? will;
lock (c)
{
if (c.Mqtt == null) return;
will = c.Mqtt.Will;
c.Mqtt.Will = null; // Prevent duplicate delivery.
}
if (will == null)
return;
// Deliver the will message via internal NATS routing.
var payload = will.Msg ?? [];
c.ProcessInboundClientMsg(payload);
}
// =========================================================================
// PUBLISH parsing + processing
// =========================================================================

View File

@@ -383,4 +383,68 @@ public sealed class MqttConnectTests
// Will should be cleared.
c.Mqtt.Will.ShouldBeNull();
}
// =========================================================================
// Will message delivery tests
// =========================================================================
[Fact]
public void DeliverWill_AbnormalClose_ShouldDeliverAndClearWill()
{
var c = CreateMqttClient();
// CONNECT with a will.
var connectBuf = BuildConnectPacket(
willTopic: "test/will",
willMessage: Encoding.UTF8.GetBytes("goodbye"));
var err = MqttParser.Parse(c, connectBuf, connectBuf.Length);
err.ShouldBeNull();
c.Mqtt!.Will.ShouldNotBeNull();
// Simulate abnormal close (not DISCONNECT) — will should be delivered.
MqttPacketHandlers.DeliverWill(c);
// Will should be cleared after delivery.
c.Mqtt.Will.ShouldBeNull();
}
[Fact]
public void DeliverWill_AfterDisconnect_ShouldNotDeliver()
{
var c = CreateMqttClient();
// CONNECT with a will.
var connectBuf = BuildConnectPacket(
willTopic: "test/will",
willMessage: Encoding.UTF8.GetBytes("goodbye"));
var err = MqttParser.Parse(c, connectBuf, connectBuf.Length);
err.ShouldBeNull();
// Clean DISCONNECT — clears the will.
var disconnectBuf = new byte[] { MqttPacket.Disconnect, 0x00 };
err = MqttParser.Parse(c, disconnectBuf, disconnectBuf.Length);
err.ShouldBeNull();
// Will was already cleared by DISCONNECT, so DeliverWill should be a no-op.
MqttPacketHandlers.DeliverWill(c);
c.Mqtt!.Will.ShouldBeNull();
}
[Fact]
public void DeliverWill_NoWill_ShouldBeNoOp()
{
var c = CreateMqttClient();
// CONNECT without a will.
var connectBuf = BuildConnectPacket();
var err = MqttParser.Parse(c, connectBuf, connectBuf.Length);
err.ShouldBeNull();
c.Mqtt!.Will.ShouldBeNull();
// DeliverWill with no will configured — should be a no-op.
MqttPacketHandlers.DeliverWill(c);
c.Mqtt.Will.ShouldBeNull();
}
}