Fail all waiting tasks when the client connection gets closed (#1563)

This commit is contained in:
Christian
2022-11-06 10:50:04 +01:00
committed by GitHub
parent 6e1ec266a7
commit e6c1ab4e4c
10 changed files with 201 additions and 62 deletions
+1
View File
@@ -1,4 +1,5 @@
* [Core] Fixed several dead locks by implementing a new async signal (#1552) * [Core] Fixed several dead locks by implementing a new async signal (#1552)
* [Client] All waiting tasks will now fail when the client connection gets closed. This fixes several freezes (#1561).
* [Server] Fixed duplicated invocation of the event _ClientAcknowledgedPublishPacketAsync_ for QoS level 2 (#1550) * [Server] Fixed duplicated invocation of the event _ClientAcknowledgedPublishPacketAsync_ for QoS level 2 (#1550)
* [Server] Fixed issue in upgrading and downgrading of QoS levels for subscriptions and retained messages (#1560) * [Server] Fixed issue in upgrading and downgrading of QoS levels for subscriptions and retained messages (#1560)
* [Server] Fixed memory leak when old sessions are discarded (#1553) * [Server] Fixed memory leak when old sessions are discarded (#1553)
@@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Client;
using MQTTnet.Protocol;
namespace MQTTnet.Tests.Server
{
[TestClass]
public sealed class Publishing_Tests : BaseTestClass
{
[TestMethod]
[ExpectedException(typeof(MqttClientDisconnectedException))]
public async Task Disconnect_While_Publishing()
{
using (var testEnvironment = CreateTestEnvironment())
{
var server = await testEnvironment.StartServer();
// The client will be disconnect directly after subscribing!
server.InterceptingPublishAsync += ev => server.DisconnectClientAsync(ev.ClientId, MqttDisconnectReasonCode.NormalDisconnection);
var client = await testEnvironment.ConnectClient();
await client.PublishStringAsync("test", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce);
}
}
}
}
@@ -46,6 +46,22 @@ namespace MQTTnet.Tests.Server
} }
} }
[TestMethod]
[ExpectedException(typeof(MqttClientDisconnectedException))]
public async Task Disconnect_While_Subscribing()
{
using (var testEnvironment = CreateTestEnvironment())
{
var server = await testEnvironment.StartServer();
// The client will be disconnect directly after subscribing!
server.ClientSubscribedTopicAsync += ev => server.DisconnectClientAsync(ev.ClientId, MqttDisconnectReasonCode.NormalDisconnection);
var client = await testEnvironment.ConnectClient();
await client.SubscribeAsync("#");
}
}
[TestMethod] [TestMethod]
public async Task Enqueue_Message_After_Subscription() public async Task Enqueue_Message_After_Subscription()
{ {
@@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MQTTnet.Client;
using MQTTnet.Protocol;
namespace MQTTnet.Tests.Server
{
[TestClass]
public sealed class Unsubscribe_Tests : BaseTestClass
{
[TestMethod]
[ExpectedException(typeof(MqttClientDisconnectedException))]
public async Task Disconnect_While_Unsubscribing()
{
using (var testEnvironment = CreateTestEnvironment())
{
var server = await testEnvironment.StartServer();
// The client will be disconnect directly after subscribing!
server.ClientUnsubscribedTopicAsync += ev => server.DisconnectClientAsync(ev.ClientId, MqttDisconnectReasonCode.NormalDisconnection);
var client = await testEnvironment.ConnectClient();
await client.SubscribeAsync("#");
await client.UnsubscribeAsync("#");
}
}
}
}
@@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using MQTTnet.Exceptions;
namespace MQTTnet.Client
{
public sealed class MqttClientDisconnectedException : MqttCommunicationException
{
public MqttClientDisconnectedException(Exception innerException) : base("The MQTT client is disconnected.", innerException)
{
}
}
}
@@ -3,14 +3,15 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Collections.Generic; using System.Collections.Generic;
using MQTTnet.Exceptions;
using MQTTnet.Packets; using MQTTnet.Packets;
using MQTTnet.Protocol; using MQTTnet.Protocol;
namespace MQTTnet.Exceptions namespace MQTTnet.Client
{ {
public class MqttUnexpectedDisconnectReceivedException : MqttCommunicationException public sealed class MqttClientUnexpectedDisconnectReceivedException : MqttCommunicationException
{ {
public MqttUnexpectedDisconnectReceivedException(MqttDisconnectPacket disconnectPacket) public MqttClientUnexpectedDisconnectReceivedException(MqttDisconnectPacket disconnectPacket)
: base($"Unexpected DISCONNECT (Reason code={disconnectPacket.ReasonCode}) received.") : base($"Unexpected DISCONNECT (Reason code={disconnectPacket.ReasonCode}) received.")
{ {
ReasonCode = disconnectPacket.ReasonCode; ReasonCode = disconnectPacket.ReasonCode;
+10 -8
View File
@@ -32,25 +32,21 @@ namespace MQTTnet.Client
readonly object _disconnectLock = new object(); readonly object _disconnectLock = new object();
readonly AsyncEvent<InspectMqttPacketEventArgs> _inspectPacketEvent = new AsyncEvent<InspectMqttPacketEventArgs>(); readonly AsyncEvent<InspectMqttPacketEventArgs> _inspectPacketEvent = new AsyncEvent<InspectMqttPacketEventArgs>();
readonly MqttNetSourceLogger _logger; readonly MqttNetSourceLogger _logger;
readonly MqttPacketDispatcher _packetDispatcher = new MqttPacketDispatcher();
readonly MqttPacketFactories _packetFactories = new MqttPacketFactories(); readonly MqttPacketFactories _packetFactories = new MqttPacketFactories();
readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider(); readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider();
readonly IMqttNetLogger _rootLogger; readonly IMqttNetLogger _rootLogger;
IMqttChannelAdapter _adapter; IMqttChannelAdapter _adapter;
CancellationTokenSource _backgroundCancellationTokenSource; CancellationTokenSource _backgroundCancellationTokenSource;
bool _cleanDisconnectInitiated; bool _cleanDisconnectInitiated;
volatile int _connectionStatus; volatile int _connectionStatus;
MqttClientDisconnectReason _disconnectReason; MqttClientDisconnectReason _disconnectReason;
string _disconnectReasonString; string _disconnectReasonString;
Task _keepAlivePacketsSenderTask; Task _keepAlivePacketsSenderTask;
DateTime _lastPacketSentTimestamp; DateTime _lastPacketSentTimestamp;
MqttPacketDispatcher _packetDispatcher;
Task _packetReceiverTask; Task _packetReceiverTask;
AsyncQueue<MqttPublishPacket> _publishPacketReceiverQueue; AsyncQueue<MqttPublishPacket> _publishPacketReceiverQueue;
Task _publishPacketReceiverTask; Task _publishPacketReceiverTask;
@@ -128,7 +124,7 @@ namespace MQTTnet.Client
} }
_packetIdentifierProvider.Reset(); _packetIdentifierProvider.Reset();
_packetDispatcher.CancelAll(); _packetDispatcher = new MqttPacketDispatcher();
_backgroundCancellationTokenSource = new CancellationTokenSource(); _backgroundCancellationTokenSource = new CancellationTokenSource();
var backgroundCancellationToken = _backgroundCancellationTokenSource.Token; var backgroundCancellationToken = _backgroundCancellationTokenSource.Token;
@@ -414,6 +410,10 @@ namespace MQTTnet.Client
_publishPacketReceiverQueue = null; _publishPacketReceiverQueue = null;
_adapter?.Dispose(); _adapter?.Dispose();
_adapter = null;
_packetDispatcher?.Dispose();
_packetDispatcher = null;
} }
} }
@@ -447,6 +447,8 @@ namespace MQTTnet.Client
try try
{ {
_packetDispatcher.Dispose(new MqttClientDisconnectedException(exception));
var receiverTask = WaitForTaskAsync(_packetReceiverTask, sender); var receiverTask = WaitForTaskAsync(_packetReceiverTask, sender);
var publishPacketReceiverTask = WaitForTaskAsync(_publishPacketReceiverTask, sender); var publishPacketReceiverTask = WaitForTaskAsync(_publishPacketReceiverTask, sender);
var keepAliveTask = WaitForTaskAsync(_keepAlivePacketsSenderTask, sender); var keepAliveTask = WaitForTaskAsync(_keepAlivePacketsSenderTask, sender);
@@ -558,7 +560,7 @@ namespace MQTTnet.Client
_disconnectReasonString = disconnectPacket.ReasonString; _disconnectReasonString = disconnectPacket.ReasonString;
// Also dispatch disconnect to waiting threads to generate a proper exception. // Also dispatch disconnect to waiting threads to generate a proper exception.
_packetDispatcher.FailAll(new MqttUnexpectedDisconnectReceivedException(disconnectPacket)); _packetDispatcher.Dispose(new MqttClientUnexpectedDisconnectReceivedException(disconnectPacket));
return DisconnectInternalAsync(_packetReceiverTask, null, null); return DisconnectInternalAsync(_packetReceiverTask, null, null);
} }
@@ -776,7 +778,7 @@ namespace MQTTnet.Client
} }
else else
{ {
_logger.Error(exception, "Error while receiving packets."); _logger.Error(exception, $"Error while processing received packet ({packet.GetType().Name}).");
} }
_packetDispatcher.FailAll(exception); _packetDispatcher.FailAll(exception);
@@ -8,12 +8,8 @@ namespace MQTTnet.Exceptions
{ {
public class MqttCommunicationException : Exception public class MqttCommunicationException : Exception
{ {
protected MqttCommunicationException()
{
}
public MqttCommunicationException(Exception innerException) public MqttCommunicationException(Exception innerException)
: base(innerException.Message, innerException) : base(innerException?.Message ?? "MQTT communication failed.", innerException)
{ {
} }
@@ -2,6 +2,7 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cconnecting/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cconnecting/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cdiagnostics/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cdiagnostics/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cdisconnecting/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cdisconnecting/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cexceptions/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cextendedauthenticationexchange/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cextendedauthenticationexchange/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Coptions/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Coptions/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cpublishing/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=client_005Cpublishing/@EntryIndexedValue">True</s:Boolean>
@@ -2,48 +2,103 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using MQTTnet.Packets;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using MQTTnet.Packets;
namespace MQTTnet.PacketDispatcher namespace MQTTnet.PacketDispatcher
{ {
public sealed class MqttPacketDispatcher public sealed class MqttPacketDispatcher : IDisposable
{ {
readonly List<IMqttPacketAwaitable> _awaitables = new List<IMqttPacketAwaitable>(); readonly List<IMqttPacketAwaitable> _waiters = new List<IMqttPacketAwaitable>();
public void FailAll(Exception exception) bool _isDisposed;
public MqttPacketAwaitable<TResponsePacket> AddAwaitable<TResponsePacket>(ushort packetIdentifier) where TResponsePacket : MqttPacket
{ {
if (exception == null) throw new ArgumentNullException(nameof(exception)); var awaitable = new MqttPacketAwaitable<TResponsePacket>(packetIdentifier, this);
lock (_awaitables) lock (_waiters)
{ {
foreach (var awaitable in _awaitables) _waiters.Add(awaitable);
{
awaitable.Fail(exception);
}
_awaitables.Clear();
} }
return awaitable;
} }
public void CancelAll() public void CancelAll()
{ {
lock (_awaitables) lock (_waiters)
{ {
foreach (var awaitable in _awaitables) foreach (var awaitable in _waiters)
{ {
awaitable.Cancel(); awaitable.Cancel();
} }
_awaitables.Clear(); _waiters.Clear();
} }
} }
public void Dispose()
{
Dispose(new ObjectDisposedException(nameof(MqttPacketDispatcher)));
}
public void Dispose(Exception exception)
{
if (exception == null)
{
throw new ArgumentNullException(nameof(exception));
}
lock (_waiters)
{
FailAll(exception);
// Make sure that no task can start waiting after this instance is already disposed.
// This will prevent unexpected freezes.
_isDisposed = true;
}
}
public void FailAll(Exception exception)
{
if (exception == null)
{
throw new ArgumentNullException(nameof(exception));
}
lock (_waiters)
{
foreach (var awaitable in _waiters)
{
awaitable.Fail(exception);
}
_waiters.Clear();
}
}
public void RemoveAwaitable(IMqttPacketAwaitable awaitable)
{
if (awaitable == null)
{
throw new ArgumentNullException(nameof(awaitable));
}
lock (_waiters)
{
_waiters.Remove(awaitable);
}
}
public bool TryDispatch(MqttPacket packet) public bool TryDispatch(MqttPacket packet)
{ {
if (packet == null) throw new ArgumentNullException(nameof(packet)); if (packet == null)
{
throw new ArgumentNullException(nameof(packet));
}
ushort identifier = 0; ushort identifier = 0;
if (packet is MqttPacketWithIdentifier packetWithIdentifier) if (packet is MqttPacketWithIdentifier packetWithIdentifier)
{ {
@@ -51,13 +106,15 @@ namespace MQTTnet.PacketDispatcher
} }
var packetType = packet.GetType(); var packetType = packet.GetType();
var awaitables = new List<IMqttPacketAwaitable>(); var waiters = new List<IMqttPacketAwaitable>();
lock (_awaitables) lock (_waiters)
{ {
for (var i = _awaitables.Count - 1; i >= 0; i--) ThrowIfDisposed();
for (var i = _waiters.Count - 1; i >= 0; i--)
{ {
var entry = _awaitables[i]; var entry = _waiters[i];
// Note: The PingRespPacket will also arrive here and has NO identifier but there // Note: The PingRespPacket will also arrive here and has NO identifier but there
// is code which waits for it. So the code must be able to deal with filters which // is code which waits for it. So the code must be able to deal with filters which
@@ -66,39 +123,25 @@ namespace MQTTnet.PacketDispatcher
{ {
continue; continue;
} }
awaitables.Add(entry); waiters.Add(entry);
_awaitables.RemoveAt(i); _waiters.RemoveAt(i);
} }
} }
foreach (var matchingEntry in awaitables) foreach (var matchingEntry in waiters)
{ {
matchingEntry.Complete(packet); matchingEntry.Complete(packet);
} }
return awaitables.Count > 0; return waiters.Count > 0;
}
public MqttPacketAwaitable<TResponsePacket> AddAwaitable<TResponsePacket>(ushort packetIdentifier) where TResponsePacket : MqttPacket
{
var awaitable = new MqttPacketAwaitable<TResponsePacket>(packetIdentifier, this);
lock (_awaitables)
{
_awaitables.Add(awaitable);
}
return awaitable;
} }
public void RemoveAwaitable(IMqttPacketAwaitable awaitable) void ThrowIfDisposed()
{ {
if (awaitable == null) throw new ArgumentNullException(nameof(awaitable)); if (_isDisposed)
lock (_awaitables)
{ {
_awaitables.Remove(awaitable); throw new ObjectDisposedException(nameof(MqttPacketDispatcher));
} }
} }
} }