From 52c8f00ef866a832a6983b7e5e2b31a6f3e129e9 Mon Sep 17 00:00:00 2001 From: Suhas Holla Karkada Chandrashekar <51073384+suhashollakc@users.noreply.github.com> Date: Sun, 21 Jun 2026 10:19:45 -0400 Subject: [PATCH] Fix keep alive ping send timeout disconnect handling (#2253) Fix keep alive ping send timeout disconnect --- .../Clients/MqttClient/MqttClient_Tests.cs | 114 +++++++++++++++++- Source/MQTTnet/MqttClient.cs | 9 +- 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Tests.cs b/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Tests.cs index 4b478427..a14c5afa 100644 --- a/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Tests.cs +++ b/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Tests.cs @@ -5,8 +5,12 @@ using System.Buffers; using System.Diagnostics; using System.Globalization; +using System.Net; using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; using System.Text; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics.Logger; using MQTTnet.Exceptions; using MQTTnet.Formatter; using MQTTnet.Internal; @@ -454,6 +458,34 @@ public sealed class MqttClient_Tests : BaseTestClass } } + [TestMethod] + public async Task KeepAlive_Ping_Send_Timeout_Raises_Disconnected() + { + var adapterFactory = new KeepAliveTimeoutAdapterFactory(); + var disconnected = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + using var client = new MqttClientFactory().CreateMqttClient(adapterFactory); + client.DisconnectedAsync += eventArgs => + { + disconnected.TrySetResult(eventArgs); + return CompletedTask.Instance; + }; + + var options = new MqttClientOptionsBuilder() + .WithTcpServer("localhost") + .WithKeepAlivePeriod(TimeSpan.FromSeconds(1)) + .Build(); + + await client.ConnectAsync(options); + + await adapterFactory.Adapter.PingRequestReceived.Task.WaitAsync(TimeSpan.FromSeconds(3)); + var disconnectedEventArgs = await disconnected.Task.WaitAsync(TimeSpan.FromSeconds(3)); + + Assert.IsTrue(disconnectedEventArgs.ClientWasConnected); + Assert.IsInstanceOfType(disconnectedEventArgs.Exception); + Assert.IsFalse(client.IsConnected); + } + [TestMethod] public async Task Publish_QoS_1_In_ApplicationMessageReceiveHandler() { @@ -975,4 +1007,84 @@ public sealed class MqttClient_Tests : BaseTestClass Assert.IsTrue(publisher.IsConnected); Assert.AreEqual(Threads * MessagesPerThread, receivedCount); } -} \ No newline at end of file + + sealed class KeepAliveTimeoutAdapterFactory : IMqttClientAdapterFactory + { + public KeepAliveTimeoutAdapter Adapter { get; private set; } + + public IMqttChannelAdapter CreateClientAdapter(MqttClientOptions options, MqttPacketInspector packetInspector, IMqttNetLogger logger) + { + Adapter = new KeepAliveTimeoutAdapter(options.ProtocolVersion, options.WriterBufferSize, options.WriterBufferSizeMax); + return Adapter; + } + } + + sealed class KeepAliveTimeoutAdapter : IMqttChannelAdapter + { + int _connAckReturned; + + public KeepAliveTimeoutAdapter(MqttProtocolVersion protocolVersion, int writerBufferSize, int writerBufferSizeMax) + { + PacketFormatterAdapter = new MqttPacketFormatterAdapter(protocolVersion, new MqttBufferWriter(writerBufferSize, writerBufferSizeMax)); + } + + public long BytesReceived { get; } + + public long BytesSent { get; } + + public X509Certificate2 ClientCertificate { get; } + + public EndPoint RemoteEndPoint { get; } = new DnsEndPoint("localhost", 1883); + + public EndPoint LocalEndPoint { get; } = new DnsEndPoint("localhost", 0); + + public bool IsSecureConnection { get; } + + public MqttPacketFormatterAdapter PacketFormatterAdapter { get; } + + public TaskCompletionSource PingRequestReceived { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public Task ConnectAsync(CancellationToken cancellationToken) + { + return CompletedTask.Instance; + } + + public Task DisconnectAsync(CancellationToken cancellationToken) + { + return CompletedTask.Instance; + } + + public void Dispose() + { + } + + public async Task ReceivePacketAsync(CancellationToken cancellationToken) + { + if (Interlocked.Exchange(ref _connAckReturned, 1) == 0) + { + return new MqttConnAckPacket + { + ReturnCode = MqttConnectReturnCode.ConnectionAccepted + }; + } + + await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + return null; + } + + public void ResetStatistics() + { + } + + public Task SendPacketAsync(MqttPacket packet, CancellationToken cancellationToken) + { + if (packet is MqttPingReqPacket) + { + PingRequestReceived.TrySetResult(); + return Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + } + + return CompletedTask.Instance; + } + } +} diff --git a/Source/MQTTnet/MqttClient.cs b/Source/MQTTnet/MqttClient.cs index a9231ddb..b7a81790 100644 --- a/Source/MQTTnet/MqttClient.cs +++ b/Source/MQTTnet/MqttClient.cs @@ -1064,7 +1064,12 @@ public sealed class MqttClient : Disposable, IMqttClient switch (exception) { case OperationCanceledException: - return; + if (cancellationToken.IsCancellationRequested) + { + return; + } + + break; case MqttCommunicationException: _logger.Warning(exception, "Communication error while sending/receiving keep alive packets"); break; @@ -1080,4 +1085,4 @@ public sealed class MqttClient : Disposable, IMqttClient _logger.Verbose("Stopped sending keep alive packets"); } } -} \ No newline at end of file +}