From f74af45fba6ca1762d87eb166e2cf74bfbe58b04 Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sat, 1 Oct 2022 11:32:14 +0200 Subject: [PATCH 01/24] Add new samples. --- Samples/Client/Client_Publish_Samples.cs | 47 ++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/Samples/Client/Client_Publish_Samples.cs b/Samples/Client/Client_Publish_Samples.cs index ed0e367d..021c0613 100644 --- a/Samples/Client/Client_Publish_Samples.cs +++ b/Samples/Client/Client_Publish_Samples.cs @@ -40,6 +40,53 @@ public static class Client_Publish_Samples await mqttClient.PublishAsync(applicationMessage, CancellationToken.None); + await mqttClient.DisconnectAsync(); + + Console.WriteLine("MQTT application message is published."); + } + } + + public static async Task Publish_Multiple_Application_Messages() + { + /* + * This sample pushes multiple simple application message including a topic and a payload. + * + * See sample _Publish_Application_Message_ for more details. + */ + + var mqttFactory = new MqttFactory(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + var mqttClientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("broker.hivemq.com") + .Build(); + + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + var applicationMessage = new MqttApplicationMessageBuilder() + .WithTopic("samples/temperature/living_room") + .WithPayload("19.5") + .Build(); + + await mqttClient.PublishAsync(applicationMessage, CancellationToken.None); + + applicationMessage = new MqttApplicationMessageBuilder() + .WithTopic("samples/temperature/living_room") + .WithPayload("20.0") + .Build(); + + await mqttClient.PublishAsync(applicationMessage, CancellationToken.None); + + applicationMessage = new MqttApplicationMessageBuilder() + .WithTopic("samples/temperature/living_room") + .WithPayload("21.0") + .Build(); + + await mqttClient.PublishAsync(applicationMessage, CancellationToken.None); + + await mqttClient.DisconnectAsync(); + Console.WriteLine("MQTT application message is published."); } } From a3227a444a7ad68c60c8fa4245094dba18fe99fd Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sat, 1 Oct 2022 16:51:11 +0200 Subject: [PATCH 02/24] Reset release notes. --- .github/workflows/ReleaseNotes.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index 554b146f..e69de29b 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -1,7 +0,0 @@ -* [Core] MQTT Packets being sent over web socket transport are now setting the web socket frame boundaries correctly (#1499). -* [Core] Add support for attaching and detaching events from different threads. -* [Core] Fixed a deadlock in _AsyncLock_ implementation (#1520). -* [Client] Keep alive mechanism now uses the configured timeout value from the options (thanks to @Stannieman, #1495). -* [Client] The _PingAsync_ will fallback to the timeout specified in the client options when the cancellation token cannot be cancelled. -* [Server] A DISCONNECT packet is no longer sent to MQTT clients < 5.0.0 (thanks to @logicaloud, #1506). -* [Server] Improved "take over" process handling. From 431142471fc4513c379ecbcea20142c70d112f4c Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sun, 16 Oct 2022 11:12:08 +0200 Subject: [PATCH 03/24] Add samples. --- Samples/Client/Client_Subscribe_Samples.cs | 130 ++++++++++++++------- 1 file changed, 90 insertions(+), 40 deletions(-) diff --git a/Samples/Client/Client_Subscribe_Samples.cs b/Samples/Client/Client_Subscribe_Samples.cs index 740f9481..d4f9ff42 100644 --- a/Samples/Client/Client_Subscribe_Samples.cs +++ b/Samples/Client/Client_Subscribe_Samples.cs @@ -8,6 +8,7 @@ using MQTTnet.Client; using MQTTnet.Packets; +using MQTTnet.Protocol; using MQTTnet.Samples.Helpers; namespace MQTTnet.Samples.Client; @@ -24,9 +25,7 @@ public static class Client_Subscribe_Samples using (var mqttClient = mqttFactory.CreateMqttClient()) { - var mqttClientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("broker.hivemq.com") - .Build(); + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); // Setup message handling before connecting so that queued messages // are also handled properly. When there is no event handler attached all @@ -42,7 +41,11 @@ public static class Client_Subscribe_Samples await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder() - .WithTopicFilter(f => { f.WithTopic("mqttnet/samples/topic/2"); }) + .WithTopicFilter( + f => + { + f.WithTopic("mqttnet/samples/topic/2"); + }) .Build(); await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); @@ -54,35 +57,6 @@ public static class Client_Subscribe_Samples } } - public static async Task Subscribe_Topic() - { - /* - * This sample subscribes to a topic. - */ - - var mqttFactory = new MqttFactory(); - - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - var mqttClientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("broker.hivemq.com") - .Build(); - - await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - - var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder() - .WithTopicFilter(f => { f.WithTopic("mqttnet/samples/topic/1"); }) - .Build(); - - var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); - - Console.WriteLine("MQTT client subscribed to topic."); - - // The response contains additional data sent by the server after subscribing. - response.DumpToConsole(); - } - } - public static async Task Send_Responses() { /* @@ -100,22 +74,98 @@ public static class Client_Subscribe_Samples // Now respond to the broker with a reason code other than success. args.ReasonCode = MqttApplicationMessageReceivedReasonCode.ImplementationSpecificError; args.ResponseReasonString = "That did not work!"; - + // User properties require MQTT v5! args.ResponseUserProperties.Add(new MqttUserProperty("My", "Data")); - + // Now the broker will resend the message again. return Task.CompletedTask; }; - - var mqttClientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("broker.hivemq.com") - .Build(); + + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder() - .WithTopicFilter(f => { f.WithTopic("mqttnet/samples/topic/1"); }) + .WithTopicFilter( + f => + { + f.WithTopic("mqttnet/samples/topic/1"); + }) + .Build(); + + var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); + + Console.WriteLine("MQTT client subscribed to topic."); + + // The response contains additional data sent by the server after subscribing. + response.DumpToConsole(); + } + } + + public static async Task Subscribe_Multiple_Topics() + { + /* + * This sample subscribes to several topics in a single request. + */ + + var mqttFactory = new MqttFactory(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + // Create the subscribe options including several topics with different options. + // It is also possible to all of these topics using a dedicated call of _SubscribeAsync_ per topic. + var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder() + .WithTopicFilter( + f => + { + f.WithTopic("mqttnet/samples/topic/1"); + }) + .WithTopicFilter( + f => + { + f.WithTopic("mqttnet/samples/topic/2").WithNoLocal(); + }) + .WithTopicFilter( + f => + { + f.WithTopic("mqttnet/samples/topic/3").WithRetainHandling(MqttRetainHandling.SendAtSubscribe); + }) + .Build(); + + var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); + + Console.WriteLine("MQTT client subscribed to topics."); + + // The response contains additional data sent by the server after subscribing. + response.DumpToConsole(); + } + } + + public static async Task Subscribe_Topic() + { + /* + * This sample subscribes to a topic. + */ + + var mqttFactory = new MqttFactory(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder() + .WithTopicFilter( + f => + { + f.WithTopic("mqttnet/samples/topic/1"); + }) .Build(); var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); From 7823d8342e453dc448f62b05295908658821c062 Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sun, 23 Oct 2022 07:43:31 +0200 Subject: [PATCH 04/24] Update version. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ae88065..8f3fbb41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: [push, pull_request] env: - VERSION: "4.1.1.${{github.run_number}}" + VERSION: "4.1.2.${{github.run_number}}" jobs: build: From 36736914bcebcfe6851319e71a932a170bbff806 Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sun, 23 Oct 2022 07:51:19 +0200 Subject: [PATCH 05/24] Fix Xamarin connect freeze (#1545) --- .github/workflows/ReleaseNotes.md | 1 + .../MQTTnet.Tests/Client/MqttClient_Tests.cs | 2 +- Source/MQTTnet/Adapter/MqttChannelAdapter.cs | 23 +++++++++++++++++-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index e69de29b..caaab403 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -0,0 +1 @@ +* [Client] Fixed connection freeze when using Xamarin etc. \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Client/MqttClient_Tests.cs b/Source/MQTTnet.Tests/Client/MqttClient_Tests.cs index 43a72ab4..b073c5fe 100644 --- a/Source/MQTTnet.Tests/Client/MqttClient_Tests.cs +++ b/Source/MQTTnet.Tests/Client/MqttClient_Tests.cs @@ -123,7 +123,7 @@ namespace MQTTnet.Tests.Client //Assert.IsInstanceOfType(exception.InnerException, typeof(SocketException)); } - await Task.Delay(100); // disconnected handler is called async + await LongTestDelay(); // disconnected handler is called async Assert.IsTrue(disconnectHandlerCalled); } } diff --git a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs index 5a0bcd6c..5af179f2 100644 --- a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs +++ b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs @@ -23,7 +23,7 @@ namespace MQTTnet.Adapter { const uint ErrorOperationAborted = 0x800703E3; const int ReadBufferSize = 4096; - + readonly IMqttChannel _channel; readonly byte[] _fixedHeaderBuffer = new byte[2]; readonly MqttNetSourceLogger _logger; @@ -73,7 +73,26 @@ namespace MQTTnet.Adapter try { - await _channel.ConnectAsync(cancellationToken).ConfigureAwait(false); + /* + * We have to implement a small workaround here to support connecting in Xamarin + * with a disabled WiFi network. If the WiFi is disabled the connect method will + * block forever. Even a cancellation token is not supported properly. + */ + + var connectTask = _channel.ConnectAsync(cancellationToken); + + var timeout = new TaskCompletionSource(); + using (cancellationToken.Register(() => timeout.TrySetResult(null))) + { + await Task.WhenAny(connectTask, timeout.Task).ConfigureAwait(false); + if (timeout.Task.IsCompleted && !connectTask.IsCompleted) + { + throw new OperationCanceledException("MQTT connect cancelled.", cancellationToken); + } + + // Make sure that the exception from the connect task gets thrown. + await connectTask.ConfigureAwait(false); + } } catch (Exception exception) { From d2ce9d607a0058d53bc46bb594b6af44e9a1925c Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sun, 23 Oct 2022 08:16:45 +0200 Subject: [PATCH 06/24] Fix freezes in _AsyncLock_ (#1549) --- .github/workflows/ReleaseNotes.md | 1 + Samples/Client/Client_Connection_Samples.cs | 63 +++++- .../MqttConnectionContext.cs | 2 +- .../MQTTnet.Benchmarks/AsyncLockBenchmark.cs | 4 +- .../ManagedMqttClient.cs | 10 +- .../ManagedMqttClientStorageManager.cs | 4 +- .../MQTTnet.Extensions.Rpc/MqttRpcClient.cs | 6 +- .../WebSocket4NetMqttChannel.cs | 5 +- Source/MQTTnet.TestApp/AsyncLockTest.cs | 2 +- Source/MQTTnet.TestApp/ClientTest.cs | 4 +- Source/MQTTnet.TestApp/ManagedClientTest.cs | 6 +- .../MQTTnet.TestApp/MessageThroughputTest.cs | 3 +- Source/MQTTnet.TestApp/PublicBrokerTest.cs | 4 +- Source/MQTTnet.TestApp/ServerTest.cs | 20 +- .../Client/ManagedMqttClient_Tests.cs | 14 +- .../MQTTnet.Tests/Client/MqttClient_Tests.cs | 38 ++-- .../Diagnostics/PacketInspection_Tests.cs | 4 +- .../Factory/MqttFactory_Tests.cs | 4 +- .../Internal/AsyncEvent_Tests.cs | 49 ++++- .../MQTTnet.Tests/Internal/AsyncLock_Tests.cs | 178 ++++++++++++++-- Source/MQTTnet.Tests/MQTTnet.Tests.csproj | 2 +- Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs | 7 +- Source/MQTTnet.Tests/MQTTv5/Server_Tests.cs | 19 +- .../Mockups/MemoryMqttChannel.cs | 5 +- .../TestApplicationMessageReceivedHandler.cs | 4 +- .../MQTTnet.Tests/Mockups/TestEnvironment.cs | 6 +- Source/MQTTnet.Tests/RPC_Tests.cs | 3 +- Source/MQTTnet.Tests/RoundtripTime_Tests.cs | 4 +- .../Server/Assigned_Client_ID_Tests.cs | 10 +- Source/MQTTnet.Tests/Server/Events_Tests.cs | 16 +- Source/MQTTnet.Tests/Server/General.cs | 65 +++--- Source/MQTTnet.Tests/Server/Load_Tests.cs | 8 +- Source/MQTTnet.Tests/Server/QoS_Tests.cs | 8 +- Source/MQTTnet.Tests/Server/Security_Tests.cs | 8 +- .../Server/Server_Reference_Tests.cs | 3 +- Source/MQTTnet.Tests/Server/Session_Tests.cs | 72 +++---- .../MQTTnet.Tests/Server/Subscribe_Tests.cs | 24 +-- .../MQTTnet.Tests/Server/Topic_Alias_Tests.cs | 4 +- .../Server/User_Properties_Tests.cs | 3 +- Source/MQTTnet.Tests/Server/Will_Tests.cs | 6 +- Source/MQTTnet/Adapter/MqttChannelAdapter.cs | 4 +- Source/MQTTnet/Client/IMqttClient.cs | 1 + Source/MQTTnet/Client/MqttClient.cs | 19 +- Source/MQTTnet/Formatter/MqttBufferReader.cs | 8 +- Source/MQTTnet/Formatter/MqttPacketBuffer.cs | 3 +- .../Implementations/CrossPlatformSocket.cs | 193 ++++++++++++------ .../MQTTnet/Implementations/MqttTcpChannel.cs | 3 +- .../Implementations/MqttTcpServerAdapter.cs | 7 +- .../Implementations/MqttWebSocketChannel.cs | 2 +- .../PlatformAbstractionLayer.cs | 43 ---- Source/MQTTnet/Internal/AsyncEvent.cs | 9 +- .../MQTTnet/Internal/AsyncEventInvocator.cs | 2 +- Source/MQTTnet/Internal/AsyncLock.cs | 188 ++++++++--------- .../Internal/AsyncTaskCompletionSource.cs | 25 ++- Source/MQTTnet/Internal/CompletedTask.cs | 17 ++ Source/MQTTnet/Internal/EmptyBuffer.cs | 19 ++ Source/MQTTnet/MQTTnet.csproj | 1 - .../Events/ValidatingConnectionEventArgs.cs | 3 +- Source/MQTTnet/Server/Internal/MqttClient.cs | 6 +- .../Internal/MqttClientSessionsManager.cs | 6 +- .../Internal/MqttRetainedMessagesManager.cs | 8 +- .../Internal/MqttServerKeepAliveMonitor.cs | 41 +++- Source/MQTTnet/Server/MqttServer.cs | 3 +- .../Server/Status/MqttSessionStatus.cs | 5 +- 64 files changed, 798 insertions(+), 516 deletions(-) delete mode 100644 Source/MQTTnet/Implementations/PlatformAbstractionLayer.cs create mode 100644 Source/MQTTnet/Internal/CompletedTask.cs create mode 100644 Source/MQTTnet/Internal/EmptyBuffer.cs diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index caaab403..8dee1663 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -1 +1,2 @@ +* [Core] Fixed dead lock and race conditions in new _AsyncLock_ implementation (#1542). * [Client] Fixed connection freeze when using Xamarin etc. \ No newline at end of file diff --git a/Samples/Client/Client_Connection_Samples.cs b/Samples/Client/Client_Connection_Samples.cs index 18b619c4..e344ab30 100644 --- a/Samples/Client/Client_Connection_Samples.cs +++ b/Samples/Client/Client_Connection_Samples.cs @@ -119,7 +119,7 @@ public static class Client_Connection_Samples // The used public broker sometimes has invalid certificates. This sample accepts all // certificates. This should not be used in live environments. o.CertificateValidationHandler = _ => true; - + // The default value is determined by the OS. Set manually to force version. o.SslProtocol = SslProtocols.Tls12; }) @@ -134,15 +134,17 @@ public static class Client_Connection_Samples } } - public static async Task Connect_Client_Using_WebSockets() + public static async Task Connect_Client_Using_WebSocket4Net() { /* * This sample creates a simple MQTT client and connects to a public broker using a WebSocket connection. + * Instead of the .NET implementation of WebSockets the implementaion from WebSocket4Net is used. It provides more + * encryption algorithms and supports more platforms. * * This is a modified version of the sample _Connect_Client_! See other sample for more details. */ - var mqttFactory = new MqttFactory(); + var mqttFactory = new MqttFactory().UseWebSocket4Net(); using (var mqttClient = mqttFactory.CreateMqttClient()) { @@ -155,18 +157,16 @@ public static class Client_Connection_Samples response.DumpToConsole(); } } - - public static async Task Connect_Client_Using_WebSocket4Net() + + public static async Task Connect_Client_Using_WebSockets() { /* * This sample creates a simple MQTT client and connects to a public broker using a WebSocket connection. - * Instead of the .NET implementation of WebSockets the implementaion from WebSocket4Net is used. It provides more - * encryption algorithms and supports more platforms. * * This is a modified version of the sample _Connect_Client_! See other sample for more details. */ - var mqttFactory = new MqttFactory().UseWebSocket4Net(); + var mqttFactory = new MqttFactory(); using (var mqttClient = mqttFactory.CreateMqttClient()) { @@ -206,7 +206,7 @@ public static class Client_Connection_Samples using (var timeout = new CancellationTokenSource(5000)) { var response = await mqttClient.ConnectAsync(mqttClientOptions, timeout.Token); - + Console.WriteLine("The MQTT client is connected."); response.DumpToConsole(); @@ -214,6 +214,49 @@ public static class Client_Connection_Samples } } + public static async Task Disconnect_Clean() + { + /* + * This sample disconnects from the server with sending a DISCONNECT packet. + * This way of disconnecting is treated as a clean disconnect which will not + * trigger sending the last will etc. + */ + + var mqttFactory = new MqttFactory(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + // Calling _DisconnectAsync_ will send a DISCONNECT packet before closing the connection. + // Using a reason code requires MQTT version 5.0.0! + await mqttClient.DisconnectAsync(MqttClientDisconnectReason.ImplementationSpecificError); + } + } + + public static async Task Disconnect_Non_Clean() + { + /* + * This sample disconnects from the server without sending a DISCONNECT packet. + * This way of disconnecting is treated as a non clean disconnect which will + * trigger sending the last will etc. + */ + + var mqttFactory = new MqttFactory(); + + var mqttClient = mqttFactory.CreateMqttClient(); + + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + // Calling _Dispose_ or use of a _using_ statement will close the transport connection + // without sending a DISCONNECT packet to the server. + mqttClient.Dispose(); + } + public static async Task Inspect_Certificate_Validation_Errors() { /* @@ -248,7 +291,7 @@ public static class Client_Connection_Samples } } } - + public static async Task Ping_Server() { /* diff --git a/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs b/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs index 1268d25e..90ee5ce6 100644 --- a/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs +++ b/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs @@ -171,7 +171,7 @@ namespace MQTTnet.AspNetCore { var formatter = PacketFormatterAdapter; - using (await _writerLock.WaitAsync(cancellationToken).ConfigureAwait(false)) + using (await _writerLock.EnterAsync(cancellationToken).ConfigureAwait(false)) { var buffer = formatter.Encode(packet); var msg = buffer.Join().AsMemory(); diff --git a/Source/MQTTnet.Benchmarks/AsyncLockBenchmark.cs b/Source/MQTTnet.Benchmarks/AsyncLockBenchmark.cs index 5b50c3e3..27a348d4 100644 --- a/Source/MQTTnet.Benchmarks/AsyncLockBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/AsyncLockBenchmark.cs @@ -29,7 +29,7 @@ namespace MQTTnet.Benchmarks tasks[i] = Task.Run( async () => { - using (await asyncLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) + using (await asyncLock.EnterAsync().ConfigureAwait(false)) { var localI = globalI; await Task.Delay(5).ConfigureAwait(false); // Increase the chance for wrong data. @@ -56,7 +56,7 @@ namespace MQTTnet.Benchmarks { for (var i = 0; i < 100000; i++) { - using (await asyncLock.WaitAsync(cancellationToken.Token).ConfigureAwait(false)) + using (await asyncLock.EnterAsync(cancellationToken.Token).ConfigureAwait(false)) { } } diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs index b4fa02c2..2583a605 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs @@ -167,7 +167,7 @@ namespace MQTTnet.Extensions.ManagedClient try { - using (await _messageQueueLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) + using (await _messageQueueLock.EnterAsync().ConfigureAwait(false)) { if (_messageQueue.Count >= Options.MaxPendingMessages) { @@ -296,7 +296,7 @@ namespace MQTTnet.Extensions.ManagedClient _subscriptionsQueuedSignal.Release(); - return Task.FromResult(0); + return CompletedTask.Instance; } public Task UnsubscribeAsync(ICollection topics) @@ -319,7 +319,7 @@ namespace MQTTnet.Extensions.ManagedClient _subscriptionsQueuedSignal.Release(); - return Task.FromResult(0); + return CompletedTask.Instance; } protected override void Dispose(bool disposing) @@ -693,7 +693,7 @@ namespace MQTTnet.Extensions.ManagedClient { await InternalClient.PublishAsync(message.ApplicationMessage).ConfigureAwait(false); - using (await _messageQueueLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) //lock to avoid conflict with this.PublishAsync + using (await _messageQueueLock.EnterAsync().ConfigureAwait(false)) //lock to avoid conflict with this.PublishAsync { // While publishing this message, this.PublishAsync could have booted this // message off the queue to make room for another (when using a cap @@ -724,7 +724,7 @@ namespace MQTTnet.Extensions.ManagedClient //contradict the expected behavior of QoS 1 and 2, that's also true //for the usage of a message queue cap, so it's still consistent //with prior behavior in that way. - using (await _messageQueueLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) //lock to avoid conflict with this.PublishAsync + using (await _messageQueueLock.EnterAsync().ConfigureAwait(false)) //lock to avoid conflict with this.PublishAsync { _messageQueue.RemoveFirst(i => i.Id.Equals(message.Id)); diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs index 1b812d8d..bd1c48c2 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs @@ -34,7 +34,7 @@ namespace MQTTnet.Extensions.ManagedClient { if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); - using (await _messagesLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) + using (await _messagesLock.EnterAsync().ConfigureAwait(false)) { _messages.Add(applicationMessage); await SaveAsync().ConfigureAwait(false); @@ -45,7 +45,7 @@ namespace MQTTnet.Extensions.ManagedClient { if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); - using (await _messagesLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) + using (await _messagesLock.EnterAsync().ConfigureAwait(false)) { var index = _messages.IndexOf(applicationMessage); if (index == -1) diff --git a/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs b/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs index 29a9d6f6..205e1591 100644 --- a/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs +++ b/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs @@ -9,7 +9,7 @@ using System; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Implementations; +using MQTTnet.Internal; namespace MQTTnet.Extensions.Rpc { @@ -116,7 +116,7 @@ namespace MQTTnet.Extensions.Rpc { if (!_waitingCalls.TryRemove(eventArgs.ApplicationMessage.Topic, out var awaitable)) { - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; } #if NET452 @@ -128,7 +128,7 @@ namespace MQTTnet.Extensions.Rpc // Set this message to handled to that other code can avoid execution etc. eventArgs.IsHandled = true; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; } public void Dispose() diff --git a/Source/MQTTnet.Extensions.WebSocket4Net/WebSocket4NetMqttChannel.cs b/Source/MQTTnet.Extensions.WebSocket4Net/WebSocket4NetMqttChannel.cs index bbbdc81f..d656a92f 100644 --- a/Source/MQTTnet.Extensions.WebSocket4Net/WebSocket4NetMqttChannel.cs +++ b/Source/MQTTnet.Extensions.WebSocket4Net/WebSocket4NetMqttChannel.cs @@ -14,6 +14,7 @@ using System.Threading.Tasks; using MQTTnet.Channel; using MQTTnet.Client; using MQTTnet.Exceptions; +using MQTTnet.Internal; using SuperSocket.ClientEngine; using WebSocket4Net; @@ -124,7 +125,7 @@ namespace MQTTnet.Extensions.WebSocket4Net _webSocket.Close(); } - return Task.FromResult(0); + return CompletedTask.Instance; } public Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) @@ -157,7 +158,7 @@ namespace MQTTnet.Extensions.WebSocket4Net public Task WriteAsync(ArraySegment buffer, bool isEndOfPacket, CancellationToken cancellationToken) { _webSocket.Send(buffer.Array, buffer.Offset, buffer.Count); - return Task.FromResult(0); + return CompletedTask.Instance; } public void Dispose() diff --git a/Source/MQTTnet.TestApp/AsyncLockTest.cs b/Source/MQTTnet.TestApp/AsyncLockTest.cs index 4f943b19..035f3d46 100644 --- a/Source/MQTTnet.TestApp/AsyncLockTest.cs +++ b/Source/MQTTnet.TestApp/AsyncLockTest.cs @@ -18,7 +18,7 @@ namespace MQTTnet.TestApp { for (var i = 0; i < 100000; i++) { - using (await asyncLock.WaitAsync(cancellationToken.Token).ConfigureAwait(false)) + using (await asyncLock.EnterAsync(cancellationToken.Token).ConfigureAwait(false)) { } } diff --git a/Source/MQTTnet.TestApp/ClientTest.cs b/Source/MQTTnet.TestApp/ClientTest.cs index 33469acb..9f233256 100644 --- a/Source/MQTTnet.TestApp/ClientTest.cs +++ b/Source/MQTTnet.TestApp/ClientTest.cs @@ -7,7 +7,7 @@ using System; using System.Text; using System.Threading.Tasks; using MQTTnet.Diagnostics; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Protocol; namespace MQTTnet.TestApp @@ -40,7 +40,7 @@ namespace MQTTnet.TestApp Console.WriteLine($"+ Retain = {e.ApplicationMessage.Retain}"); Console.WriteLine(); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; client.ConnectedAsync += async e => diff --git a/Source/MQTTnet.TestApp/ManagedClientTest.cs b/Source/MQTTnet.TestApp/ManagedClientTest.cs index 88125ff8..976c17e6 100644 --- a/Source/MQTTnet.TestApp/ManagedClientTest.cs +++ b/Source/MQTTnet.TestApp/ManagedClientTest.cs @@ -9,7 +9,7 @@ using Newtonsoft.Json; using System.Collections.Generic; using MQTTnet.Client; using MQTTnet.Extensions.ManagedClient; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Protocol; namespace MQTTnet.TestApp @@ -42,7 +42,7 @@ namespace MQTTnet.TestApp managedClient.ApplicationMessageReceivedAsync += e => { Console.WriteLine(">> RECEIVED: " + e.ApplicationMessage.Topic); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await managedClient.StartAsync(options); @@ -85,7 +85,7 @@ namespace MQTTnet.TestApp public Task SaveQueuedMessagesAsync(IList messages) { File.WriteAllText(Filename, JsonConvert.SerializeObject(messages)); - return Task.FromResult(0); + return CompletedTask.Instance; } public Task> LoadQueuedMessagesAsync() diff --git a/Source/MQTTnet.TestApp/MessageThroughputTest.cs b/Source/MQTTnet.TestApp/MessageThroughputTest.cs index 67232f10..a7b36df9 100644 --- a/Source/MQTTnet.TestApp/MessageThroughputTest.cs +++ b/Source/MQTTnet.TestApp/MessageThroughputTest.cs @@ -5,6 +5,7 @@ using System; using System.Threading; using System.Threading.Tasks; using System.Linq; +using MQTTnet.Internal; namespace MQTTnet.TestApp { @@ -124,7 +125,7 @@ namespace MQTTnet.TestApp { _cancellationTokenSource.Cancel(); } - return Implementations.PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; } public async Task Cleanup() diff --git a/Source/MQTTnet.TestApp/PublicBrokerTest.cs b/Source/MQTTnet.TestApp/PublicBrokerTest.cs index 65aaa31c..768ba874 100644 --- a/Source/MQTTnet.TestApp/PublicBrokerTest.cs +++ b/Source/MQTTnet.TestApp/PublicBrokerTest.cs @@ -8,7 +8,7 @@ using System.Security.Authentication; using System.Threading; using System.Threading.Tasks; using MQTTnet.Formatter; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Protocol; namespace MQTTnet.TestApp @@ -144,7 +144,7 @@ namespace MQTTnet.TestApp client.ApplicationMessageReceivedAsync += e => { receivedMessage = e.ApplicationMessage; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await client.ConnectAsync(options); diff --git a/Source/MQTTnet.TestApp/ServerTest.cs b/Source/MQTTnet.TestApp/ServerTest.cs index e71f9761..a0925def 100644 --- a/Source/MQTTnet.TestApp/ServerTest.cs +++ b/Source/MQTTnet.TestApp/ServerTest.cs @@ -4,13 +4,11 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Text; using System.Threading.Tasks; using MQTTnet.Diagnostics; -using MQTTnet.Extensions.ManagedClient; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Protocol; using MQTTnet.Server; using Newtonsoft.Json; @@ -69,13 +67,13 @@ namespace MQTTnet.TestApp } File.WriteAllText(Filename, JsonConvert.SerializeObject(e.StoredRetainedMessages)); - return Task.FromResult(0); + return CompletedTask.Instance; }; mqttServer.RetainedMessagesClearedAsync += e => { File.Delete(Filename); - return Task.FromResult(0); + return CompletedTask.Instance; }; mqttServer.LoadingRetainedMessageAsync += e => @@ -93,7 +91,7 @@ namespace MQTTnet.TestApp e.LoadedRetainedMessages = retainedMessages; - return Task.FromResult(0); + return CompletedTask.Instance; }; mqttServer.InterceptingPublishAsync += e => @@ -111,7 +109,7 @@ namespace MQTTnet.TestApp e.CloseConnection = true; } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; mqttServer.ValidatingConnectionAsync += e => @@ -124,7 +122,7 @@ namespace MQTTnet.TestApp } } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; mqttServer.InterceptingSubscriptionAsync += e => @@ -140,7 +138,7 @@ namespace MQTTnet.TestApp e.CloseConnection = true; } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; mqttServer.InterceptingPublishAsync += e => @@ -149,7 +147,7 @@ namespace MQTTnet.TestApp $"'{e.ClientId}' reported '{e.ApplicationMessage.Topic}' > '{Encoding.UTF8.GetString(e.ApplicationMessage.Payload ?? new byte[0])}'", ConsoleColor.Magenta); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; //options.ApplicationMessageInterceptor = c => @@ -177,7 +175,7 @@ namespace MQTTnet.TestApp mqttServer.ClientConnectedAsync += e => { Console.Write("Client disconnected event fired."); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await mqttServer.StartAsync(); diff --git a/Source/MQTTnet.Tests/Client/ManagedMqttClient_Tests.cs b/Source/MQTTnet.Tests/Client/ManagedMqttClient_Tests.cs index a590027d..e161b212 100644 --- a/Source/MQTTnet.Tests/Client/ManagedMqttClient_Tests.cs +++ b/Source/MQTTnet.Tests/Client/ManagedMqttClient_Tests.cs @@ -11,12 +11,10 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; using MQTTnet.Diagnostics; using MQTTnet.Extensions.ManagedClient; -using MQTTnet.Implementations; using MQTTnet.Internal; using MQTTnet.Protocol; using MQTTnet.Server; using MQTTnet.Tests.Mockups; -using MqttClient = MQTTnet.Client.MqttClient; namespace MQTTnet.Tests.Client { @@ -39,7 +37,7 @@ namespace MQTTnet.Tests.Client managedClient.ConnectingFailedAsync += args => { connectingFailedEventArgs = args; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await managedClient.StartAsync( @@ -117,7 +115,7 @@ namespace MQTTnet.Tests.Client receivingClient.ApplicationMessageReceivedAsync += e => { Interlocked.Increment(ref receivedMessagesCount); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await receivingClient.SubscribeAsync("My/last/will"); @@ -294,7 +292,7 @@ namespace MQTTnet.Tests.Client managedClient.ApplicationMessageReceivedAsync += e => { receivedManagedMessages.Add(e.ApplicationMessage); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await managedClient.SubscribeAsync("topic"); @@ -428,7 +426,7 @@ namespace MQTTnet.Tests.Client managedClient.ConnectedAsync += e => { connected.TrySetResult(true); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; return connected.Task; @@ -457,7 +455,7 @@ namespace MQTTnet.Tests.Client result.TrySetResult(receivedMessages); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; return result.Task; @@ -486,7 +484,7 @@ namespace MQTTnet.Tests.Client public Task SaveQueuedMessagesAsync(IList messages) { _messages = messages; - return Task.FromResult(0); + return CompletedTask.Instance; } } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Client/MqttClient_Tests.cs b/Source/MQTTnet.Tests/Client/MqttClient_Tests.cs index b073c5fe..8553ebd0 100644 --- a/Source/MQTTnet.Tests/Client/MqttClient_Tests.cs +++ b/Source/MQTTnet.Tests/Client/MqttClient_Tests.cs @@ -14,7 +14,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; using MQTTnet.Exceptions; using MQTTnet.Formatter; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; using MQTTnet.Tests.Mockups; @@ -58,7 +58,7 @@ namespace MQTTnet.Tests.Client } _ = InvokeInternal(); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var publishes = Task.WhenAll(publisher.PublishStringAsync("a", null, qos), publisher.PublishStringAsync("b", null, qos)); @@ -109,7 +109,7 @@ namespace MQTTnet.Tests.Client client.DisconnectedAsync += args => { disconnectHandlerCalled = true; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").Build()); @@ -138,7 +138,7 @@ namespace MQTTnet.Tests.Client client.DisconnectedAsync += e => { ex = e.Exception; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; try @@ -170,7 +170,7 @@ namespace MQTTnet.Tests.Client server.InterceptingPublishAsync += c => { i++; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await client.SendAsync( @@ -211,7 +211,7 @@ namespace MQTTnet.Tests.Client client.DisconnectedAsync += e => { handlerFired = true; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await server.StopAsync(); @@ -255,7 +255,7 @@ namespace MQTTnet.Tests.Client receiveClient.ApplicationMessageReceivedAsync += e => { receivedPayload = e.ApplicationMessage.ConvertPayloadToString(); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await receiveClient.SubscribeAsync("x"); @@ -313,7 +313,7 @@ namespace MQTTnet.Tests.Client receiver.ApplicationMessageReceivedAsync += e => { receivedMessage = e.ApplicationMessage; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await sender.PublishAsync(message.Build(), CancellationToken.None); @@ -456,7 +456,7 @@ namespace MQTTnet.Tests.Client receivedValues.Add(value); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; } client1.ApplicationMessageReceivedAsync += Handler1; @@ -534,7 +534,7 @@ namespace MQTTnet.Tests.Client client2.ApplicationMessageReceivedAsync += e => { client2TopicResults.Add(Encoding.UTF8.GetString(e.ApplicationMessage.Payload)); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await client2.SubscribeAsync(client2Topic); @@ -569,7 +569,7 @@ namespace MQTTnet.Tests.Client receivedMessages.Add(e.ApplicationMessage); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await client1.SubscribeAsync("a"); @@ -728,7 +728,7 @@ namespace MQTTnet.Tests.Client replies.Add(eventArgs.ApplicationMessage.Topic); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; } client2.ApplicationMessageReceivedAsync += Handler2; @@ -766,7 +766,7 @@ namespace MQTTnet.Tests.Client replyReceived = true; } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; client2.ApplicationMessageReceivedAsync += async e => @@ -817,7 +817,7 @@ namespace MQTTnet.Tests.Client replyReceived = true; } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await client.PublishStringAsync("request", null, MqttQualityOfServiceLevel.AtLeastOnce); @@ -840,7 +840,7 @@ namespace MQTTnet.Tests.Client client.DisconnectedAsync += e => { Assert.IsTrue(e.ClientWasConnected); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await client.DisconnectAsync(); @@ -860,7 +860,7 @@ namespace MQTTnet.Tests.Client client.DisconnectedAsync += e => { Assert.IsTrue(e.ClientWasConnected); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await server.StopAsync(); @@ -897,7 +897,7 @@ namespace MQTTnet.Tests.Client receivedMessages.Add(e.ApplicationMessage); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).Build()); @@ -922,14 +922,14 @@ namespace MQTTnet.Tests.Client client1.DisconnectedAsync += c => { disconnectedFired = true; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var messageReceived = false; client1.ApplicationMessageReceivedAsync += e => { messageReceived = true; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await client1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("topic1").WithExactlyOnceQoS().Build()); diff --git a/Source/MQTTnet.Tests/Diagnostics/PacketInspection_Tests.cs b/Source/MQTTnet.Tests/Diagnostics/PacketInspection_Tests.cs index 225f2cec..5589a37d 100644 --- a/Source/MQTTnet.Tests/Diagnostics/PacketInspection_Tests.cs +++ b/Source/MQTTnet.Tests/Diagnostics/PacketInspection_Tests.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Implementations; +using MQTTnet.Internal; namespace MQTTnet.Tests.Diagnostics { @@ -33,7 +33,7 @@ namespace MQTTnet.Tests.Diagnostics mqttClient.InspectPackage += eventArgs => { packets.Add(eventArgs.Direction + ":" + Convert.ToBase64String(eventArgs.Buffer)); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); diff --git a/Source/MQTTnet.Tests/Factory/MqttFactory_Tests.cs b/Source/MQTTnet.Tests/Factory/MqttFactory_Tests.cs index b34d555e..68c0fc61 100644 --- a/Source/MQTTnet.Tests/Factory/MqttFactory_Tests.cs +++ b/Source/MQTTnet.Tests/Factory/MqttFactory_Tests.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Diagnostics; using MQTTnet.Extensions.ManagedClient; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Server; namespace MQTTnet.Tests.Factory @@ -56,7 +56,7 @@ namespace MQTTnet.Tests.Factory managedClient.ConnectingFailedAsync += e => { tcs.TrySetResult(null); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await Task.WhenAny(Task.Delay(managedClient.Options.ClientOptions.Timeout), tcs.Task); diff --git a/Source/MQTTnet.Tests/Internal/AsyncEvent_Tests.cs b/Source/MQTTnet.Tests/Internal/AsyncEvent_Tests.cs index 54442bf6..94a960e5 100644 --- a/Source/MQTTnet.Tests/Internal/AsyncEvent_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/AsyncEvent_Tests.cs @@ -6,7 +6,6 @@ using System; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Implementations; using MQTTnet.Internal; namespace MQTTnet.Tests.Internal @@ -16,6 +15,28 @@ namespace MQTTnet.Tests.Internal { int _testEventAsyncCount; + [TestMethod] + public async Task Attach_and_Detach_Sync_And_Async_EventHandler() + { + var testClass = new TestClass(); + + testClass.TestEventAsync += OnTestEventAsync; + testClass.TestEvent += OnTestEvent; + + await testClass.FireEventAsync(EventArgs.Empty); + Assert.AreEqual(2, _testEventAsyncCount); + + testClass.TestEvent -= OnTestEvent; + + await testClass.FireEventAsync(EventArgs.Empty); + Assert.AreEqual(3, _testEventAsyncCount); + + testClass.TestEventAsync -= OnTestEventAsync; + + await testClass.FireEventAsync(EventArgs.Empty); + Assert.AreEqual(3, _testEventAsyncCount); + } + [TestMethod] public async Task Attach_EventHandler() { @@ -28,6 +49,19 @@ namespace MQTTnet.Tests.Internal Assert.AreEqual(1, _testEventAsyncCount); } + [TestMethod] + public async Task Attach_Sync_And_Async_EventHandler() + { + var testClass = new TestClass(); + + testClass.TestEventAsync += OnTestEventAsync; + testClass.TestEvent += OnTestEvent; + + await testClass.FireEventAsync(EventArgs.Empty); + + Assert.AreEqual(2, _testEventAsyncCount); + } + [TestMethod] public async Task Detach_EventHandler() { @@ -68,16 +102,27 @@ namespace MQTTnet.Tests.Internal Assert.AreEqual(false, testClass.HasTestHandlers); } + void OnTestEvent(EventArgs arg) + { + Interlocked.Increment(ref _testEventAsyncCount); + } + Task OnTestEventAsync(EventArgs arg) { Interlocked.Increment(ref _testEventAsyncCount); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; } sealed class TestClass { readonly AsyncEvent _testEvent = new AsyncEvent(); + public event Action TestEvent + { + add => _testEvent.AddHandler(value); + remove => _testEvent.RemoveHandler(value); + } + public event Func TestEventAsync { add => _testEvent.AddHandler(value); diff --git a/Source/MQTTnet.Tests/Internal/AsyncLock_Tests.cs b/Source/MQTTnet.Tests/Internal/AsyncLock_Tests.cs index fae81227..e30fd63d 100644 --- a/Source/MQTTnet.Tests/Internal/AsyncLock_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/AsyncLock_Tests.cs @@ -12,26 +12,130 @@ using MQTTnet.Internal; namespace MQTTnet.Tests.Internal { [TestClass] - public class AsyncLock_Tests + public sealed class AsyncLock_Tests { [TestMethod] - public void Lock_10_Parallel_Tasks() + public async Task Cancellation_Of_Awaiter() { - const int ThreadsCount = 10; - - var threads = new Task[ThreadsCount]; var @lock = new AsyncLock(); + + // This call will not yet "release" the lock due to missing _using_. + var releaser = await @lock.EnterAsync().ConfigureAwait(false); + + var counter = 0; + + Debug.WriteLine("Prepared locked lock."); + + _ = Task.Run( + async () => + { + // SHOULD GET TIMEOUT! + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(1))) + { + using (await @lock.EnterAsync(timeout.Token)) + { + Debug.WriteLine("Task 1 incremented"); + counter++; + } + } + }); + + _ = Task.Run( + async () => + { + // SHOULD GET TIMEOUT! + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2))) + { + using (await @lock.EnterAsync(timeout.Token)) + { + Debug.WriteLine("Task 2 incremented"); + counter++; + } + } + }); + + _ = Task.Run( + async () => + { + // SHOULD GET TIMEOUT! + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3))) + { + using (await @lock.EnterAsync(timeout.Token)) + { + Debug.WriteLine("Task 3 incremented"); + counter++; + } + } + }); + + _ = Task.Run( + async () => + { + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(4))) + { + using (await @lock.EnterAsync(timeout.Token)) + { + Debug.WriteLine("Task 4 incremented"); + counter++; + } + } + }); + + _ = Task.Run( + async () => + { + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5))) + { + using (await @lock.EnterAsync(timeout.Token)) + { + Debug.WriteLine("Task 5 incremented"); + counter++; + } + } + }); + + _ = Task.Run( + async () => + { + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(6))) + { + using (await @lock.EnterAsync(timeout.Token)) + { + Debug.WriteLine("Task 6 incremented"); + counter++; + } + } + }); + + Debug.WriteLine("Delay before release..."); + await Task.Delay(TimeSpan.FromSeconds(3.1)); + releaser.Dispose(); + + Debug.WriteLine("Wait for all tasks..."); + await Task.Delay(TimeSpan.FromSeconds(6.1)); + + Assert.AreEqual(3, counter); + } + + [TestMethod] + public void Lock_Parallel_Tasks() + { + const int taskCount = 50; + + var @lock = new AsyncLock(); + + var tasks = new Task[taskCount]; var globalI = 0; - for (var i = 0; i < ThreadsCount; i++) + for (var i = 0; i < taskCount; i++) { #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - threads[i] = Task.Run( + tasks[i] = Task.Run( async () => { - using (await @lock.WaitAsync(CancellationToken.None)) + using (await @lock.EnterAsync()) { var localI = globalI; - await Task.Delay(10); // Increase the chance for wrong data. + await Task.Delay(5); // Increase the chance for wrong data. localI++; globalI = localI; } @@ -39,8 +143,8 @@ namespace MQTTnet.Tests.Internal #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed } - Task.WaitAll(threads); - Assert.AreEqual(ThreadsCount, globalI); + Task.WaitAll(tasks); + Assert.AreEqual(taskCount, globalI); } [TestMethod] @@ -57,7 +161,7 @@ namespace MQTTnet.Tests.Internal threads[i] = Task.Run( async () => { - using (await @lock.WaitAsync(CancellationToken.None)) + using (await @lock.EnterAsync()) { var localI = globalI; await Task.Delay(10); // Increase the chance for wrong data. @@ -96,7 +200,7 @@ namespace MQTTnet.Tests.Internal var @lock = new AsyncLock(); for (var i = 0; i < 100; i++) { - using (await @lock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) + using (await @lock.EnterAsync().ConfigureAwait(false)) { sum++; } @@ -112,11 +216,11 @@ namespace MQTTnet.Tests.Internal var @lock = new AsyncLock(); // This call will never "release" the lock due to missing _using_. - await @lock.WaitAsync(CancellationToken.None).ConfigureAwait(false); + await @lock.EnterAsync().ConfigureAwait(false); using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3))) { - await @lock.WaitAsync(cts.Token).ConfigureAwait(false); + await @lock.EnterAsync(cts.Token).ConfigureAwait(false); } } @@ -125,15 +229,15 @@ namespace MQTTnet.Tests.Internal { var asyncLock = new AsyncLock(); - var releaser = await asyncLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); + var releaser = await asyncLock.EnterAsync().ConfigureAwait(false); try { using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3))) { - await asyncLock.WaitAsync(timeout.Token).ConfigureAwait(false); + await asyncLock.EnterAsync(timeout.Token).ConfigureAwait(false); } - + Assert.Fail("Exception should be thrown!"); } catch (OperationCanceledException) @@ -142,10 +246,46 @@ namespace MQTTnet.Tests.Internal releaser.Dispose(); - using (await asyncLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) + using (await asyncLock.EnterAsync(CancellationToken.None).ConfigureAwait(false)) { // When the method finished, the thread got access. } } + + [TestMethod] + public async Task Use_After_Cancellation() + { + var @lock = new AsyncLock(); + + // This call will not yet "release" the lock due to missing _using_. + var releaser = await @lock.EnterAsync().ConfigureAwait(false); + + try + { + using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3))) + { + await @lock.EnterAsync(cts.Token).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + // Expected exception! + } + + releaser.Dispose(); + + // Regular usage after cancellation. + using (await @lock.EnterAsync().ConfigureAwait(false)) + { + } + + using (await @lock.EnterAsync().ConfigureAwait(false)) + { + } + + using (await @lock.EnterAsync().ConfigureAwait(false)) + { + } + } } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/MQTTnet.Tests.csproj b/Source/MQTTnet.Tests/MQTTnet.Tests.csproj index 2e95c15d..d9a7ce1e 100644 --- a/Source/MQTTnet.Tests/MQTTnet.Tests.csproj +++ b/Source/MQTTnet.Tests/MQTTnet.Tests.csproj @@ -1,7 +1,7 @@  - net452;net461;netcoreapp3.1;net6.0 + net452;net461;net48;netcoreapp3.1;net6.0 false 7.3 false diff --git a/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs b/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs index b87ca6af..ac7b8792 100644 --- a/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs +++ b/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs @@ -8,10 +8,9 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; using MQTTnet.Formatter; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; -using MQTTnet.Tests.Mockups; namespace MQTTnet.Tests.MQTTv5 { @@ -43,7 +42,7 @@ namespace MQTTnet.Tests.MQTTv5 client.ApplicationMessageReceivedAsync += e => { receivedMessage = e.ApplicationMessage; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await client.PublishAsync(new MqttApplicationMessageBuilder() @@ -245,7 +244,7 @@ namespace MQTTnet.Tests.MQTTv5 receiver.ApplicationMessageReceivedAsync += e => { receivedMessage = e.ApplicationMessage; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var sender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); diff --git a/Source/MQTTnet.Tests/MQTTv5/Server_Tests.cs b/Source/MQTTnet.Tests/MQTTv5/Server_Tests.cs index 180117b0..a187972d 100644 --- a/Source/MQTTnet.Tests/MQTTv5/Server_Tests.cs +++ b/Source/MQTTnet.Tests/MQTTv5/Server_Tests.cs @@ -8,7 +8,7 @@ using MQTTnet.Formatter; using MQTTnet.Tests.Mockups; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Protocol; namespace MQTTnet.Tests.MQTTv5 @@ -31,7 +31,7 @@ namespace MQTTnet.Tests.MQTTv5 c1.ApplicationMessageReceivedAsync += e => { Interlocked.Increment(ref receivedMessagesCount); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); @@ -171,7 +171,6 @@ namespace MQTTnet.Tests.MQTTv5 // Session should be present Assert.IsTrue(!result3.IsSessionPresent, "Session is present when it should not"); - } } @@ -180,7 +179,7 @@ namespace MQTTnet.Tests.MQTTv5 { using (var testEnvironment = CreateTestEnvironment()) { - var disconnectReason = MQTTnet.Client.MqttClientDisconnectReason.UnspecifiedError; + var disconnectReason = MqttClientDisconnectReason.UnspecifiedError; string testClientId = null; @@ -189,15 +188,15 @@ namespace MQTTnet.Tests.MQTTv5 testEnvironment.Server.ClientConnectedAsync += e => { testClientId = e.ClientId; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; - var client = await testEnvironment.ConnectClient(new MQTTnet.Client.MqttClientOptionsBuilder().WithProtocolVersion(MQTTnet.Formatter.MqttProtocolVersion.V500)); + var client = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); client.DisconnectedAsync += e => { disconnectReason = e.Reason; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await LongTestDelay(); @@ -208,17 +207,17 @@ namespace MQTTnet.Tests.MQTTv5 // Have the server disconnect the client with AdministrativeAction reason - await testEnvironment.Server.DisconnectClientAsync(testClientId, Protocol.MqttDisconnectReasonCode.AdministrativeAction); + await testEnvironment.Server.DisconnectClientAsync(testClientId, MqttDisconnectReasonCode.AdministrativeAction); await LongTestDelay(); // The reason should be returned to the client in the DISCONNECT packet - Assert.AreEqual(MQTTnet.Client.MqttClientDisconnectReason.AdministrativeAction, disconnectReason); + Assert.AreEqual(MqttClientDisconnectReason.AdministrativeAction, disconnectReason); } } - MqttClientOptions CreateClientOptions(TestEnvironment testEnvironment, string clientId, bool cleanSession, uint sessionExpiryInterval) + static MqttClientOptions CreateClientOptions(TestEnvironment testEnvironment, string clientId, bool cleanSession, uint sessionExpiryInterval) { return testEnvironment.Factory.CreateClientOptionsBuilder() .WithProtocolVersion(MqttProtocolVersion.V500) diff --git a/Source/MQTTnet.Tests/Mockups/MemoryMqttChannel.cs b/Source/MQTTnet.Tests/Mockups/MemoryMqttChannel.cs index 7bdf97de..5cb8ead6 100644 --- a/Source/MQTTnet.Tests/Mockups/MemoryMqttChannel.cs +++ b/Source/MQTTnet.Tests/Mockups/MemoryMqttChannel.cs @@ -8,6 +8,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using MQTTnet.Channel; +using MQTTnet.Internal; namespace MQTTnet.Tests.Mockups { @@ -33,12 +34,12 @@ namespace MQTTnet.Tests.Mockups public Task ConnectAsync(CancellationToken cancellationToken) { - return Task.FromResult(0); + return CompletedTask.Instance; } public Task DisconnectAsync(CancellationToken cancellationToken) { - return Task.FromResult(0); + return CompletedTask.Instance; } public Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) diff --git a/Source/MQTTnet.Tests/Mockups/TestApplicationMessageReceivedHandler.cs b/Source/MQTTnet.Tests/Mockups/TestApplicationMessageReceivedHandler.cs index 81bca57b..c1a7c482 100644 --- a/Source/MQTTnet.Tests/Mockups/TestApplicationMessageReceivedHandler.cs +++ b/Source/MQTTnet.Tests/Mockups/TestApplicationMessageReceivedHandler.cs @@ -7,7 +7,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; -using MQTTnet.Implementations; +using MQTTnet.Internal; namespace MQTTnet.Tests.Mockups { @@ -46,7 +46,7 @@ namespace MQTTnet.Tests.Mockups _receivedEventArgs.Add(eventArgs); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; } } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs b/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs index c3da9717..6b79a963 100644 --- a/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs +++ b/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs @@ -13,7 +13,7 @@ using MQTTnet.Client; using MQTTnet.Diagnostics; using MQTTnet.Extensions.Rpc; using MQTTnet.Formatter; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.LowLevelClient; using MQTTnet.Protocol; using MQTTnet.Server; @@ -220,7 +220,7 @@ namespace MQTTnet.Tests.Mockups } } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; return client; @@ -261,7 +261,7 @@ namespace MQTTnet.Tests.Mockups } } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; return Server; diff --git a/Source/MQTTnet.Tests/RPC_Tests.cs b/Source/MQTTnet.Tests/RPC_Tests.cs index e37996ea..f266f5a6 100644 --- a/Source/MQTTnet.Tests/RPC_Tests.cs +++ b/Source/MQTTnet.Tests/RPC_Tests.cs @@ -13,6 +13,7 @@ using MQTTnet.Extensions.Rpc; using MQTTnet.Protocol; using MQTTnet.Formatter; using MQTTnet.Implementations; +using MQTTnet.Internal; namespace MQTTnet.Tests { @@ -189,7 +190,7 @@ namespace MQTTnet.Tests responseSender.ApplicationMessageReceivedAsync += async e => { Assert.IsNull(e.ApplicationMessage.ResponseTopic); - await PlatformAbstractionLayer.CompletedTask; + await CompletedTask.Instance; }; var requestSender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); diff --git a/Source/MQTTnet.Tests/RoundtripTime_Tests.cs b/Source/MQTTnet.Tests/RoundtripTime_Tests.cs index 6ebbab04..01cb9a93 100644 --- a/Source/MQTTnet.Tests/RoundtripTime_Tests.cs +++ b/Source/MQTTnet.Tests/RoundtripTime_Tests.cs @@ -8,7 +8,7 @@ using System.Diagnostics; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Tests.Mockups; namespace MQTTnet.Tests @@ -33,7 +33,7 @@ namespace MQTTnet.Tests receiverClient.ApplicationMessageReceivedAsync += e => { response?.TrySetResult(e.ApplicationMessage.ConvertPayloadToString()); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await receiverClient.SubscribeAsync("#"); diff --git a/Source/MQTTnet.Tests/Server/Assigned_Client_ID_Tests.cs b/Source/MQTTnet.Tests/Server/Assigned_Client_ID_Tests.cs index 8e983be6..cce7c299 100644 --- a/Source/MQTTnet.Tests/Server/Assigned_Client_ID_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Assigned_Client_ID_Tests.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; using MQTTnet.Formatter; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Protocol; namespace MQTTnet.Tests.Server @@ -47,20 +47,20 @@ namespace MQTTnet.Tests.Server e.ReasonCode = MqttConnectReasonCode.Success; } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; testEnvironment.Server.ClientConnectedAsync += args => { serverConnectedClientId = args.ClientId; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; testEnvironment.Server.ClientDisconnectedAsync += args => { serverDisconnectedClientId = args.ClientId; disconnectedMre.Set(); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; // Arrange client @@ -68,7 +68,7 @@ namespace MQTTnet.Tests.Server client.ConnectedAsync += args => { clientAssignedClientId = args.ConnectResult.AssignedClientIdentifier; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; // Act diff --git a/Source/MQTTnet.Tests/Server/Events_Tests.cs b/Source/MQTTnet.Tests/Server/Events_Tests.cs index a8d23361..db453c64 100644 --- a/Source/MQTTnet.Tests/Server/Events_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Events_Tests.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; using MQTTnet.Formatter; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Protocol; using MQTTnet.Server; @@ -27,7 +27,7 @@ namespace MQTTnet.Tests.Server server.ClientConnectedAsync += e => { eventArgs = e; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await testEnvironment.ConnectClient(o => o.WithCredentials("TheUser")); @@ -54,7 +54,7 @@ namespace MQTTnet.Tests.Server server.ClientDisconnectedAsync += e => { eventArgs = e; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var client = await testEnvironment.ConnectClient(o => o.WithCredentials("TheUser")); @@ -81,7 +81,7 @@ namespace MQTTnet.Tests.Server server.ClientSubscribedTopicAsync += e => { eventArgs = e; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var client = await testEnvironment.ConnectClient(); @@ -108,7 +108,7 @@ namespace MQTTnet.Tests.Server server.ClientUnsubscribedTopicAsync += e => { eventArgs = e; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var client = await testEnvironment.ConnectClient(); @@ -134,7 +134,7 @@ namespace MQTTnet.Tests.Server server.InterceptingPublishAsync += e => { eventArgs = e; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var client = await testEnvironment.ConnectClient(); @@ -161,7 +161,7 @@ namespace MQTTnet.Tests.Server server.StartedAsync += e => { eventArgs = e; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await server.StartAsync(); @@ -183,7 +183,7 @@ namespace MQTTnet.Tests.Server server.StoppedAsync += e => { eventArgs = e; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await server.StopAsync(); diff --git a/Source/MQTTnet.Tests/Server/General.cs b/Source/MQTTnet.Tests/Server/General.cs index 148bf98c..eeb75fc6 100644 --- a/Source/MQTTnet.Tests/Server/General.cs +++ b/Source/MQTTnet.Tests/Server/General.cs @@ -11,8 +11,7 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Adapter; using MQTTnet.Client; -using MQTTnet.Formatter; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; using MQTTnet.Server; @@ -99,7 +98,7 @@ namespace MQTTnet.Tests.Server server.ValidatingConnectionAsync += e => { e.ReasonCode = MqttConnectReasonCode.NotAuthorized; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var connectingFailedException = await Assert.ThrowsExceptionAsync(() => testEnvironment.ConnectClient()); @@ -122,7 +121,7 @@ namespace MQTTnet.Tests.Server e.Response.ReasonCode = MqttSubscribeReasonCode.UnspecifiedError; } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; // Prepare some retained messages. @@ -146,7 +145,7 @@ namespace MQTTnet.Tests.Server buffer.Append(e.ApplicationMessage.Topic); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await client2.SubscribeAsync("y"); @@ -172,13 +171,13 @@ namespace MQTTnet.Tests.Server server.ClientConnectedAsync += e => { Interlocked.Increment(ref clientConnectedCalled); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; server.ClientDisconnectedAsync += e => { Interlocked.Increment(ref clientDisconnectedCalled); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()); @@ -225,7 +224,7 @@ namespace MQTTnet.Tests.Server // Clear retained message. await client.PublishAsync( new MqttApplicationMessageBuilder().WithTopic("r" + i2) - .WithPayload(PlatformAbstractionLayer.EmptyByteArray) + .WithPayload(EmptyBuffer.Array) .WithRetainFlag() .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) .Build()); @@ -276,7 +275,7 @@ namespace MQTTnet.Tests.Server { e.ApplicationMessage = new MqttApplicationMessage { Topic = "new_topic" }; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; string receivedTopic = null; @@ -286,7 +285,7 @@ namespace MQTTnet.Tests.Server c1.ApplicationMessageReceivedAsync += a => { receivedTopic = a.ApplicationMessage.Topic; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("original_topic").Build()); @@ -305,7 +304,7 @@ namespace MQTTnet.Tests.Server server.InterceptingPublishAsync += e => { e.ApplicationMessage.Payload = Encoding.ASCII.GetBytes("extended"); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var c1 = await testEnvironment.ConnectClient(); @@ -316,7 +315,7 @@ namespace MQTTnet.Tests.Server c2.ApplicationMessageReceivedAsync += e => { isIntercepted = string.Compare("extended", Encoding.UTF8.GetString(e.ApplicationMessage.Payload), StringComparison.Ordinal) == 0; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("test").Build()); @@ -339,7 +338,7 @@ namespace MQTTnet.Tests.Server server.ApplicationMessageNotConsumedAsync += e => { undeliverd = e.ApplicationMessage.Topic; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var client = await testEnvironment.ConnectClient(); @@ -376,7 +375,7 @@ namespace MQTTnet.Tests.Server receivedMessages.Add(e.ApplicationMessage); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await Task.Delay(500); @@ -400,7 +399,7 @@ namespace MQTTnet.Tests.Server s.RetainedMessageChangedAsync += e => { savedRetainedMessages = e.StoredRetainedMessages; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var c1 = await testEnvironment.ConnectClient(); @@ -440,7 +439,7 @@ namespace MQTTnet.Tests.Server c1.ApplicationMessageReceivedAsync += e => { receivedTopic = e.ApplicationMessage.Topic; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await c1.SubscribeAsync("#"); @@ -487,7 +486,7 @@ namespace MQTTnet.Tests.Server client.ApplicationMessageReceivedAsync += e => { Interlocked.Increment(ref receivedMessagesCount); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var message = new MqttApplicationMessageBuilder().WithTopic("a").WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce).Build(); @@ -521,13 +520,13 @@ namespace MQTTnet.Tests.Server c2.ApplicationMessageReceivedAsync += e => { Interlocked.Increment(ref receivedMessagesCount); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; c3.ApplicationMessageReceivedAsync += e => { Interlocked.Increment(ref receivedMessagesCount); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await c2.SubscribeAsync(new MqttTopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }).ConfigureAwait(false); @@ -581,7 +580,7 @@ namespace MQTTnet.Tests.Server events.Add("c"); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; server.ClientDisconnectedAsync += e => @@ -591,7 +590,7 @@ namespace MQTTnet.Tests.Server events.Add("d"); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var clientOptionsBuilder = new MqttClientOptionsBuilder().WithClientId(Guid.NewGuid().ToString()); @@ -621,7 +620,7 @@ namespace MQTTnet.Tests.Server events.Add("r"); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await c2.SubscribeAsync("topic"); @@ -669,7 +668,7 @@ namespace MQTTnet.Tests.Server server.ValidatingConnectionAsync += e => { ConnectionValidationHandler(e); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var events = new List(); @@ -681,7 +680,7 @@ namespace MQTTnet.Tests.Server events.Add("c"); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; server.ClientDisconnectedAsync += e => @@ -691,7 +690,7 @@ namespace MQTTnet.Tests.Server events.Add("d"); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var clientOptions = new MqttClientOptionsBuilder().WithClientId("same_id"); @@ -706,7 +705,7 @@ namespace MQTTnet.Tests.Server events.Add("x"); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; c1.ApplicationMessageReceivedAsync += e => @@ -716,7 +715,7 @@ namespace MQTTnet.Tests.Server events.Add("r"); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; c1.SubscribeAsync("topic").Wait(); @@ -785,7 +784,7 @@ namespace MQTTnet.Tests.Server client1.ApplicationMessageReceivedAsync += e => { receivedBody = e.ApplicationMessage.Payload; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await client1.SubscribeAsync("string"); @@ -822,7 +821,7 @@ namespace MQTTnet.Tests.Server receivedMessages.Add(e.ApplicationMessage); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await Task.Delay(500); @@ -850,7 +849,7 @@ namespace MQTTnet.Tests.Server c1.DisconnectedAsync += e => { disconnectCalled++; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await Task.Delay(100); @@ -950,7 +949,7 @@ namespace MQTTnet.Tests.Server testEnvironment.Server.ClientConnectedAsync += e => { testClientId = e.ClientId; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var client = testEnvironment.CreateClient(); @@ -959,12 +958,12 @@ namespace MQTTnet.Tests.Server { if (e.Buffer.Length > 0) { - if (e.Buffer[0] == (byte)Protocol.MqttControlPacketType.Disconnect << 4) + if (e.Buffer[0] == (byte)MqttControlPacketType.Disconnect << 4) { disconnectPacketReceived = true; } } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await client.ConnectAsync( diff --git a/Source/MQTTnet.Tests/Server/Load_Tests.cs b/Source/MQTTnet.Tests/Server/Load_Tests.cs index b4ddfe47..7cba9c12 100644 --- a/Source/MQTTnet.Tests/Server/Load_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Load_Tests.cs @@ -3,7 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; @@ -26,7 +26,7 @@ namespace MQTTnet.Tests.Server receiverClient.ApplicationMessageReceivedAsync += e => { Interlocked.Increment(ref receivedMessages); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await receiverClient.SubscribeAsync("t/+"); @@ -74,7 +74,7 @@ namespace MQTTnet.Tests.Server server.InterceptingPublishAsync += e => { Interlocked.Increment(ref receivedMessages); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; for (var i = 0; i < 100; i++) @@ -137,7 +137,7 @@ namespace MQTTnet.Tests.Server server.InterceptingPublishAsync += e => { Interlocked.Increment(ref receivedMessages); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; for (var i = 0; i < 100; i++) diff --git a/Source/MQTTnet.Tests/Server/QoS_Tests.cs b/Source/MQTTnet.Tests/Server/QoS_Tests.cs index 483c8d07..08bc6219 100644 --- a/Source/MQTTnet.Tests/Server/QoS_Tests.cs +++ b/Source/MQTTnet.Tests/Server/QoS_Tests.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Protocol; using MQTTnet.Server; @@ -26,7 +26,7 @@ namespace MQTTnet.Tests.Server server.ClientAcknowledgedPublishPacketAsync += args => { eventArgs = args; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var client1 = await testEnvironment.ConnectClient(); @@ -53,7 +53,7 @@ namespace MQTTnet.Tests.Server server.ClientAcknowledgedPublishPacketAsync += args => { eventArgs = args; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var client1 = await testEnvironment.ConnectClient(); @@ -88,7 +88,7 @@ namespace MQTTnet.Tests.Server eventArgs.Add(args); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var client1 = await testEnvironment.ConnectClient(); diff --git a/Source/MQTTnet.Tests/Server/Security_Tests.cs b/Source/MQTTnet.Tests/Server/Security_Tests.cs index 0e144df8..bc03e9c3 100644 --- a/Source/MQTTnet.Tests/Server/Security_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Security_Tests.cs @@ -8,7 +8,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Adapter; using MQTTnet.Client; using MQTTnet.Exceptions; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Protocol; namespace MQTTnet.Tests.Server @@ -33,7 +33,7 @@ namespace MQTTnet.Tests.Server publishedApplicationMessages.Add(eventArgs.ApplicationMessage); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; testEnvironment.Server.ValidatingConnectionAsync += eventArgs => @@ -47,7 +47,7 @@ namespace MQTTnet.Tests.Server eventArgs.ReasonCode = MqttConnectReasonCode.NotAuthorized; } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; using (var validClient = testEnvironment.CreateClient()) @@ -157,7 +157,7 @@ namespace MQTTnet.Tests.Server e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var client = testEnvironment.CreateClient(); diff --git a/Source/MQTTnet.Tests/Server/Server_Reference_Tests.cs b/Source/MQTTnet.Tests/Server/Server_Reference_Tests.cs index 19885d96..f8f76c18 100644 --- a/Source/MQTTnet.Tests/Server/Server_Reference_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Server_Reference_Tests.cs @@ -8,6 +8,7 @@ using MQTTnet.Adapter; using MQTTnet.Client; using MQTTnet.Formatter; using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Protocol; namespace MQTTnet.Tests.Server @@ -28,7 +29,7 @@ namespace MQTTnet.Tests.Server { e.ReasonCode = MqttConnectReasonCode.ServerMoved; e.ServerReference = "new_server"; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; try diff --git a/Source/MQTTnet.Tests/Server/Session_Tests.cs b/Source/MQTTnet.Tests/Server/Session_Tests.cs index 772b8484..d86d2e8d 100644 --- a/Source/MQTTnet.Tests/Server/Session_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Session_Tests.cs @@ -10,9 +10,8 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; using MQTTnet.Formatter; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Protocol; -using MQTTnet.Server; using MQTTnet.Tests.Mockups; namespace MQTTnet.Tests.Server @@ -29,7 +28,7 @@ namespace MQTTnet.Tests.Server await testEnvironment.StartServer(o => o.WithPersistentSessions()); - const string ClientId = "Client1"; + const string clientId = "Client1"; // Create client with clean session and long session expiry interval @@ -38,7 +37,7 @@ namespace MQTTnet.Tests.Server .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) .WithSessionExpiryInterval(9999) // not relevant for v311 but testing impact .WithCleanSession() // start and end with clean session - .WithClientId(ClientId) + .WithClientId(clientId) .Build()); // Disconnect; empty session should be removed from server @@ -57,7 +56,7 @@ namespace MQTTnet.Tests.Server .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) .WithSessionExpiryInterval(9999) // not relevant for v311 but testing impact .WithCleanSession(false) // see if there is a session - .WithClientId(ClientId) + .WithClientId(clientId) .Build(); @@ -83,7 +82,7 @@ namespace MQTTnet.Tests.Server server.SessionDeletedAsync += e => { deletedEventFired = true; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var client = await testEnvironment.ConnectClient(); @@ -111,7 +110,7 @@ namespace MQTTnet.Tests.Server e.SessionItems["can_subscribe_x"] = true; e.SessionItems["default_payload"] = "Hello World"; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await testEnvironment.ConnectClient(); @@ -123,6 +122,8 @@ namespace MQTTnet.Tests.Server } } + #if !NET452 + [TestMethod] [DataRow(MqttProtocolVersion.V311)] [DataRow(MqttProtocolVersion.V500)] @@ -134,7 +135,7 @@ namespace MQTTnet.Tests.Server await testEnvironment.StartServer(); - var options = new MqttClientOptionsBuilder().WithClientId("1").WithTimeout(TimeSpan.FromSeconds(1)).WithProtocolVersion(protocolVersion).WithKeepAlivePeriod(TimeSpan.FromSeconds(5)); + var options = new MqttClientOptionsBuilder().WithClientId("1").WithTimeout(TimeSpan.FromSeconds(1)).WithProtocolVersion(protocolVersion); var hasReceive = false; @@ -149,14 +150,14 @@ namespace MQTTnet.Tests.Server // Try to connect 50 clients at the same time. var clients = await Task.WhenAll(Enumerable.Range(0, 50).Select(i => ConnectAndSubscribe(testEnvironment, options, OnReceive))); + await Task.Delay(5000); + + var connectedClients = clients.Where(c => c?.TryPingAsync().GetAwaiter().GetResult() == true).ToList(); + await LongTestDelay(); - var connectedClients = clients.Where(c => c != null).Where(c => c.TryPingAsync().GetAwaiter().GetResult()).ToList(); - Assert.AreEqual(1, connectedClients.Count); - await Task.Delay(5000); - var option2 = new MqttClientOptionsBuilder().WithClientId("2").WithKeepAlivePeriod(TimeSpan.FromSeconds(10)); var sendClient = await testEnvironment.ConnectClient(option2); await sendClient.PublishStringAsync("aaa", "1"); @@ -166,6 +167,7 @@ namespace MQTTnet.Tests.Server Assert.AreEqual(true, hasReceive); } } +#endif [DataTestMethod] [DataRow(MqttQualityOfServiceLevel.ExactlyOnce)] @@ -186,7 +188,7 @@ namespace MQTTnet.Tests.Server c.AutoAcknowledge = false; ++count; Console.WriteLine("process"); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await subscriber.SubscribeAsync("#", qos); @@ -223,7 +225,7 @@ namespace MQTTnet.Tests.Server client1.DisconnectedAsync += c => { disconnectReason = c.Reason; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var client2 = await testEnvironment.ConnectClient(options); @@ -249,7 +251,7 @@ namespace MQTTnet.Tests.Server e.SessionItems["can_subscribe_x"] = true; e.SessionItems["default_payload"] = "Hello World"; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; server.InterceptingSubscriptionAsync += e => @@ -262,13 +264,13 @@ namespace MQTTnet.Tests.Server } } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; server.InterceptingPublishAsync += e => { e.ApplicationMessage.Payload = Encoding.UTF8.GetBytes(e.SessionItems["default_payload"] as string ?? string.Empty); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; string receivedPayload = null; @@ -277,7 +279,7 @@ namespace MQTTnet.Tests.Server client.ApplicationMessageReceivedAsync += e => { receivedPayload = e.ApplicationMessage.ConvertPayloadToString(); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var subscribeResult = await client.SubscribeAsync("x"); @@ -327,7 +329,7 @@ namespace MQTTnet.Tests.Server c1.ApplicationMessageReceivedAsync += e => { Interlocked.Increment(ref receivedMessagesCount); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); @@ -335,10 +337,10 @@ namespace MQTTnet.Tests.Server // C2 has the last will defined. var clientOptions = new MqttClientOptionsBuilder().WithWillTopic("My/last/will").WithClientId("WillOwner"); - var c2 = await testEnvironment.ConnectClient(clientOptions); + await testEnvironment.ConnectClient(clientOptions); // C3 will do the connection takeover. - var c3 = await testEnvironment.ConnectClient(clientOptions); + await testEnvironment.ConnectClient(clientOptions); await Task.Delay(1000); @@ -350,34 +352,22 @@ namespace MQTTnet.Tests.Server { try { - var sendClient = await testEnvironment.ConnectClient(options); + var client = await testEnvironment.ConnectClient(options).ConfigureAwait(false); - sendClient.ApplicationMessageReceivedAsync += e => + client.ApplicationMessageReceivedAsync += e => { onReceive(); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; - - using (var subscribeTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5))) + + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5))) { - await sendClient.SubscribeAsync("aaa", MqttQualityOfServiceLevel.AtMostOnce, subscribeTimeout.Token).ConfigureAwait(false); + await client.SubscribeAsync("aaa", MqttQualityOfServiceLevel.AtMostOnce, timeout.Token).ConfigureAwait(false); } - return sendClient; + return client; } - catch - { - return null; - } - } - - static async Task TryConnect(TestEnvironment testEnvironment, MqttClientOptionsBuilder options) - { - try - { - return await testEnvironment.ConnectClient(options); - } - catch + catch (Exception) { return null; } diff --git a/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs b/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs index 862c2202..b67adea9 100644 --- a/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; using MQTTnet.Formatter; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; using MQTTnet.Server; @@ -33,7 +33,7 @@ namespace MQTTnet.Tests.Server e.Response.ReasonCode = MqttSubscribeReasonCode.TopicFilterInvalid; } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var client = await testEnvironment.ConnectClient(); @@ -56,7 +56,7 @@ namespace MQTTnet.Tests.Server server.ClientSubscribedTopicAsync += e => { server.InjectApplicationMessage(new InjectedMqttApplicationMessage(new MqttApplicationMessageBuilder().WithTopic("test_topic").Build())); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var client = await testEnvironment.ConnectClient(); @@ -81,7 +81,7 @@ namespace MQTTnet.Tests.Server { // Set the topic to "a" regards what the client wants to subscribe. e.TopicFilter.Topic = "a"; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var topicAReceived = false; @@ -99,7 +99,7 @@ namespace MQTTnet.Tests.Server topicBReceived = true; } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await client.SubscribeAsync("b"); @@ -146,7 +146,7 @@ namespace MQTTnet.Tests.Server c1.ApplicationMessageReceivedAsync += e => { Interlocked.Increment(ref receivedMessagesCount); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; for (var i = 0; i < 500; i++) @@ -189,7 +189,7 @@ namespace MQTTnet.Tests.Server c1.ApplicationMessageReceivedAsync += e => { Interlocked.Increment(ref receivedMessagesCount); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var optionsBuilder = new MqttClientSubscribeOptionsBuilder(); @@ -229,7 +229,7 @@ namespace MQTTnet.Tests.Server c1.ApplicationMessageReceivedAsync += e => { Interlocked.Increment(ref receivedMessagesCount); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await c1.SubscribeAsync(new MqttClientSubscribeOptionsBuilder().WithTopicFilter("a").Build()); @@ -267,7 +267,7 @@ namespace MQTTnet.Tests.Server c1.ApplicationMessageReceivedAsync += e => { Interlocked.Increment(ref receivedMessagesCount); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await c1.SubscribeAsync(new MqttClientSubscribeOptionsBuilder().WithTopicFilter("a").WithTopicFilter("b").WithTopicFilter("c").Build()); @@ -301,7 +301,7 @@ namespace MQTTnet.Tests.Server c1.ApplicationMessageReceivedAsync += e => { Interlocked.Increment(ref receivedMessagesCount); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; var c2 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("c2")); @@ -316,7 +316,7 @@ namespace MQTTnet.Tests.Server server.ClientSubscribedTopicAsync += e => { subscribeEventCalled = e.TopicFilter.Topic == "a" && e.ClientId == c1.Options.ClientId; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await c1.SubscribeAsync(new MqttTopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); @@ -331,7 +331,7 @@ namespace MQTTnet.Tests.Server server.ClientUnsubscribedTopicAsync += e => { unsubscribeEventCalled = e.TopicFilter == "a" && e.ClientId == c1.Options.ClientId; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await c1.UnsubscribeAsync("a"); diff --git a/Source/MQTTnet.Tests/Server/Topic_Alias_Tests.cs b/Source/MQTTnet.Tests/Server/Topic_Alias_Tests.cs index b15c3b1a..a92b023e 100644 --- a/Source/MQTTnet.Tests/Server/Topic_Alias_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Topic_Alias_Tests.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; using MQTTnet.Formatter; -using MQTTnet.Implementations; +using MQTTnet.Internal; namespace MQTTnet.Tests.Server { @@ -50,7 +50,7 @@ namespace MQTTnet.Tests.Server receivedTopics.Add(e.ApplicationMessage.Topic); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await c1.SubscribeAsync("#"); diff --git a/Source/MQTTnet.Tests/Server/User_Properties_Tests.cs b/Source/MQTTnet.Tests/Server/User_Properties_Tests.cs index 27f3e72d..0bbc7030 100644 --- a/Source/MQTTnet.Tests/Server/User_Properties_Tests.cs +++ b/Source/MQTTnet.Tests/Server/User_Properties_Tests.cs @@ -9,6 +9,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; using MQTTnet.Formatter; using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Tests.Mockups; @@ -48,7 +49,7 @@ namespace MQTTnet.Tests.Server receiver.ApplicationMessageReceivedAsync += e => { receivedMessage = e.ApplicationMessage; - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await sender.PublishAsync(message.Build(), CancellationToken.None); diff --git a/Source/MQTTnet.Tests/Server/Will_Tests.cs b/Source/MQTTnet.Tests/Server/Will_Tests.cs index d464cd7e..12b24f63 100644 --- a/Source/MQTTnet.Tests/Server/Will_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Will_Tests.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; -using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Protocol; namespace MQTTnet.Tests.Server @@ -26,7 +26,7 @@ namespace MQTTnet.Tests.Server c1.ApplicationMessageReceivedAsync += e => { Interlocked.Increment(ref receivedMessagesCount); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); @@ -53,7 +53,7 @@ namespace MQTTnet.Tests.Server c1.ApplicationMessageReceivedAsync += e => { Interlocked.Increment(ref receivedMessagesCount); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; }; await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); diff --git a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs index 5af179f2..7a9db45d 100644 --- a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs +++ b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs @@ -195,7 +195,7 @@ namespace MQTTnet.Adapter // This lock makes sure that multiple threads can send packets at the same time. // This is required when a disconnect is sent from another thread while the // worker thread is still sending publish packets etc. - using (await _syncRoot.WaitAsync(cancellationToken).ConfigureAwait(false)) + using (await _syncRoot.EnterAsync(cancellationToken).ConfigureAwait(false)) { // Check for cancellation here again because "WaitAsync" might take some time. cancellationToken.ThrowIfCancellationRequested(); @@ -363,7 +363,7 @@ namespace MQTTnet.Adapter var fixedHeader = readFixedHeaderResult.FixedHeader; if (fixedHeader.RemainingLength == 0) { - return new ReceivedMqttPacket(fixedHeader.Flags, PlatformAbstractionLayer.EmptyByteArraySegment, 2); + return new ReceivedMqttPacket(fixedHeader.Flags, EmptyBuffer.ArraySegment, 2); } var bodyLength = fixedHeader.RemainingLength; diff --git a/Source/MQTTnet/Client/IMqttClient.cs b/Source/MQTTnet/Client/IMqttClient.cs index ada0ad0f..849bff12 100644 --- a/Source/MQTTnet/Client/IMqttClient.cs +++ b/Source/MQTTnet/Client/IMqttClient.cs @@ -8,6 +8,7 @@ namespace MQTTnet.Client public interface IMqttClient : IDisposable { event Func ApplicationMessageReceivedAsync; + event Func ConnectedAsync; event Func ConnectingAsync; diff --git a/Source/MQTTnet/Client/MqttClient.cs b/Source/MQTTnet/Client/MqttClient.cs index 00b0e3aa..92004fe8 100644 --- a/Source/MQTTnet/Client/MqttClient.cs +++ b/Source/MQTTnet/Client/MqttClient.cs @@ -9,7 +9,6 @@ using MQTTnet.Adapter; using MQTTnet.Diagnostics; using MQTTnet.Exceptions; using MQTTnet.Formatter; -using MQTTnet.Implementations; using MQTTnet.Internal; using MQTTnet.PacketDispatcher; using MQTTnet.Packets; @@ -224,17 +223,17 @@ namespace MQTTnet.Client public async Task PingAsync(CancellationToken cancellationToken = default) { - if (!cancellationToken.CanBeCanceled) + if (cancellationToken.CanBeCanceled) + { + await SendAndReceiveAsync(MqttPingReqPacket.Instance, cancellationToken).ConfigureAwait(false); + } + else { using (var timeout = new CancellationTokenSource(Options.Timeout)) { await SendAndReceiveAsync(MqttPingReqPacket.Instance, timeout.Token).ConfigureAwait(false); } } - else - { - await SendAndReceiveAsync(MqttPingReqPacket.Instance, cancellationToken).ConfigureAwait(false); - } } public Task PublishAsync(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken = default) @@ -369,7 +368,7 @@ namespace MQTTnet.Client throw new MqttProtocolViolationException("Received a not supported QoS level."); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; } async Task AuthenticateAsync(MqttClientOptions options, CancellationToken cancellationToken) @@ -490,7 +489,7 @@ namespace MQTTnet.Client return DisconnectCoreAsync(sender, exception, connectResult, clientWasConnected); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; } bool DisconnectIsPendingOrFinished() @@ -550,7 +549,7 @@ namespace MQTTnet.Client return extendedAuthenticationExchangeHandler.HandleRequestAsync(new MqttExtendedAuthenticationExchangeContext(authPacket, this)); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; } Task ProcessReceivedDisconnectPacket(MqttDisconnectPacket disconnectPacket) @@ -604,7 +603,7 @@ namespace MQTTnet.Client return SendAsync(pubRelPacket, cancellationToken); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; } Task ProcessReceivedPubRelPacket(MqttPubRelPacket pubRelPacket, CancellationToken cancellationToken) diff --git a/Source/MQTTnet/Formatter/MqttBufferReader.cs b/Source/MQTTnet/Formatter/MqttBufferReader.cs index 4c5cc36e..665186ab 100644 --- a/Source/MQTTnet/Formatter/MqttBufferReader.cs +++ b/Source/MQTTnet/Formatter/MqttBufferReader.cs @@ -6,13 +6,13 @@ using System; using System.Runtime.CompilerServices; using System.Text; using MQTTnet.Exceptions; -using MQTTnet.Implementations; +using MQTTnet.Internal; namespace MQTTnet.Formatter { public sealed class MqttBufferReader { - byte[] _buffer = PlatformAbstractionLayer.EmptyByteArray; + byte[] _buffer = EmptyBuffer.Array; int _initialPosition; int _length; int _position; @@ -29,7 +29,7 @@ namespace MQTTnet.Formatter if (length == 0) { - return PlatformAbstractionLayer.EmptyByteArray; + return EmptyBuffer.Array; } ValidateReceiveBuffer(length); @@ -66,7 +66,7 @@ namespace MQTTnet.Formatter if (bufferLength == 0) { - return PlatformAbstractionLayer.EmptyByteArray; + return EmptyBuffer.Array; } var buffer = new byte[bufferLength]; diff --git a/Source/MQTTnet/Formatter/MqttPacketBuffer.cs b/Source/MQTTnet/Formatter/MqttPacketBuffer.cs index 4fdd77ee..fb8692f5 100644 --- a/Source/MQTTnet/Formatter/MqttPacketBuffer.cs +++ b/Source/MQTTnet/Formatter/MqttPacketBuffer.cs @@ -5,12 +5,13 @@ using System; using System.Linq; using MQTTnet.Implementations; +using MQTTnet.Internal; namespace MQTTnet.Formatter { public readonly struct MqttPacketBuffer { - static readonly ArraySegment EmptyPayload = PlatformAbstractionLayer.EmptyByteArraySegment; + static readonly ArraySegment EmptyPayload = EmptyBuffer.ArraySegment; public MqttPacketBuffer(ArraySegment packet, ArraySegment payload) { diff --git a/Source/MQTTnet/Implementations/CrossPlatformSocket.cs b/Source/MQTTnet/Implementations/CrossPlatformSocket.cs index 1d14ffcd..f60bdab6 100644 --- a/Source/MQTTnet/Implementations/CrossPlatformSocket.cs +++ b/Source/MQTTnet/Implementations/CrossPlatformSocket.cs @@ -15,14 +15,20 @@ namespace MQTTnet.Implementations public sealed class CrossPlatformSocket : IDisposable { readonly Socket _socket; + +#if !NET5_0_OR_GREATER readonly Action _socketDisposeAction; +#endif NetworkStream _networkStream; public CrossPlatformSocket(AddressFamily addressFamily) { _socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp); + +#if !NET5_0_OR_GREATER _socketDisposeAction = _socket.Dispose; +#endif } public CrossPlatformSocket() @@ -31,17 +37,76 @@ namespace MQTTnet.Implementations // will make use of dual mode in the .net framework. _socket = new Socket(SocketType.Stream, ProtocolType.Tcp); +#if !NET5_0_OR_GREATER _socketDisposeAction = _socket.Dispose; +#endif } - public CrossPlatformSocket(Socket socket) + CrossPlatformSocket(Socket socket) { _socket = socket ?? throw new ArgumentNullException(nameof(socket)); _networkStream = new NetworkStream(socket, true); +#if !NET5_0_OR_GREATER _socketDisposeAction = _socket.Dispose; +#endif } + public bool DualMode + { + get => _socket.DualMode; + set => _socket.DualMode = value; + } + + public bool IsConnected => _socket.Connected; + + public bool KeepAlive + { + get => _socket.GetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive) as int? == 1; + set => _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, value ? 1 : 0); + } + + public int TcpKeepAliveInterval + { +#if NETCOREAPP3_0_OR_GREATER + get => _socket.GetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval) as int? ?? 0; + set => _socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, value); +#else + get { throw new NotSupportedException("TcpKeepAliveInterval requires at least net5.0."); } + set { throw new NotSupportedException("TcpKeepAliveInterval requires at least net5.0."); } +#endif + } + + public int TcpKeepAliveRetryCount + { +#if NETCOREAPP3_0_OR_GREATER + get => _socket.GetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount) as int? ?? 0; + set => _socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, value); +#else + get { throw new NotSupportedException("TcpKeepAliveRetryCount requires at least net5.0."); } + set { throw new NotSupportedException("TcpKeepAliveRetryCount requires at least net5.0."); } +#endif + } + + public int TcpKeepAliveTime + { +#if NETCOREAPP3_0_OR_GREATER + get => _socket.GetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime) as int? ?? 0; + set => _socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, value); +#else + get { throw new NotSupportedException("TcpKeepAliveTime requires at least net5.0."); } + set { throw new NotSupportedException("TcpKeepAliveTime requires at least net5.0."); } +#endif + } + + public LingerOption LingerState + { + get => _socket.LingerState; + set => _socket.LingerState = value; + } + + public EndPoint LocalEndPoint => _socket.LocalEndPoint; + public bool NoDelay { // We cannot use the _NoDelay_ property from the socket because there is an issue in .NET 4.5.2, 4.6. @@ -51,24 +116,20 @@ namespace MQTTnet.Implementations set => _socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, value ? 1 : 0); } - public LingerOption LingerState - { - get => _socket.LingerState; - set => _socket.LingerState = value; - } - - public bool DualMode - { - get => _socket.DualMode; - set => _socket.DualMode = value; - } - public int ReceiveBufferSize { get => _socket.ReceiveBufferSize; set => _socket.ReceiveBufferSize = value; } + public EndPoint RemoteEndPoint => _socket.RemoteEndPoint; + + public bool ReuseAddress + { + get => _socket.GetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress) as int? != 0; + set => _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, value ? 1 : 0); + } + public int SendBufferSize { get => _socket.SendBufferSize; @@ -81,16 +142,6 @@ namespace MQTTnet.Implementations set => _socket.SendTimeout = value; } - public EndPoint RemoteEndPoint => _socket.RemoteEndPoint; - - public bool ReuseAddress - { - get => (int)_socket.GetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress) != 0; - set => _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, value ? 1 : 0); - } - - public bool IsConnected => _socket.Connected; - public async Task AcceptAsync() { try @@ -109,8 +160,6 @@ namespace MQTTnet.Implementations } } - public EndPoint LocalEndPoint => _socket.LocalEndPoint; - public void Bind(EndPoint localEndPoint) { if (localEndPoint is null) @@ -121,11 +170,6 @@ namespace MQTTnet.Implementations _socket.Bind(localEndPoint); } - public void Listen(int connectionBacklog) - { - _socket.Listen(connectionBacklog); - } - public async Task ConnectAsync(string host, int port, CancellationToken cancellationToken) { if (host is null) @@ -137,11 +181,17 @@ namespace MQTTnet.Implementations try { - _networkStream?.Dispose(); - #if NET5_0_OR_GREATER + + if (_networkStream != null) + { + await _networkStream.DisposeAsync().ConfigureAwait(false); + } + await _socket.ConnectAsync(host, port, cancellationToken).ConfigureAwait(false); #else + _networkStream?.Dispose(); + // Workaround for: https://github.com/dotnet/corefx/issues/24430 using (cancellationToken.Register(_socketDisposeAction)) { @@ -175,20 +225,26 @@ namespace MQTTnet.Implementations } } - public async Task SendAsync(ArraySegment buffer, SocketFlags socketFlags) + public void Dispose() { - try + _networkStream?.Dispose(); + _socket?.Dispose(); + } + + public NetworkStream GetStream() + { + var networkStream = _networkStream; + if (networkStream == null) { -#if NET452 || NET461 - await Task.Factory.FromAsync(SocketWrapper.BeginSend, _socket.EndSend, new SocketWrapper(_socket, buffer, socketFlags)).ConfigureAwait(false); -#else - await _socket.SendAsync(buffer, socketFlags).ConfigureAwait(false); -#endif - } - catch (ObjectDisposedException) - { - // This will happen when _socket.EndConnect gets called by Task library but the socket is already disposed. + throw new IOException("The socket is not connected."); } + + return networkStream; + } + + public void Listen(int connectionBacklog) + { + _socket.Listen(connectionBacklog); } public async Task ReceiveAsync(ArraySegment buffer, SocketFlags socketFlags) @@ -208,28 +264,27 @@ namespace MQTTnet.Implementations } } - public NetworkStream GetStream() + public async Task SendAsync(ArraySegment buffer, SocketFlags socketFlags) { - var networkStream = _networkStream; - if (networkStream == null) + try { - throw new IOException("The socket is not connected."); +#if NET452 || NET461 + await Task.Factory.FromAsync(SocketWrapper.BeginSend, _socket.EndSend, new SocketWrapper(_socket, buffer, socketFlags)).ConfigureAwait(false); +#else + await _socket.SendAsync(buffer, socketFlags).ConfigureAwait(false); +#endif + } + catch (ObjectDisposedException) + { + // This will happen when _socket.EndConnect gets called by Task library but the socket is already disposed. } - - return networkStream; - } - - public void Dispose() - { - _networkStream?.Dispose(); - _socket?.Dispose(); } #if NET452 || NET461 sealed class SocketWrapper { - readonly Socket _socket; readonly ArraySegment _buffer; + readonly Socket _socket; readonly SocketFlags _socketFlags; public SocketWrapper(Socket socket, ArraySegment buffer, SocketFlags socketFlags) @@ -239,16 +294,28 @@ namespace MQTTnet.Implementations _socketFlags = socketFlags; } - public static IAsyncResult BeginSend(AsyncCallback callback, object state) - { - var socketWrapper = (SocketWrapper)state; - return socketWrapper._socket.BeginSend(socketWrapper._buffer.Array, socketWrapper._buffer.Offset, socketWrapper._buffer.Count, socketWrapper._socketFlags, callback, state); - } - public static IAsyncResult BeginReceive(AsyncCallback callback, object state) { var socketWrapper = (SocketWrapper)state; - return socketWrapper._socket.BeginReceive(socketWrapper._buffer.Array, socketWrapper._buffer.Offset, socketWrapper._buffer.Count, socketWrapper._socketFlags, callback, state); + return socketWrapper._socket.BeginReceive( + socketWrapper._buffer.Array, + socketWrapper._buffer.Offset, + socketWrapper._buffer.Count, + socketWrapper._socketFlags, + callback, + state); + } + + public static IAsyncResult BeginSend(AsyncCallback callback, object state) + { + var socketWrapper = (SocketWrapper)state; + return socketWrapper._socket.BeginSend( + socketWrapper._buffer.Array, + socketWrapper._buffer.Offset, + socketWrapper._buffer.Count, + socketWrapper._socketFlags, + callback, + state); } } #endif diff --git a/Source/MQTTnet/Implementations/MqttTcpChannel.cs b/Source/MQTTnet/Implementations/MqttTcpChannel.cs index ad2c58bf..c1e0b316 100644 --- a/Source/MQTTnet/Implementations/MqttTcpChannel.cs +++ b/Source/MQTTnet/Implementations/MqttTcpChannel.cs @@ -14,6 +14,7 @@ using System.Runtime.ExceptionServices; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; +using MQTTnet.Internal; namespace MQTTnet.Implementations { @@ -154,7 +155,7 @@ namespace MQTTnet.Implementations public Task DisconnectAsync(CancellationToken cancellationToken) { Dispose(); - return Task.FromResult(0); + return CompletedTask.Instance; } public void Dispose() diff --git a/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs index ba1ea40d..ac94261d 100644 --- a/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs +++ b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs @@ -13,6 +13,7 @@ using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; +using MQTTnet.Internal; namespace MQTTnet.Implementations { @@ -56,13 +57,13 @@ namespace MQTTnet.Implementations RegisterListeners(options.TlsEndpointOptions, tlsCertificate, logger, _cancellationTokenSource.Token); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; } public Task StopAsync() { Cleanup(); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; } public void Dispose() @@ -124,7 +125,7 @@ namespace MQTTnet.Implementations var clientHandler = ClientHandler; if (clientHandler == null) { - return Task.FromResult(0); + return CompletedTask.Instance; } return clientHandler(channelAdapter); diff --git a/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs b/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs index e39d545e..90e3666e 100644 --- a/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs +++ b/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs @@ -117,7 +117,7 @@ namespace MQTTnet.Implementations return; } - using (await _sendLock.WaitAsync(cancellationToken).ConfigureAwait(false)) + using (await _sendLock.EnterAsync(cancellationToken).ConfigureAwait(false)) { await _webSocket.SendAsync(buffer, WebSocketMessageType.Binary, isEndOfPacket, cancellationToken).ConfigureAwait(false); } diff --git a/Source/MQTTnet/Implementations/PlatformAbstractionLayer.cs b/Source/MQTTnet/Implementations/PlatformAbstractionLayer.cs deleted file mode 100644 index d8803349..00000000 --- a/Source/MQTTnet/Implementations/PlatformAbstractionLayer.cs +++ /dev/null @@ -1,43 +0,0 @@ -// 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 System.Threading; -using System.Threading.Tasks; - -namespace MQTTnet.Implementations -{ - public static class PlatformAbstractionLayer - { -#if NET452 - public static Task CompletedTask { get; } = Task.FromResult(0); - - public static byte[] EmptyByteArray { get; } = new byte[0]; -#else - public static Task CompletedTask { get; } = Task.CompletedTask; - - public static byte[] EmptyByteArray { get; } = Array.Empty(); -#endif - - public static ArraySegment EmptyByteArraySegment { get; } = new ArraySegment(EmptyByteArray); - - public static void Sleep(TimeSpan timeout) - { -#if !NETSTANDARD1_3 && !WINDOWS_UWP - try - { - System.Threading.Thread.Sleep(timeout); - } - catch (ThreadAbortException) - { - // The ThreadAbortException is not actively catched in this project. - // So we use a one which is similar and will be catched properly. - throw new OperationCanceledException(); - } -#else - Task.Delay(timeout).Wait(); -#endif - } - } -} diff --git a/Source/MQTTnet/Internal/AsyncEvent.cs b/Source/MQTTnet/Internal/AsyncEvent.cs index cd67b241..8e988d75 100644 --- a/Source/MQTTnet/Internal/AsyncEvent.cs +++ b/Source/MQTTnet/Internal/AsyncEvent.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using MQTTnet.Diagnostics; @@ -37,7 +36,7 @@ namespace MQTTnet.Internal _handlers.Add(new AsyncEventInvocator(null, handler)); HasHandlers = true; - _handlersForInvoke = new List>(_handlers.ToList()); + _handlersForInvoke = new List>(_handlers); } } @@ -53,7 +52,7 @@ namespace MQTTnet.Internal _handlers.Add(new AsyncEventInvocator(handler, null)); HasHandlers = true; - _handlersForInvoke = new List>(_handlers.ToList()); + _handlersForInvoke = new List>(_handlers); } } @@ -86,7 +85,7 @@ namespace MQTTnet.Internal _handlers.RemoveAll(h => h.WrapsHandler(handler)); HasHandlers = _handlers.Count > 0; - _handlersForInvoke = new List>(_handlers.ToList()); + _handlersForInvoke = new List>(_handlers); } } @@ -102,7 +101,7 @@ namespace MQTTnet.Internal _handlers.RemoveAll(h => h.WrapsHandler(handler)); HasHandlers = _handlers.Count > 0; - _handlersForInvoke = new List>(_handlers.ToList()); + _handlersForInvoke = new List>(_handlers); } } diff --git a/Source/MQTTnet/Internal/AsyncEventInvocator.cs b/Source/MQTTnet/Internal/AsyncEventInvocator.cs index 6258b28b..f344cb1e 100644 --- a/Source/MQTTnet/Internal/AsyncEventInvocator.cs +++ b/Source/MQTTnet/Internal/AsyncEventInvocator.cs @@ -36,7 +36,7 @@ namespace MQTTnet.Internal if (_handler != null) { _handler.Invoke(eventArgs); - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; } if (_asyncHandler != null) diff --git a/Source/MQTTnet/Internal/AsyncLock.cs b/Source/MQTTnet/Internal/AsyncLock.cs index 17384ef3..580d570d 100644 --- a/Source/MQTTnet/Internal/AsyncLock.cs +++ b/Source/MQTTnet/Internal/AsyncLock.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -12,173 +12,151 @@ namespace MQTTnet.Internal { public sealed class AsyncLock : IDisposable { - /* - * This async supporting lock does not support reentrancy! - */ - - readonly List _queuedTasks = new List(64); - readonly Task _releaserTaskWithDirectApproval; - - readonly Releaser _releaserWithDirectApproval; - + readonly Task _completedTask; + readonly IDisposable _releaser; readonly object _syncRoot = new object(); + readonly Queue _waiters = new Queue(64); + + volatile bool _isDisposed; + bool _isLocked; - bool _isDisposed; - public AsyncLock() { - _releaserWithDirectApproval = new Releaser(this, null, CancellationToken.None); - _releaserTaskWithDirectApproval = Task.FromResult((IDisposable)_releaserWithDirectApproval); + _releaser = new Releaser(this); + _completedTask = Task.FromResult(_releaser); } public void Dispose() { lock (_syncRoot) { - foreach (var waitingTask in _queuedTasks) - { - waitingTask.Fail(new ObjectDisposedException(nameof(AsyncLock))); - } - - _queuedTasks.Clear(); - _isDisposed = true; + + while (_waiters.Any()) + { + _waiters.Dequeue().Dispose(); + } } } - public Task WaitAsync(CancellationToken cancellationToken) + public Task EnterAsync(CancellationToken cancellationToken = default) { - var hasDirectApproval = false; - Releaser releaser; + cancellationToken.ThrowIfCancellationRequested(); + + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(AsyncLock)); + } lock (_syncRoot) { - if (_isDisposed) + if (!_isLocked) { - throw new ObjectDisposedException(nameof(AsyncLock)); - } - - if (_queuedTasks.Count == 0) - { - // There is no other waiting task apart from the current one. - // So we can approve the current task directly. - releaser = _releaserWithDirectApproval; - hasDirectApproval = true; - Debug.WriteLine("AsyncLock: Task -1 directly approved."); - } - else - { - releaser = new Releaser(this, new TaskCompletionSource(), cancellationToken); + _isLocked = true; + return _completedTask; } - _queuedTasks.Add(releaser); + var waiter = new AsyncLockWaiter(cancellationToken); + _waiters.Enqueue(waiter); + + return waiter.Task; } - - if (!hasDirectApproval) - { - return releaser.Task; - } - - return _releaserTaskWithDirectApproval; } - void Release(Releaser releaser) + void Release() { lock (_syncRoot) { if (_isDisposed) { - // There is no much left to do! + // All waiters have been canceled with a ObjectDisposedException. + // So there is nothing left to do. return; } - var activeTask = _queuedTasks[0]; - if (!ReferenceEquals(activeTask, releaser)) - { - throw new InvalidOperationException("The active task must be the current releaser."); - } + // Assume that there is no waiter left first. + _isLocked = false; - _queuedTasks.RemoveAt(0); - - while (_queuedTasks.Count > 0) + // Try to find the next waiter which can be approved. + // Some of them might be canceled already so it is not + // guaranteed that the very next waiter is the correct one. + while (_waiters.Any()) { - var nextTask = _queuedTasks[0]; - if (!nextTask.IsPending) + var waiter = _waiters.Dequeue(); + var isApproved = waiter.Approve(_releaser); + waiter.Dispose(); + + if (isApproved) { - // Dequeue all canceled or failed tasks. - _queuedTasks.RemoveAt(0); - continue; + _isLocked = true; + return; } - - nextTask.Approve(); - return; } - - Debug.WriteLine("AsyncLock: No Task pending."); } } - sealed class Releaser : IDisposable + sealed class AsyncLockWaiter : IDisposable { - readonly AsyncLock _asyncLock; - readonly CancellationToken _cancellationToken; - readonly int _id; - readonly TaskCompletionSource _promise; + readonly CancellationTokenRegistration _cancellationRegistration; + readonly bool _hasCancellationRegistration; + readonly AsyncTaskCompletionSource _promise = new AsyncTaskCompletionSource(); - // ReSharper disable once FieldCanBeMadeReadOnly.Local - CancellationTokenRegistration _cancellationTokenRegistration; - - internal Releaser(AsyncLock asyncLock, TaskCompletionSource promise, CancellationToken cancellationToken) + public AsyncLockWaiter(CancellationToken cancellationToken) { - _asyncLock = asyncLock ?? throw new ArgumentNullException(nameof(asyncLock)); - _promise = promise; - _cancellationToken = cancellationToken; - + cancellationToken.ThrowIfCancellationRequested(); + if (cancellationToken.CanBeCanceled) { - _cancellationTokenRegistration = cancellationToken.Register(Cancel); + _cancellationRegistration = cancellationToken.Register(Cancel); + _hasCancellationRegistration = true; } - - _id = promise?.Task.Id ?? -1; - - Debug.WriteLine($"AsyncLock: Task {_id} queued."); } - public bool IsPending => _promise != null && !_promise.Task.IsCanceled && !_promise.Task.IsFaulted && !_promise.Task.IsCompleted; + public Task Task => _promise.Task; - public Task Task => _promise?.Task; - - public void Approve() + public bool Approve(IDisposable scope) { - _promise?.TrySetResult(this); + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } - Debug.WriteLine($"AsyncLock: Task {_id} approved."); + if (_promise.Task.IsCompleted) + { + return false; + } + + return _promise.TrySetResult(scope); } public void Dispose() { - if (_cancellationToken.CanBeCanceled) + if (_hasCancellationRegistration) { - _cancellationTokenRegistration.Dispose(); + _cancellationRegistration.Dispose(); } - Debug.WriteLine($"AsyncLock: Task {_id} completed."); - - _asyncLock.Release(this); - } - - public void Fail(Exception exception) - { - _promise?.TrySetException(exception); - - Debug.WriteLine($"AsyncLock: Task {_id} failed ({exception.GetType().Name})."); + _promise.TrySetException(new ObjectDisposedException(nameof(AsyncLockWaiter))); } void Cancel() { - _promise?.TrySetCanceled(); + _promise.TrySetCanceled(); + } + } - Debug.WriteLine($"AsyncLock: Task {_id} canceled."); + readonly struct Releaser : IDisposable + { + readonly AsyncLock _asyncLock; + + public Releaser(AsyncLock asyncLock) + { + _asyncLock = asyncLock ?? throw new ArgumentNullException(nameof(asyncLock)); + } + + public void Dispose() + { + _asyncLock.Release(); } } } diff --git a/Source/MQTTnet/Internal/AsyncTaskCompletionSource.cs b/Source/MQTTnet/Internal/AsyncTaskCompletionSource.cs index a77b56d9..d96c2b5f 100644 --- a/Source/MQTTnet/Internal/AsyncTaskCompletionSource.cs +++ b/Source/MQTTnet/Internal/AsyncTaskCompletionSource.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Threading; using System.Threading.Tasks; namespace MQTTnet.Internal @@ -22,7 +23,7 @@ namespace MQTTnet.Internal public Task Task => _taskCompletionSource.Task; - public bool TrySetCanceled() + public void TrySetCanceled() { #if NET452 // To prevent deadlocks it is required to call the _TrySetCanceled_ method @@ -31,19 +32,14 @@ namespace MQTTnet.Internal // do it. But _this_ thread is also reading incoming packets -> deadlock. // NET452 does not support RunContinuationsAsynchronously System.Threading.Tasks.Task.Run(() => _taskCompletionSource.TrySetCanceled()); - return true; + SpinWait.SpinUntil(() => _taskCompletionSource.Task.IsCompleted); #else - return _taskCompletionSource.TrySetCanceled(); + _taskCompletionSource.TrySetCanceled(); #endif } - public bool TrySetException(Exception exception) + public void TrySetException(Exception exception) { - if (exception == null) - { - throw new ArgumentNullException(nameof(exception)); - } - #if NET452 // To prevent deadlocks it is required to call the _TrySetException_ method // from a new thread because the awaiting code will not(!) be executed in @@ -51,9 +47,9 @@ namespace MQTTnet.Internal // do it. But _this_ thread is also reading incoming packets -> deadlock. // NET452 does not support RunContinuationsAsynchronously System.Threading.Tasks.Task.Run(() => _taskCompletionSource.TrySetException(exception)); - return true; + SpinWait.SpinUntil(() => _taskCompletionSource.Task.IsCompleted); #else - return _taskCompletionSource.TrySetException(exception); + _taskCompletionSource.TrySetException(exception); #endif } @@ -65,7 +61,14 @@ namespace MQTTnet.Internal // a new thread automatically (due to await). Furthermore _this_ thread will // do it. But _this_ thread is also reading incoming packets -> deadlock. // NET452 does not support RunContinuationsAsynchronously + if (_taskCompletionSource.Task.IsCompleted) + { + return false; + } + System.Threading.Tasks.Task.Run(() => _taskCompletionSource.TrySetResult(result)); + SpinWait.SpinUntil(() => _taskCompletionSource.Task.IsCompleted); + return true; #else return _taskCompletionSource.TrySetResult(result); diff --git a/Source/MQTTnet/Internal/CompletedTask.cs b/Source/MQTTnet/Internal/CompletedTask.cs new file mode 100644 index 00000000..1f050842 --- /dev/null +++ b/Source/MQTTnet/Internal/CompletedTask.cs @@ -0,0 +1,17 @@ +// 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; + +namespace MQTTnet.Internal +{ + public static class CompletedTask + { +#if NET452 + public static readonly Task Instance = Task.FromResult(true); +#else + public static readonly Task Instance = Task.CompletedTask; +#endif + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Internal/EmptyBuffer.cs b/Source/MQTTnet/Internal/EmptyBuffer.cs new file mode 100644 index 00000000..5992a741 --- /dev/null +++ b/Source/MQTTnet/Internal/EmptyBuffer.cs @@ -0,0 +1,19 @@ +// 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; + +namespace MQTTnet.Internal +{ + public static class EmptyBuffer + { +#if NET452 + public static readonly byte[] Array = new byte[0]; +#else + public static readonly byte[] Array = System.Array.Empty(); +#endif + + public static readonly ArraySegment ArraySegment = new ArraySegment(Array, 0, 0); + } +} \ No newline at end of file diff --git a/Source/MQTTnet/MQTTnet.csproj b/Source/MQTTnet/MQTTnet.csproj index 35b1536f..c8bf7f0a 100644 --- a/Source/MQTTnet/MQTTnet.csproj +++ b/Source/MQTTnet/MQTTnet.csproj @@ -14,7 +14,6 @@ $(TargetFrameworks);net452;net461 $(TargetFrameworks);uap10.0 7.3 - MQTTnet MQTTnet True diff --git a/Source/MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs b/Source/MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs index eaa916e1..35b70369 100644 --- a/Source/MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs +++ b/Source/MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs @@ -10,6 +10,7 @@ using System.Text; using MQTTnet.Adapter; using MQTTnet.Formatter; using MQTTnet.Implementations; +using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; @@ -88,7 +89,7 @@ namespace MQTTnet.Server /// public uint MaximumPacketSize => _connectPacket.MaximumPacketSize; - public string Password => Encoding.UTF8.GetString(RawPassword ?? PlatformAbstractionLayer.EmptyByteArray); + public string Password => Encoding.UTF8.GetString(RawPassword ?? EmptyBuffer.Array); public MqttProtocolVersion ProtocolVersion => ChannelAdapter.PacketFormatterAdapter.ProtocolVersion; diff --git a/Source/MQTTnet/Server/Internal/MqttClient.cs b/Source/MQTTnet/Server/Internal/MqttClient.cs index 18c3804a..4ae2d40d 100644 --- a/Source/MQTTnet/Server/Internal/MqttClient.cs +++ b/Source/MQTTnet/Server/Internal/MqttClient.cs @@ -181,7 +181,7 @@ namespace MQTTnet.Server return _eventContainer.ClientAcknowledgedPublishPacketEvent.TryInvokeAsync(eventArgs, _logger); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; } Task HandleIncomingPubAckPacket(MqttPubAckPacket pubAckPacket) @@ -193,7 +193,7 @@ namespace MQTTnet.Server return ClientAcknowledgedPublishPacket(acknowledgedPublishPacket, pubAckPacket); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; } Task HandleIncomingPubCompPacket(MqttPubCompPacket pubCompPacket) @@ -205,7 +205,7 @@ namespace MQTTnet.Server return ClientAcknowledgedPublishPacket(acknowledgedPublishPacket, pubCompPacket); } - return PlatformAbstractionLayer.CompletedTask; + return CompletedTask.Instance; } async Task HandleIncomingPublishPacket(MqttPublishPacket publishPacket, CancellationToken cancellationToken) diff --git a/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs b/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs index 8a2c2364..308e9416 100644 --- a/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs +++ b/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs @@ -306,6 +306,9 @@ namespace MQTTnet.Server await client.RunAsync().ConfigureAwait(false); } + catch (ObjectDisposedException) + { + } catch (OperationCanceledException) { } @@ -479,7 +482,7 @@ namespace MQTTnet.Server sessionShouldPersist = !connectPacket.CleanSession; } - using (await _createConnectionSyncRoot.WaitAsync(CancellationToken.None).ConfigureAwait(false)) + using (await _createConnectionSyncRoot.EnterAsync().ConfigureAwait(false)) { MqttSession session; lock (_sessionsManagementLock) @@ -537,6 +540,7 @@ namespace MQTTnet.Server MqttClientDisconnectType.Takeover, existingClient.Endpoint, existingClient.Session.Items); + await _eventContainer.ClientDisconnectedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); } } diff --git a/Source/MQTTnet/Server/Internal/MqttRetainedMessagesManager.cs b/Source/MQTTnet/Server/Internal/MqttRetainedMessagesManager.cs index acc4672d..a9b86a7a 100644 --- a/Source/MQTTnet/Server/Internal/MqttRetainedMessagesManager.cs +++ b/Source/MQTTnet/Server/Internal/MqttRetainedMessagesManager.cs @@ -5,10 +5,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; using MQTTnet.Diagnostics; -using MQTTnet.Implementations; using MQTTnet.Internal; namespace MQTTnet.Server @@ -85,7 +83,7 @@ namespace MQTTnet.Server } else { - if (existingMessage.QualityOfServiceLevel != applicationMessage.QualityOfServiceLevel || !existingMessage.Payload.SequenceEqual(applicationMessage.Payload ?? PlatformAbstractionLayer.EmptyByteArray)) + if (existingMessage.QualityOfServiceLevel != applicationMessage.QualityOfServiceLevel || !existingMessage.Payload.SequenceEqual(applicationMessage.Payload ?? EmptyBuffer.Array)) { _messages[applicationMessage.Topic] = applicationMessage; saveIsRequired = true; @@ -103,7 +101,7 @@ namespace MQTTnet.Server if (saveIsRequired) { - using (await _storageAccessLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) + using (await _storageAccessLock.EnterAsync().ConfigureAwait(false)) { var eventArgs = new RetainedMessageChangedEventArgs(clientId, applicationMessage, messagesForSave); await _eventContainer.RetainedMessageChangedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); @@ -132,7 +130,7 @@ namespace MQTTnet.Server _messages.Clear(); } - using (await _storageAccessLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) + using (await _storageAccessLock.EnterAsync().ConfigureAwait(false)) { await _eventContainer.RetainedMessagesClearedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false); } diff --git a/Source/MQTTnet/Server/Internal/MqttServerKeepAliveMonitor.cs b/Source/MQTTnet/Server/Internal/MqttServerKeepAliveMonitor.cs index de2bd442..3b193446 100644 --- a/Source/MQTTnet/Server/Internal/MqttServerKeepAliveMonitor.cs +++ b/Source/MQTTnet/Server/Internal/MqttServerKeepAliveMonitor.cs @@ -6,7 +6,6 @@ using System; using System.Threading; using System.Threading.Tasks; using MQTTnet.Diagnostics; -using MQTTnet.Implementations; using MQTTnet.Internal; using MQTTnet.Protocol; @@ -14,16 +13,20 @@ namespace MQTTnet.Server { public sealed class MqttServerKeepAliveMonitor { + readonly MqttNetSourceLogger _logger; readonly MqttServerOptions _options; readonly MqttClientSessionsManager _sessionsManager; - readonly MqttNetSourceLogger _logger; public MqttServerKeepAliveMonitor(MqttServerOptions options, MqttClientSessionsManager sessionsManager, IMqttNetLogger logger) { _options = options ?? throw new ArgumentNullException(nameof(options)); _sessionsManager = sessionsManager ?? throw new ArgumentNullException(nameof(sessionsManager)); - - if (logger == null) throw new ArgumentNullException(nameof(logger)); + + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + _logger = logger.WithSource(nameof(MqttServerKeepAliveMonitor)); } @@ -45,7 +48,7 @@ namespace MQTTnet.Server while (!cancellationToken.IsCancellationRequested) { TryProcessClients(); - PlatformAbstractionLayer.Sleep(_options.KeepAliveMonitorInterval); + Sleep(_options.KeepAliveMonitorInterval); } } catch (OperationCanceledException) @@ -61,13 +64,22 @@ namespace MQTTnet.Server } } - void TryProcessClients() + static void Sleep(TimeSpan timeout) { - var now = DateTime.UtcNow; - foreach (var client in _sessionsManager.GetClients()) +#if !NETSTANDARD1_3 && !WINDOWS_UWP + try { - TryProcessClient(client, now); + Thread.Sleep(timeout); } + catch (ThreadAbortException) + { + // The ThreadAbortException is not actively catched in this project. + // So we use a one which is similar and will be catched properly. + throw new OperationCanceledException(); + } +#else + Task.Delay(timeout).Wait(); +#endif } void TryProcessClient(MqttClient connection, DateTime now) @@ -119,5 +131,14 @@ namespace MQTTnet.Server _logger.Error(exception, "Client {0}: Unhandled exception while checking keep alive timeouts.", connection.Id); } } + + void TryProcessClients() + { + var now = DateTime.UtcNow; + foreach (var client in _sessionsManager.GetClients()) + { + TryProcessClient(client, now); + } + } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/MqttServer.cs b/Source/MQTTnet/Server/MqttServer.cs index 85b73354..2c5969e7 100644 --- a/Source/MQTTnet/Server/MqttServer.cs +++ b/Source/MQTTnet/Server/MqttServer.cs @@ -11,7 +11,6 @@ using System.Threading; using System.Threading.Tasks; using MQTTnet.Adapter; using MQTTnet.Diagnostics; -using MQTTnet.Implementations; using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; @@ -172,7 +171,7 @@ namespace MQTTnet.Server { ThrowIfNotStarted(); - return _retainedMessagesManager?.ClearMessages() ?? PlatformAbstractionLayer.CompletedTask; + return _retainedMessagesManager?.ClearMessages() ?? CompletedTask.Instance; } public Task DisconnectClientAsync(string id, MqttDisconnectReasonCode reasonCode) diff --git a/Source/MQTTnet/Server/Status/MqttSessionStatus.cs b/Source/MQTTnet/Server/Status/MqttSessionStatus.cs index e9319395..6cea318b 100644 --- a/Source/MQTTnet/Server/Status/MqttSessionStatus.cs +++ b/Source/MQTTnet/Server/Status/MqttSessionStatus.cs @@ -6,7 +6,6 @@ using System; using System.Collections; using System.Threading.Tasks; using MQTTnet.Formatter; -using MQTTnet.Implementations; using MQTTnet.Internal; namespace MQTTnet.Server @@ -34,8 +33,8 @@ namespace MQTTnet.Server var publishPacketFactory = new MqttPublishPacketFactory(); _session.EnqueueDataPacket(new MqttPacketBusItem(publishPacketFactory.Create(applicationMessage))); - - return PlatformAbstractionLayer.CompletedTask; + + return CompletedTask.Instance; } public Task DeliverApplicationMessageAsync(MqttApplicationMessage applicationMessage) From 8cf5537043b42050abe791fd18b1f9493a910d7c Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sun, 23 Oct 2022 08:45:16 +0200 Subject: [PATCH 07/24] Improve unit tests --- .../Client/ManagedMqttClient_Tests.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Source/MQTTnet.Tests/Client/ManagedMqttClient_Tests.cs b/Source/MQTTnet.Tests/Client/ManagedMqttClient_Tests.cs index e161b212..554cc663 100644 --- a/Source/MQTTnet.Tests/Client/ManagedMqttClient_Tests.cs +++ b/Source/MQTTnet.Tests/Client/ManagedMqttClient_Tests.cs @@ -279,7 +279,7 @@ namespace MQTTnet.Tests.Client }); // Wait a bit for the retained message to be available - await Task.Delay(1000); + await LongTestDelay(); await sendingClient.DisconnectAsync(); @@ -288,7 +288,8 @@ namespace MQTTnet.Tests.Client var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort); var receivedManagedMessages = new List(); - var managedClient = new ManagedMqttClient(testEnvironment.CreateClient(), new MqttNetEventLogger()); + + var managedClient = testEnvironment.Factory.CreateManagedMqttClient(testEnvironment.CreateClient()); managedClient.ApplicationMessageReceivedAsync += e => { receivedManagedMessages.Add(e.ApplicationMessage); @@ -299,17 +300,17 @@ namespace MQTTnet.Tests.Client await managedClient.StartAsync(new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).WithAutoReconnectDelay(TimeSpan.FromSeconds(1)).Build()); - await Task.Delay(1000); + await LongTestDelay(); Assert.AreEqual(1, receivedManagedMessages.Count); await managedClient.StopAsync(); - await Task.Delay(500); + await LongTestDelay(); await managedClient.StartAsync(new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).WithAutoReconnectDelay(TimeSpan.FromSeconds(1)).Build()); - await Task.Delay(1000); + await LongTestDelay(); // After reconnect and then some delay, the retained message must not be received, // showing that the subscriptions were cleared @@ -317,7 +318,9 @@ namespace MQTTnet.Tests.Client // Make sure that it gets received after subscribing again. await managedClient.SubscribeAsync("topic"); - await Task.Delay(500); + + await LongTestDelay(); + Assert.AreEqual(2, receivedManagedMessages.Count); } } From d481e9383295cdbec3c4bdace8c2c6163f6a825f Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Tue, 25 Oct 2022 17:29:45 +0200 Subject: [PATCH 08/24] Add new samples. --- Samples/Server/Server_Intercepting_Samples.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 Samples/Server/Server_Intercepting_Samples.cs diff --git a/Samples/Server/Server_Intercepting_Samples.cs b/Samples/Server/Server_Intercepting_Samples.cs new file mode 100644 index 00000000..62129c6d --- /dev/null +++ b/Samples/Server/Server_Intercepting_Samples.cs @@ -0,0 +1,47 @@ +// 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. + +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global +// ReSharper disable InconsistentNaming +// ReSharper disable EmptyConstructor +// ReSharper disable MemberCanBeMadeStatic.Local + +using MQTTnet.Internal; +using MQTTnet.Server; + +namespace MQTTnet.Samples.Server; + +public static class Server_Intercepting_Samples +{ + public static async Task Intercept_Application_Messages() + { + /* + * This sample starts a simple MQTT server which manipulate all processed application messages. + * Please see _Server_Simple_Samples_ for more details on how to start a server. + */ + + var mqttFactory = new MqttFactory(); + var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build(); + + using (var mqttServer = mqttFactory.CreateMqttServer(mqttServerOptions)) + { + mqttServer.InterceptingPublishAsync += args => + { + // Here we only change the topic of the received application message. + // but also changing the payload etc. is required. Changing the QoS after + // transmitting is not supported and makes no sense at all. + args.ApplicationMessage.Topic += "/manipulated"; + + return CompletedTask.Instance; + }; + + await mqttServer.StartAsync(); + + Console.WriteLine("Press Enter to exit."); + Console.ReadLine(); + await mqttServer.StopAsync(); + } + } +} \ No newline at end of file From b1a336eb62a1b7955d294d409133ae9061d4cdfd Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sun, 6 Nov 2022 10:24:58 +0100 Subject: [PATCH 09/24] Reset release notes, update version --- .github/workflows/ReleaseNotes.md | 2 -- .github/workflows/ci.yml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index 8dee1663..e69de29b 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -1,2 +0,0 @@ -* [Core] Fixed dead lock and race conditions in new _AsyncLock_ implementation (#1542). -* [Client] Fixed connection freeze when using Xamarin etc. \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f3fbb41..b69b8710 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: [push, pull_request] env: - VERSION: "4.1.2.${{github.run_number}}" + VERSION: "4.1.3.${{github.run_number}}" jobs: build: From 7ef137e26e0a97f4bdc6498846d9a7d74ccb736f Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sun, 6 Nov 2022 10:29:49 +0100 Subject: [PATCH 10/24] No longer fire _ClientAcknowledgedPublishPacketEvent_ for PUBREC packets (#1550) (#1551) --- .github/workflows/ReleaseNotes.md | 1 + MQTTnet.sln.DotSettings | 1 + Source/MQTTnet.Tests/Server/QoS_Tests.cs | 13 ++--------- ...lientAcknowledgedPublishPacketEventArgs.cs | 16 +++++++++---- Source/MQTTnet/Server/Internal/MqttClient.cs | 23 +++++-------------- 5 files changed, 22 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index e69de29b..687239f2 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -0,0 +1 @@ +* [Server] Fixed duplicated invocation of the event _ClientAcknowledgedPublishPacketAsync_ for QoS level 2 (#1550) \ No newline at end of file diff --git a/MQTTnet.sln.DotSettings b/MQTTnet.sln.DotSettings index 7eed6537..83eed4bd 100644 --- a/MQTTnet.sln.DotSettings +++ b/MQTTnet.sln.DotSettings @@ -242,5 +242,6 @@ See the LICENSE file in the project root for more information. True True True + True True True \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/QoS_Tests.cs b/Source/MQTTnet.Tests/Server/QoS_Tests.cs index 08bc6219..7e5a9491 100644 --- a/Source/MQTTnet.Tests/Server/QoS_Tests.cs +++ b/Source/MQTTnet.Tests/Server/QoS_Tests.cs @@ -99,25 +99,16 @@ namespace MQTTnet.Tests.Server await LongTestDelay(); - Assert.AreEqual(2, eventArgs.Count); + Assert.AreEqual(1, eventArgs.Count); var firstEvent = eventArgs[0]; Assert.IsNotNull(firstEvent); Assert.IsNotNull(firstEvent.PublishPacket); Assert.IsNotNull(firstEvent.AcknowledgePacket); - Assert.IsFalse(firstEvent.IsCompleted); + Assert.IsTrue(firstEvent.IsCompleted); Assert.AreEqual("A", firstEvent.PublishPacket.Topic); - - var secondEvent = eventArgs[1]; - - Assert.IsNotNull(secondEvent); - Assert.IsNotNull(secondEvent.PublishPacket); - Assert.IsNotNull(secondEvent.AcknowledgePacket); - Assert.IsTrue(secondEvent.IsCompleted); - - Assert.AreEqual("A", secondEvent.PublishPacket.Topic); } } } diff --git a/Source/MQTTnet/Server/Events/ClientAcknowledgedPublishPacketEventArgs.cs b/Source/MQTTnet/Server/Events/ClientAcknowledgedPublishPacketEventArgs.cs index e0c09ab1..5b126060 100644 --- a/Source/MQTTnet/Server/Events/ClientAcknowledgedPublishPacketEventArgs.cs +++ b/Source/MQTTnet/Server/Events/ClientAcknowledgedPublishPacketEventArgs.cs @@ -10,15 +10,23 @@ namespace MQTTnet.Server { public sealed class ClientAcknowledgedPublishPacketEventArgs : EventArgs { + public ClientAcknowledgedPublishPacketEventArgs(string clientId, IDictionary sessionItems, MqttPublishPacket publishPacket, MqttPacketWithIdentifier acknowledgePacket) + { + ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); + SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); + PublishPacket = publishPacket ?? throw new ArgumentNullException(nameof(publishPacket)); + AcknowledgePacket = acknowledgePacket ?? throw new ArgumentNullException(nameof(acknowledgePacket)); + } + /// /// Gets the packet which was used for acknowledge. This can be a PubAck or PubComp packet. /// - public MqttPacketWithIdentifier AcknowledgePacket { get; internal set; } + public MqttPacketWithIdentifier AcknowledgePacket { get; } /// /// Gets the ID of the client which acknowledged a PUBLISH packet. /// - public string ClientId { get; internal set; } + public string ClientId { get; } /// /// Gets whether the PUBLISH packet is fully acknowledged. This is the case for PUBACK (QoS 1) and PUBCOMP (QoS 2. @@ -28,11 +36,11 @@ namespace MQTTnet.Server /// /// Gets the PUBLISH packet which was acknowledged. /// - public MqttPublishPacket PublishPacket { get; internal set; } + public MqttPublishPacket PublishPacket { get; } /// /// Gets the session items which contain custom user data per session. /// - public IDictionary SessionItems { get; internal set; } + public IDictionary SessionItems { get; } } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/MqttClient.cs b/Source/MQTTnet/Server/Internal/MqttClient.cs index 4ae2d40d..cb003f83 100644 --- a/Source/MQTTnet/Server/Internal/MqttClient.cs +++ b/Source/MQTTnet/Server/Internal/MqttClient.cs @@ -11,7 +11,6 @@ using MQTTnet.Client; using MQTTnet.Diagnostics; using MQTTnet.Exceptions; using MQTTnet.Formatter; -using MQTTnet.Implementations; using MQTTnet.Internal; using MQTTnet.PacketDispatcher; using MQTTnet.Packets; @@ -170,14 +169,7 @@ namespace MQTTnet.Server { if (_eventContainer.ClientAcknowledgedPublishPacketEvent.HasHandlers) { - var eventArgs = new ClientAcknowledgedPublishPacketEventArgs - { - PublishPacket = publishPacket, - AcknowledgePacket = acknowledgePacket, - ClientId = Id, - SessionItems = Session.Items - }; - + var eventArgs = new ClientAcknowledgedPublishPacketEventArgs(Id, Session.Items, publishPacket, acknowledgePacket); return _eventContainer.ClientAcknowledgedPublishPacketEvent.TryInvokeAsync(eventArgs, _logger); } @@ -272,17 +264,14 @@ namespace MQTTnet.Server } } - async Task HandleIncomingPubRecPacket(MqttPubRecPacket pubRecPacket) + Task HandleIncomingPubRecPacket(MqttPubRecPacket pubRecPacket) { - var acknowledgedPublishPacket = Session.PeekAcknowledgePublishPacket(pubRecPacket.PacketIdentifier); - - if (acknowledgedPublishPacket != null) - { - await ClientAcknowledgedPublishPacket(acknowledgedPublishPacket, pubRecPacket).ConfigureAwait(false); - } - + // Do not fire the event _ClientAcknowledgedPublishPacket_ here because the QoS 2 process is only finished + // properly when the client has sent the PUBCOMP packet. var pubRelPacket = _packetFactories.PubRel.Create(pubRecPacket, MqttApplicationMessageReceivedReasonCode.Success); Session.EnqueueControlPacket(new MqttPacketBusItem(pubRelPacket)); + + return CompletedTask.Instance; } void HandleIncomingPubRelPacket(MqttPubRelPacket pubRelPacket) From 2842f27522fb3f5dbcb781e423564b1ef65043cf Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sun, 6 Nov 2022 10:34:20 +0100 Subject: [PATCH 11/24] 1552 mqttnetservermqttclientsessionsmanager subscribersessions is not proper removed on special case (#1562) --- .github/workflows/ReleaseNotes.md | 1 + .../Internal/AsyncSignal_Tests.cs | 114 ++++++++++++++ .../Internal/MqttPacketBus_Tests.cs | 2 +- Source/MQTTnet.Tests/Server/Load_Tests.cs | 2 +- .../Server/Subscription_TopicHash_Tests.cs | 146 ++++++------------ Source/MQTTnet/Adapter/MqttChannelAdapter.cs | 21 ++- Source/MQTTnet/Client/MqttClient.cs | 5 + Source/MQTTnet/Internal/AsyncQueue.cs | 109 +++++++------ Source/MQTTnet/Internal/AsyncSignal.cs | 113 ++++++++++++++ Source/MQTTnet/Internal/MqttPacketBus.cs | 21 ++- 10 files changed, 369 insertions(+), 165 deletions(-) create mode 100644 Source/MQTTnet.Tests/Internal/AsyncSignal_Tests.cs create mode 100644 Source/MQTTnet/Internal/AsyncSignal.cs diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index 687239f2..b2cc9759 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -1 +1,2 @@ +* [Core] Fixed several dead locks by implementing a new async signal (#1552) * [Server] Fixed duplicated invocation of the event _ClientAcknowledgedPublishPacketAsync_ for QoS level 2 (#1550) \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Internal/AsyncSignal_Tests.cs b/Source/MQTTnet.Tests/Internal/AsyncSignal_Tests.cs new file mode 100644 index 00000000..df63fd40 --- /dev/null +++ b/Source/MQTTnet.Tests/Internal/AsyncSignal_Tests.cs @@ -0,0 +1,114 @@ +// 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 System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Internal; + +namespace MQTTnet.Tests.Internal +{ + [TestClass] + public sealed class AsyncSignal_Tests + { + [TestMethod] + [ExpectedException(typeof(TaskCanceledException))] + public async Task Cancel_If_No_Signal() + { + var asyncSignal = new AsyncSignal(); + + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2))) + { + await asyncSignal.WaitAsync(timeout.Token); + + Assert.Fail("There is no signal. So we must fail here!"); + } + } + + [TestMethod] + [ExpectedException(typeof(ObjectDisposedException))] + public async Task Dispose_Properly() + { + var asyncSignal = new AsyncSignal(); + + // The timeout will not be reached but another task will kill the async signal. + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(999))) + { + _ = Task.Run( + async () => + { + await Task.Delay(2000, CancellationToken.None); + asyncSignal.Dispose(); + }, + CancellationToken.None); + + await asyncSignal.WaitAsync(timeout.Token); + + Assert.Fail("There is no signal. So we must fail here!"); + } + } + + [TestMethod] + public async Task Loop_Signal() + { + var asyncSignal = new AsyncSignal(); + + for (var i = 0; i < 10; i++) + { + asyncSignal.Set(); + + // WaitAsync should return directly because the signal is available. + await asyncSignal.WaitAsync(); + } + } + + [TestMethod] + public async Task Signal() + { + var asyncSignal = new AsyncSignal(); + + // The timeout will not be reached but another task will kill the async signal. + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3))) + { + var stopwatch = Stopwatch.StartNew(); + + _ = Task.Run( + async () => + { + await Task.Delay(1000, CancellationToken.None); + asyncSignal.Set(); + }, + CancellationToken.None); + + await asyncSignal.WaitAsync(timeout.Token); + + stopwatch.Stop(); + + Assert.IsTrue(stopwatch.ElapsedMilliseconds > 900); + } + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public async Task Fail_For_Two_Waiters() + { + var asyncSignal = new AsyncSignal(); + + // This thread will wait properly because it is the first waiter. + _ = Task.Run( + async () => + { + await asyncSignal.WaitAsync(); + }, + CancellationToken.None); + + await Task.Delay(1000); + + // Now the current thread must fail because there is already a waiter. + await asyncSignal.WaitAsync(); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs b/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs index 814d6232..b09159fd 100644 --- a/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs @@ -15,7 +15,7 @@ namespace MQTTnet.Tests.Internal public sealed class MqttPacketBus_Tests { [TestMethod] - [ExpectedException(typeof(OperationCanceledException))] + [ExpectedException(typeof(TaskCanceledException))] public async Task Wait_With_Empty_Bus() { var bus = new MqttPacketBus(); diff --git a/Source/MQTTnet.Tests/Server/Load_Tests.cs b/Source/MQTTnet.Tests/Server/Load_Tests.cs index 7cba9c12..7c446ee0 100644 --- a/Source/MQTTnet.Tests/Server/Load_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Load_Tests.cs @@ -55,7 +55,7 @@ namespace MQTTnet.Tests.Server }); } - SpinWait.SpinUntil(() => receivedMessages == 100000, TimeSpan.FromSeconds(60)); + SpinWait.SpinUntil(() => receivedMessages == 100000, TimeSpan.FromSeconds(90)); Assert.AreEqual(100000, receivedMessages); } diff --git a/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs b/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs index b514a399..70e9bffd 100644 --- a/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs @@ -2,10 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; -using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Client; using MQTTnet.Protocol; using MQTTnet.Server; using MQTTnet.Tests.Mockups; @@ -13,11 +11,11 @@ using MQTTnet.Tests.Mockups; namespace MQTTnet.Tests.Server { [TestClass] - public class Subscription_TopicHash_Tests + public sealed class SubscriptionTopicHashTests { enum TopicHashSelector { NoWildcard, SingleWildcard, MultiWildcard }; - MQTTnet.Server.MqttSession _clientSession; + MqttSession _clientSession; [TestMethod] public void Match_Hash_Test_SingleWildCard() @@ -26,13 +24,9 @@ namespace MQTTnet.Tests.Server var l1 = "topic1"; var l2 = "+"; var l3 = "prop1"; - var topic = string.Format("{0}/{1}/{2}/{3}", l0, l1, l2, l3); + var topic = $"{l0}/{l1}/{l2}/{l3}"; - - UInt64 topicHash; - UInt64 topicHashMask; - bool topicHasWildcard; - MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard); + MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); @@ -65,13 +59,9 @@ namespace MQTTnet.Tests.Server var l1 = "topic1"; var l2 = "sub1"; var l3 = "#"; - var topic = string.Format("{0}/{1}/{2}/{3}", l0, l1, l2, l3); + var topic = $"{l0}/{l1}/{l2}/{l3}"; - UInt64 topicHash; - UInt64 topicHashMask; - bool topicHasWildcard; - - MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard); + MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); @@ -104,13 +94,9 @@ namespace MQTTnet.Tests.Server var l1 = "topic1"; var l2 = "sub1"; var l3 = "prop1"; - var topic = string.Format("{0}/{1}/{2}/{3}", l0, l1, l2, l3); + var topic = $"{l0}/{l1}/{l2}/{l3}"; - UInt64 topicHash; - UInt64 topicHashMask; - bool topicHasWildcard; - - MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard); + MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); Assert.IsFalse(topicHasWildcard, "Wildcard detected when not wildcard present"); @@ -144,14 +130,14 @@ namespace MQTTnet.Tests.Server public void Match_Hash_Test_LongTopic_MultiWildcard() { var sb = new StringBuilder(); - const int NumLevels = 8; - var levelNames = new string[NumLevels]; - for (var i = 0; i < NumLevels; ++i) + const int numLevels = 8; + var levelNames = new string[numLevels]; + for (var i = 0; i < numLevels; ++i) { if (i > 0) sb.Append("/"); string levelName; - if (i == NumLevels - 1) + if (i == numLevels - 1) { // last one is # levelName = "#"; @@ -167,11 +153,7 @@ namespace MQTTnet.Tests.Server // UInt64 is limited to 8 levels - UInt64 topicHash; - UInt64 topicHashMask; - bool topicHasWildcard; - - MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard); + MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); @@ -214,14 +196,14 @@ namespace MQTTnet.Tests.Server public void Match_Hash_Test_LongTopic_SingleWildCard() { var sb = new StringBuilder(); - const int NumLevels = 8; - var levelNames = new string[NumLevels]; - for (var i = 0; i < NumLevels; ++i) + const int numLevels = 8; + var levelNames = new string[numLevels]; + for (var i = 0; i < numLevels; ++i) { if (i > 0) sb.Append("/"); string levelName; - if (i == NumLevels - 1) + if (i == numLevels - 1) { // last one is + levelName = "+"; @@ -235,11 +217,7 @@ namespace MQTTnet.Tests.Server } var topic = sb.ToString(); - UInt64 topicHash; - UInt64 topicHashMask; - bool topicHasWildcard; - - MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard); + MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); @@ -284,9 +262,9 @@ namespace MQTTnet.Tests.Server public void Match_Hash_Test_LongTopic_NoWildCard() { var sb = new StringBuilder(); - const int NumLevels = 9; - var levelNames = new string[NumLevels]; - for(var i=0; i < NumLevels; ++i) + const int numLevels = 9; + var levelNames = new string[numLevels]; + for(var i=0; i < numLevels; ++i) { if (i > 0) sb.Append("/"); @@ -296,11 +274,7 @@ namespace MQTTnet.Tests.Server } var topic = sb.ToString(); - UInt64 topicHash; - UInt64 topicHashMask; - bool topicHasWildcard; - - MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard); + MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); Assert.IsFalse(topicHasWildcard, "Wildcard detected when not present"); @@ -332,11 +306,7 @@ namespace MQTTnet.Tests.Server { var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/+"; - UInt64 topicHash; - UInt64 topicHashMask; - bool topicHasWildcard; - - MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard); + MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); } @@ -349,11 +319,7 @@ namespace MQTTnet.Tests.Server { var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/#"; - UInt64 topicHash; - UInt64 topicHashMask; - bool topicHasWildcard; - - MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard); + MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); } @@ -422,14 +388,10 @@ namespace MQTTnet.Tests.Server [TestMethod] public void Check_Hash_Bucket_Depth() { - Dictionary> topicsByPublisher; - Dictionary> singleWildcardTopicsByPublisher; - Dictionary> multiWildcardTopicsByPublisher; + const int numPublishers = 5000; + const int numTopicsPerPublisher = 10; - const int NumPublishers = 5000; - const int NumTopicsPerPublisher = 10; - - TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out topicsByPublisher, out singleWildcardTopicsByPublisher, out multiWildcardTopicsByPublisher); + TopicGenerator.Generate(numPublishers, numTopicsPerPublisher, out var topicsByPublisher, out var singleWildcardTopicsByPublisher, out var multiWildcardTopicsByPublisher); // There will be many 'similar' topics ending with, i.e. "sensor100", "sensor101", ... // Hash bucket depths should remain low. @@ -444,10 +406,7 @@ namespace MQTTnet.Tests.Server var topics = t.Value; foreach (var topic in topics) { - UInt64 topicHash; - UInt64 hashMask; - bool hasWildcard; - MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out hashMask, out hasWildcard); + MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var hashMask, out var hasWildcard); bucketDepths.TryGetValue(topicHash, out var currentValue); ++currentValue; @@ -489,16 +448,12 @@ namespace MQTTnet.Tests.Server void CheckTopicHash(string topic, ulong expectedHash, ulong expectedHashMask) { - ulong topicHash; - ulong hashMask; - bool hasWildcard; - - MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out hashMask, out hasWildcard); + MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var hashMask, out var hasWildcard); Console.WriteLine(); Console.WriteLine("Topic: " + topic); - Console.WriteLine(string.Format("Hash: {0:X8}", topicHash)); - Console.WriteLine(string.Format("Hash Mask: {0:X8}", hashMask)); + Console.WriteLine($"Hash: {topicHash:X8}"); + Console.WriteLine($"Hash Mask: {hashMask:X8}"); Assert.AreEqual(expectedHash, topicHash, "Topic hash not as expected. Has the hash function changed?"); Assert.AreEqual(expectedHashMask, hashMask, "Topic hash mask not as expected"); @@ -507,27 +462,23 @@ namespace MQTTnet.Tests.Server async Task> PrepareTopicHashSubscriptions(TopicHashSelector selector) { - Dictionary> topicsByPublisher; - Dictionary> singleWildcardTopicsByPublisher; - Dictionary> multiWildcardTopicsByPublisher; + const int numPublishers = 1; + const int numTopicsPerPublisher = 10000; - const int NumPublishers = 1; - const int NumTopicsPerPublisher = 10000; - - TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out topicsByPublisher, out singleWildcardTopicsByPublisher, out multiWildcardTopicsByPublisher); + TopicGenerator.Generate(numPublishers, numTopicsPerPublisher, out var topicsByPublisher, out var singleWildcardTopicsByPublisher, out var multiWildcardTopicsByPublisher); var topics = topicsByPublisher.FirstOrDefault().Value; var singleWildcardTopics = singleWildcardTopicsByPublisher.FirstOrDefault().Value; var multiWildcardTopics = multiWildcardTopicsByPublisher.FirstOrDefault().Value; - const string ClientId = "Client1"; - var logger = new Mockups.TestLogger(); - var serverOptions = new MQTTnet.Server.MqttServerOptions(); - var eventContainer = new MQTTnet.Server.MqttServerEventContainer(); + const string clientId = "Client1"; + var logger = new TestLogger(); + var serverOptions = new MqttServerOptions(); + var eventContainer = new MqttServerEventContainer(); var retainedMessagesManager = new MqttRetainedMessagesManager(eventContainer, logger); var sessionManager = new MqttClientSessionsManager(serverOptions, retainedMessagesManager, eventContainer, logger); - _clientSession = new MQTTnet.Server.MqttSession( - ClientId, + _clientSession = new MqttSession( + clientId, false, new Dictionary(), serverOptions, @@ -553,16 +504,18 @@ namespace MQTTnet.Tests.Server foreach (var t in topicsToSubscribe) { var subPacket = new Packets.MqttSubscribePacket(); - var filter = new Packets.MqttTopicFilter(); - filter.Topic = t; + var filter = new Packets.MqttTopicFilter + { + Topic = t + }; subPacket.TopicFilters.Add(filter); - await _clientSession.SubscriptionsManager.Subscribe(subPacket, default(CancellationToken)); + await _clientSession.SubscriptionsManager.Subscribe(subPacket, default); } return topics; } - int CheckTopicSubscriptions(MQTTnet.Server.MqttSession clientSession, List topicsToFind, string subject) + int CheckTopicSubscriptions(MqttSession clientSession, List topicsToFind, string subject) { var matchCount = 0; @@ -577,12 +530,9 @@ namespace MQTTnet.Tests.Server { var topicToFind = topicsToFind[countDown]; - UInt64 topicHash; - UInt64 hashMask; - bool hasWildcard; - MQTTnet.Server.MqttSubscription.CalculateTopicHash((string)topicToFind, out topicHash, out hashMask, out hasWildcard); + MqttSubscription.CalculateTopicHash(topicToFind, out var topicHash, out var hashMask, out var hasWildcard); - var result = clientSession.SubscriptionsManager.CheckSubscriptions((string)topicToFind, topicHash, MqttQualityOfServiceLevel.AtMostOnce, "OtherClient"); + var result = clientSession.SubscriptionsManager.CheckSubscriptions(topicToFind, topicHash, MqttQualityOfServiceLevel.AtMostOnce, "OtherClient"); if (result.IsSubscribed) { ++matchCount; diff --git a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs index 7a9db45d..4b030150 100644 --- a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs +++ b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs @@ -13,7 +13,6 @@ using MQTTnet.Channel; using MQTTnet.Diagnostics; using MQTTnet.Exceptions; using MQTTnet.Formatter; -using MQTTnet.Implementations; using MQTTnet.Internal; using MQTTnet.Packets; @@ -296,8 +295,26 @@ namespace MQTTnet.Adapter while (totalBytesRead < buffer.Length) { - var bytesRead = await _channel.ReadAsync(buffer, totalBytesRead, buffer.Length - totalBytesRead, cancellationToken).ConfigureAwait(false); + // Check two times for cancellation because the call to _ReadAsync_ might block for some time. + if (cancellationToken.IsCancellationRequested) + { + return ReadFixedHeaderResult.Cancelled; + } + int bytesRead; + try + { + bytesRead = await _channel.ReadAsync(buffer, totalBytesRead, buffer.Length - totalBytesRead, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + return ReadFixedHeaderResult.Cancelled; + } + catch (SocketException) + { + return ReadFixedHeaderResult.ConnectionClosed; + } + if (cancellationToken.IsCancellationRequested) { return ReadFixedHeaderResult.Cancelled; diff --git a/Source/MQTTnet/Client/MqttClient.cs b/Source/MQTTnet/Client/MqttClient.cs index 92004fe8..48c32bf4 100644 --- a/Source/MQTTnet/Client/MqttClient.cs +++ b/Source/MQTTnet/Client/MqttClient.cs @@ -827,6 +827,11 @@ namespace MQTTnet.Client return; } + if (exception is AggregateException aggregateException) + { + exception = aggregateException.GetBaseException(); + } + if (exception is OperationCanceledException) { } diff --git a/Source/MQTTnet/Internal/AsyncQueue.cs b/Source/MQTTnet/Internal/AsyncQueue.cs index 5605c0f8..c54f5467 100644 --- a/Source/MQTTnet/Internal/AsyncQueue.cs +++ b/Source/MQTTnet/Internal/AsyncQueue.cs @@ -11,64 +11,39 @@ namespace MQTTnet.Internal { public sealed class AsyncQueue : IDisposable { + readonly AsyncSignal _signal = new AsyncSignal(); readonly object _syncRoot = new object(); - SemaphoreSlim _semaphore = new SemaphoreSlim(0); + ConcurrentQueue _queue = new ConcurrentQueue(); + bool _isDisposed; + public int Count => _queue.Count; + public void Clear() + { + Interlocked.Exchange(ref _queue, new ConcurrentQueue()); + } + + public void Dispose() + { + lock (_syncRoot) + { + _signal?.Dispose(); + + _isDisposed = true; + } + } + public void Enqueue(TItem item) { lock (_syncRoot) { _queue.Enqueue(item); - _semaphore?.Release(); + _signal.Set(); } } - public async Task> TryDequeueAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - try - { - Task task; - lock (_syncRoot) - { - if (_semaphore == null) - { - return new AsyncQueueDequeueResult(false, default); - } - - task = _semaphore.WaitAsync(cancellationToken); - } - - await task.ConfigureAwait(false); - - if (cancellationToken.IsCancellationRequested) - { - return new AsyncQueueDequeueResult(false, default); - } - - if (_queue.TryDequeue(out var item)) - { - return new AsyncQueueDequeueResult(true, item); - } - } - catch (ArgumentNullException) - { - // The semaphore throws this internally sometimes. - return new AsyncQueueDequeueResult(false, default); - } - catch (OperationCanceledException) - { - return new AsyncQueueDequeueResult(false, default); - } - } - - return new AsyncQueueDequeueResult(false, default); - } - public AsyncQueueDequeueResult TryDequeue() { if (_queue.TryDequeue(out var item)) @@ -79,18 +54,42 @@ namespace MQTTnet.Internal return new AsyncQueueDequeueResult(false, default); } - public void Clear() + public async Task> TryDequeueAsync(CancellationToken cancellationToken) { - Interlocked.Exchange(ref _queue, new ConcurrentQueue()); - } - - public void Dispose() - { - lock (_syncRoot) + while (!cancellationToken.IsCancellationRequested) { - _semaphore?.Dispose(); - _semaphore = null; + try + { + Task task; + lock (_syncRoot) + { + if (_isDisposed) + { + return new AsyncQueueDequeueResult(false, default); + } + + task = _signal.WaitAsync(cancellationToken); + } + + await task.ConfigureAwait(false); + + if (cancellationToken.IsCancellationRequested) + { + return new AsyncQueueDequeueResult(false, default); + } + + if (_queue.TryDequeue(out var item)) + { + return new AsyncQueueDequeueResult(true, item); + } + } + catch (OperationCanceledException) + { + return new AsyncQueueDequeueResult(false, default); + } } + + return new AsyncQueueDequeueResult(false, default); } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Internal/AsyncSignal.cs b/Source/MQTTnet/Internal/AsyncSignal.cs new file mode 100644 index 00000000..cde8532c --- /dev/null +++ b/Source/MQTTnet/Internal/AsyncSignal.cs @@ -0,0 +1,113 @@ +// 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 System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.Internal +{ + public sealed class AsyncSignal : IDisposable + { + readonly object _syncRoot = new object(); + + bool _isDisposed; + bool _isSignaled; + AsyncSignalWaiter _waiter; + + public void Dispose() + { + lock (_syncRoot) + { + _waiter?.Dispose(); + _waiter = null; + + _isDisposed = true; + } + } + + public void Set() + { + lock (_syncRoot) + { + _isSignaled = true; + + // If there is already a waiting task let it run. + _waiter?.Approve(); + _waiter = null; + } + } + + public Task WaitAsync(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + lock (_syncRoot) + { + ThrowIfDisposed(); + + if (_isSignaled) + { + return CompletedTask.Instance; + } + + if (_waiter != null) + { + if (!_waiter.Task.IsCompleted) + { + throw new InvalidOperationException("Only one waiting task is permitted per async signal."); + } + + _waiter.Dispose(); + } + + _waiter = new AsyncSignalWaiter(cancellationToken); + return _waiter.Task; + } + } + + void ThrowIfDisposed() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(AsyncSignal)); + } + } + + sealed class AsyncSignalWaiter : IDisposable + { + readonly AsyncTaskCompletionSource _promise = new AsyncTaskCompletionSource(); + + // ReSharper disable once FieldCanBeMadeReadOnly.Local + CancellationTokenRegistration _cancellationTokenRegistration; + + public AsyncSignalWaiter(CancellationToken cancellationToken) + { + if (cancellationToken.CanBeCanceled) + { + _cancellationTokenRegistration = cancellationToken.Register(Cancel); + } + } + + public Task Task => _promise.Task; + + public void Approve() + { + _promise.TrySetResult(true); + } + + public void Dispose() + { + _cancellationTokenRegistration.Dispose(); + + _promise.TrySetException(new ObjectDisposedException(nameof(AsyncSignalWaiter))); + } + + void Cancel() + { + _promise.TrySetCanceled(); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Internal/MqttPacketBus.cs b/Source/MQTTnet/Internal/MqttPacketBus.cs index af02cbed..002e3d58 100644 --- a/Source/MQTTnet/Internal/MqttPacketBus.cs +++ b/Source/MQTTnet/Internal/MqttPacketBus.cs @@ -20,7 +20,7 @@ namespace MQTTnet.Internal new LinkedList() }; - readonly SemaphoreSlim _semaphore = new SemaphoreSlim(0, 1); + readonly AsyncSignal _signal = new AsyncSignal(); readonly object _syncRoot = new object(); int _activePartition = (int)MqttPacketBusPartition.Health; @@ -82,7 +82,16 @@ namespace MQTTnet.Internal // No partition contains data so that we have to wait and put // the worker back to the thread pool. - await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + await _signal.WaitAsync(cancellationToken).ConfigureAwait(false); + } + catch (ObjectDisposedException) + { + // The cancelled token should "hide" the disposal of the signal. + cancellationToken.ThrowIfCancellationRequested(); + throw; + } } cancellationToken.ThrowIfCancellationRequested(); @@ -92,7 +101,7 @@ namespace MQTTnet.Internal public void Dispose() { - _semaphore?.Dispose(); + _signal.Dispose(); } public void DropFirstItem(MqttPacketBusPartition partition) @@ -118,11 +127,7 @@ namespace MQTTnet.Internal lock (_syncRoot) { _partitions[(int)partition].AddLast(item); - - if (_semaphore.CurrentCount == 0) - { - _semaphore.Release(); - } + _signal.Set(); } } From 5b1cc7d713cfdf76aa19b3d86c42a0b0f042acbf Mon Sep 17 00:00:00 2001 From: SilverFox Date: Sun, 6 Nov 2022 17:35:15 +0800 Subject: [PATCH 12/24] Also cleanup `MqttClientSessionsManager._subscriberSessions` when session is taken over by a new one with CleanSession=true, fix #1552 (#1553) --- Source/MQTTnet.Tests/Internal/AsyncSignal_Tests.cs | 2 +- .../Server/Internal/MqttClientSessionsManager.cs | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Source/MQTTnet.Tests/Internal/AsyncSignal_Tests.cs b/Source/MQTTnet.Tests/Internal/AsyncSignal_Tests.cs index df63fd40..88ec56fa 100644 --- a/Source/MQTTnet.Tests/Internal/AsyncSignal_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/AsyncSignal_Tests.cs @@ -90,7 +90,7 @@ namespace MQTTnet.Tests.Internal Assert.IsTrue(stopwatch.ElapsedMilliseconds > 900); } } - + [TestMethod] [ExpectedException(typeof(InvalidOperationException))] public async Task Fail_For_Two_Waiters() diff --git a/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs b/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs index 308e9416..125af7fd 100644 --- a/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs +++ b/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs @@ -485,9 +485,10 @@ namespace MQTTnet.Server using (await _createConnectionSyncRoot.EnterAsync().ConfigureAwait(false)) { MqttSession session; + MqttSession oldSession; lock (_sessionsManagementLock) { - if (!_sessions.TryGetValue(connectPacket.ClientId, out session)) + if (!_sessions.TryGetValue(connectPacket.ClientId, out oldSession)) { session = CreateSession(connectPacket.ClientId, validatingConnectionEventArgs.SessionItems, sessionShouldPersist); } @@ -496,11 +497,14 @@ namespace MQTTnet.Server if (connectPacket.CleanSession) { _logger.Verbose("Deleting existing session of client '{0}' due to clean start.", connectPacket.ClientId); + _subscriberSessions.Remove(oldSession); session = CreateSession(connectPacket.ClientId, validatingConnectionEventArgs.SessionItems, sessionShouldPersist); } else { _logger.Verbose("Reusing existing session of client '{0}'.", connectPacket.ClientId); + session = oldSession; + oldSession = null; // Session persistence could change for MQTT 5 clients that reconnect with different SessionExpiryInterval session.IsPersistent = sessionShouldPersist; connAckPacket.IsSessionPresent = true; @@ -533,7 +537,7 @@ namespace MQTTnet.Server existingClient.IsTakenOver = true; await existingClient.StopAsync(MqttDisconnectReasonCode.SessionTakenOver).ConfigureAwait(false); - if (_eventContainer.ClientConnectedEvent.HasHandlers) + if (_eventContainer.ClientDisconnectedEvent.HasHandlers) { var eventArgs = new ClientDisconnectedEventArgs( existingClient.Id, @@ -544,6 +548,8 @@ namespace MQTTnet.Server await _eventContainer.ClientDisconnectedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); } } + + oldSession?.Dispose(); } return client; From 3fc6982e9a08bb8a78690ac9645724dc0a314d50 Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sun, 6 Nov 2022 10:35:59 +0100 Subject: [PATCH 13/24] Fix upgrading and downgrading of retained messages QoS for subscriptions (#1564) --- .github/workflows/ReleaseNotes.md | 3 +- .../Server/Retained_Messages_Tests.cs | 62 ++++++++++++++++++- .../MQTTnet/Formatter/MqttPacketFactories.cs | 1 + .../Formatter/MqttPublishPacketFactory.cs | 13 ++++ Source/MQTTnet/Server/Internal/MqttClient.cs | 5 +- .../Internal/MqttClientSessionsManager.cs | 4 +- .../MqttClientSubscriptionsManager.cs | 30 ++++----- .../Server/Internal/SubscribeResult.cs | 7 ++- .../Server/MqttRetainedMessageMatch.cs | 9 ++- 9 files changed, 111 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index b2cc9759..95fe5078 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -1,2 +1,3 @@ * [Core] Fixed several dead locks by implementing a new async signal (#1552) -* [Server] Fixed duplicated invocation of the event _ClientAcknowledgedPublishPacketAsync_ for QoS level 2 (#1550) \ No newline at end of file +* [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) \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Retained_Messages_Tests.cs b/Source/MQTTnet.Tests/Server/Retained_Messages_Tests.cs index 1a6b0996..b2774157 100644 --- a/Source/MQTTnet.Tests/Server/Retained_Messages_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Retained_Messages_Tests.cs @@ -65,6 +65,66 @@ namespace MQTTnet.Tests.Server } } + [TestMethod] + public async Task Downgrade_QoS_Level() + { + using (var testEnvironment = CreateTestEnvironment()) + { + await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(); + + // Add the retained message with QoS 2! + await c1.PublishAsync( + new MqttApplicationMessageBuilder().WithTopic("retained") + .WithPayload(new byte[3]) + .WithRetainFlag() + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.ExactlyOnce) + .Build()); + + // The second client uses QoS 1 so a downgrade is required. + var c2 = await testEnvironment.ConnectClient(); + var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2); + await c2.SubscribeAsync(new MqttTopicFilter { Topic = "retained", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); + + await LongTestDelay(); + + messageHandler.AssertReceivedCountEquals(1); + + Assert.AreEqual(MqttQualityOfServiceLevel.AtLeastOnce, messageHandler.ReceivedEventArgs.First().ApplicationMessage.QualityOfServiceLevel); + } + } + + [TestMethod] + public async Task No_Upgrade_QoS_Level() + { + using (var testEnvironment = CreateTestEnvironment()) + { + await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(); + + // Add the retained message with QoS 1! + await c1.PublishAsync( + new MqttApplicationMessageBuilder().WithTopic("retained") + .WithPayload(new byte[3]) + .WithRetainFlag() + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .Build()); + + // The second client uses QoS 2 so an upgrade is expected but according to the MQTT spec this is not supported! + var c2 = await testEnvironment.ConnectClient(); + var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2); + await c2.SubscribeAsync(new MqttTopicFilter { Topic = "retained", QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce }); + + await LongTestDelay(); + + messageHandler.AssertReceivedCountEquals(1); + + Assert.AreEqual(MqttQualityOfServiceLevel.AtLeastOnce, messageHandler.ReceivedEventArgs.First().ApplicationMessage.QualityOfServiceLevel); + } + } + [TestMethod] public async Task Receive_No_Retained_Message_After_Subscribe() { @@ -124,7 +184,7 @@ namespace MQTTnet.Tests.Server .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) .WithRetainFlag() .Build()); - + await c1.DisconnectAsync(); // Subscribe using a new client. diff --git a/Source/MQTTnet/Formatter/MqttPacketFactories.cs b/Source/MQTTnet/Formatter/MqttPacketFactories.cs index a13c2aa6..70de69d2 100644 --- a/Source/MQTTnet/Formatter/MqttPacketFactories.cs +++ b/Source/MQTTnet/Formatter/MqttPacketFactories.cs @@ -7,6 +7,7 @@ namespace MQTTnet.Formatter public sealed class MqttPacketFactories { public MqttConnAckPacketFactory ConnAck { get; } = new MqttConnAckPacketFactory(); + public MqttConnectPacketFactory Connect { get; } = new MqttConnectPacketFactory(); public MqttDisconnectPacketFactory Disconnect { get; } = new MqttDisconnectPacketFactory(); diff --git a/Source/MQTTnet/Formatter/MqttPublishPacketFactory.cs b/Source/MQTTnet/Formatter/MqttPublishPacketFactory.cs index 90325e9e..ab33f7a9 100644 --- a/Source/MQTTnet/Formatter/MqttPublishPacketFactory.cs +++ b/Source/MQTTnet/Formatter/MqttPublishPacketFactory.cs @@ -5,6 +5,7 @@ using System; using MQTTnet.Exceptions; using MQTTnet.Packets; +using MQTTnet.Server; namespace MQTTnet.Formatter { @@ -80,5 +81,17 @@ namespace MQTTnet.Formatter return packet; } + + public MqttPublishPacket Create(MqttRetainedMessageMatch retainedMessage) + { + if (retainedMessage == null) + { + throw new ArgumentNullException(nameof(retainedMessage)); + } + + var publishPacket = Create(retainedMessage.ApplicationMessage); + publishPacket.QualityOfServiceLevel = retainedMessage.SubscriptionQualityOfServiceLevel; + return publishPacket; + } } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/MqttClient.cs b/Source/MQTTnet/Server/Internal/MqttClient.cs index cb003f83..1bc51152 100644 --- a/Source/MQTTnet/Server/Internal/MqttClient.cs +++ b/Source/MQTTnet/Server/Internal/MqttClient.cs @@ -296,15 +296,14 @@ namespace MQTTnet.Server if (subscribeResult.RetainedMessages != null) { - foreach (var retainedApplicationMessage in subscribeResult.RetainedMessages) + foreach (var retainedMessageMatch in subscribeResult.RetainedMessages) { - var publishPacket = _packetFactories.Publish.Create(retainedApplicationMessage.ApplicationMessage); + var publishPacket = _packetFactories.Publish.Create(retainedMessageMatch); Session.EnqueueDataPacket(new MqttPacketBusItem(publishPacket)); } } } - async Task HandleIncomingUnsubscribePacket(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken) { var unsubscribeResult = await Session.SubscriptionsManager.Unsubscribe(unsubscribePacket, cancellationToken).ConfigureAwait(false); diff --git a/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs b/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs index 125af7fd..87fcdd34 100644 --- a/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs +++ b/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs @@ -418,9 +418,9 @@ namespace MQTTnet.Server if (subscribeResult.RetainedMessages != null) { - foreach (var retainedApplicationMessage in subscribeResult.RetainedMessages) + foreach (var retainedMessageMatch in subscribeResult.RetainedMessages) { - var publishPacket = _packetFactories.Publish.Create(retainedApplicationMessage.ApplicationMessage); + var publishPacket = _packetFactories.Publish.Create(retainedMessageMatch); clientSession.EnqueueDataPacket(new MqttPacketBusItem(publishPacket)); } } diff --git a/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs b/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs index 3a505a40..a6542d14 100644 --- a/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs +++ b/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs @@ -169,10 +169,7 @@ namespace MQTTnet.Server } var retainedApplicationMessages = await _retainedMessagesManager.GetMessages().ConfigureAwait(false); - var result = new SubscribeResult - { - ReasonCodes = new List(subscribePacket.TopicFilters.Count) - }; + var result = new SubscribeResult(subscribePacket.TopicFilters.Count); var addedSubscriptions = new List(); var finalTopicFilters = new List(); @@ -412,14 +409,14 @@ namespace MQTTnet.Server } static void FilterRetainedApplicationMessages( - IList retainedApplicationMessages, + IList retainedMessages, CreateSubscriptionResult createSubscriptionResult, SubscribeResult subscribeResult) { - for (var index = retainedApplicationMessages.Count - 1; index >= 0; index--) + for (var index = retainedMessages.Count - 1; index >= 0; index--) { - var retainedApplicationMessage = retainedApplicationMessages[index]; - if (retainedApplicationMessage == null) + var retainedMessage = retainedMessages[index]; + if (retainedMessage == null) { continue; } @@ -436,16 +433,21 @@ namespace MQTTnet.Server continue; } - if (MqttTopicFilterComparer.Compare(retainedApplicationMessage.Topic, createSubscriptionResult.Subscription.Topic) != MqttTopicFilterCompareResult.IsMatch) + if (MqttTopicFilterComparer.Compare(retainedMessage.Topic, createSubscriptionResult.Subscription.Topic) != MqttTopicFilterCompareResult.IsMatch) { continue; } - var retainedMessageMatch = new MqttRetainedMessageMatch + var retainedMessageMatch = new MqttRetainedMessageMatch(retainedMessage, createSubscriptionResult.Subscription.GrantedQualityOfServiceLevel); + if (retainedMessageMatch.SubscriptionQualityOfServiceLevel > retainedMessageMatch.ApplicationMessage.QualityOfServiceLevel) { - ApplicationMessage = retainedApplicationMessage, - SubscriptionQualityOfServiceLevel = createSubscriptionResult.Subscription.GrantedQualityOfServiceLevel - }; + // UPGRADING the QoS is not allowed! + // From MQTT spec: Subscribing to a Topic Filter at QoS 2 is equivalent to saying + // "I would like to receive Messages matching this filter at the QoS with which they were published". + // This means a publisher is responsible for determining the maximum QoS a Message can be delivered at, + // but a subscriber is able to require that the Server downgrades the QoS to one more suitable for its usage. + retainedMessageMatch.SubscriptionQualityOfServiceLevel = retainedMessageMatch.ApplicationMessage.QualityOfServiceLevel; + } if (subscribeResult.RetainedMessages == null) { @@ -456,7 +458,7 @@ namespace MQTTnet.Server // Clear the retained message from the list because the client should receive every message only // one time even if multiple subscriptions affect them. - retainedApplicationMessages[index] = null; + retainedMessages[index] = null; } } diff --git a/Source/MQTTnet/Server/Internal/SubscribeResult.cs b/Source/MQTTnet/Server/Internal/SubscribeResult.cs index beff649b..9c828eda 100644 --- a/Source/MQTTnet/Server/Internal/SubscribeResult.cs +++ b/Source/MQTTnet/Server/Internal/SubscribeResult.cs @@ -10,8 +10,13 @@ namespace MQTTnet.Server { public sealed class SubscribeResult { + public SubscribeResult(int topicsCount) + { + ReasonCodes = new List(topicsCount); + } + public bool CloseConnection { get; set; } - + public List ReasonCodes { get; set; } public string ReasonString { get; set; } diff --git a/Source/MQTTnet/Server/MqttRetainedMessageMatch.cs b/Source/MQTTnet/Server/MqttRetainedMessageMatch.cs index a54e5b7f..59bfbc2c 100644 --- a/Source/MQTTnet/Server/MqttRetainedMessageMatch.cs +++ b/Source/MQTTnet/Server/MqttRetainedMessageMatch.cs @@ -2,13 +2,20 @@ // 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.Protocol; namespace MQTTnet.Server { public sealed class MqttRetainedMessageMatch { - public MqttApplicationMessage ApplicationMessage { get; set; } + public MqttRetainedMessageMatch(MqttApplicationMessage applicationMessage, MqttQualityOfServiceLevel subscriptionQualityOfServiceLevel) + { + ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage)); + SubscriptionQualityOfServiceLevel = subscriptionQualityOfServiceLevel; + } + + public MqttApplicationMessage ApplicationMessage { get; } public MqttQualityOfServiceLevel SubscriptionQualityOfServiceLevel { get; set; } } From 6e1ec266a7da3ba1e802c95ec31ae0ae6c6af006 Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sun, 6 Nov 2022 10:49:19 +0100 Subject: [PATCH 14/24] Update ReleaseNotes.md --- .github/workflows/ReleaseNotes.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index 95fe5078..cb58d55b 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -1,3 +1,4 @@ * [Core] Fixed several dead locks by implementing a new async signal (#1552) * [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) \ No newline at end of file +* [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) \ No newline at end of file From e6c1ab4e4ccb7499e29888b498536756108a6dd1 Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sun, 6 Nov 2022 10:50:04 +0100 Subject: [PATCH 15/24] Fail all waiting tasks when the client connection gets closed (#1563) --- .github/workflows/ReleaseNotes.md | 1 + .../MQTTnet.Tests/Server/Publishing_Tests.cs | 31 ++++ .../MQTTnet.Tests/Server/Subscribe_Tests.cs | 16 +++ .../MQTTnet.Tests/Server/Unsubscribe_Tests.cs | 32 +++++ .../MqttClientDisconnectedException.cs | 16 +++ ...tUnexpectedDisconnectReceivedException.cs} | 7 +- Source/MQTTnet/Client/MqttClient.cs | 18 +-- .../Exceptions/MqttCommunicationException.cs | 6 +- Source/MQTTnet/MQTTnet.csproj.DotSettings | 1 + .../PacketDispatcher/MqttPacketDispatcher.cs | 135 ++++++++++++------ 10 files changed, 201 insertions(+), 62 deletions(-) create mode 100644 Source/MQTTnet.Tests/Server/Publishing_Tests.cs create mode 100644 Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs create mode 100644 Source/MQTTnet/Client/Exceptions/MqttClientDisconnectedException.cs rename Source/MQTTnet/{Exceptions/MqttUnexpectedDisconnectReceivedException.cs => Client/Exceptions/MqttClientUnexpectedDisconnectReceivedException.cs} (79%) diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index cb58d55b..3ef3a30e 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -1,4 +1,5 @@ * [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 issue in upgrading and downgrading of QoS levels for subscriptions and retained messages (#1560) * [Server] Fixed memory leak when old sessions are discarded (#1553) \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Publishing_Tests.cs b/Source/MQTTnet.Tests/Server/Publishing_Tests.cs new file mode 100644 index 00000000..c5df97f7 --- /dev/null +++ b/Source/MQTTnet.Tests/Server/Publishing_Tests.cs @@ -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); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs b/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs index b67adea9..d4631bc2 100644 --- a/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs @@ -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] public async Task Enqueue_Message_After_Subscription() { diff --git a/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs b/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs new file mode 100644 index 00000000..5692fa8d --- /dev/null +++ b/Source/MQTTnet.Tests/Server/Unsubscribe_Tests.cs @@ -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("#"); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Exceptions/MqttClientDisconnectedException.cs b/Source/MQTTnet/Client/Exceptions/MqttClientDisconnectedException.cs new file mode 100644 index 00000000..ce548428 --- /dev/null +++ b/Source/MQTTnet/Client/Exceptions/MqttClientDisconnectedException.cs @@ -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) + { + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Exceptions/MqttUnexpectedDisconnectReceivedException.cs b/Source/MQTTnet/Client/Exceptions/MqttClientUnexpectedDisconnectReceivedException.cs similarity index 79% rename from Source/MQTTnet/Exceptions/MqttUnexpectedDisconnectReceivedException.cs rename to Source/MQTTnet/Client/Exceptions/MqttClientUnexpectedDisconnectReceivedException.cs index eca736fc..5501e39c 100644 --- a/Source/MQTTnet/Exceptions/MqttUnexpectedDisconnectReceivedException.cs +++ b/Source/MQTTnet/Client/Exceptions/MqttClientUnexpectedDisconnectReceivedException.cs @@ -3,14 +3,15 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using MQTTnet.Exceptions; using MQTTnet.Packets; 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.") { ReasonCode = disconnectPacket.ReasonCode; diff --git a/Source/MQTTnet/Client/MqttClient.cs b/Source/MQTTnet/Client/MqttClient.cs index 48c32bf4..50e9223a 100644 --- a/Source/MQTTnet/Client/MqttClient.cs +++ b/Source/MQTTnet/Client/MqttClient.cs @@ -32,25 +32,21 @@ namespace MQTTnet.Client readonly object _disconnectLock = new object(); readonly AsyncEvent _inspectPacketEvent = new AsyncEvent(); readonly MqttNetSourceLogger _logger; - readonly MqttPacketDispatcher _packetDispatcher = new MqttPacketDispatcher(); readonly MqttPacketFactories _packetFactories = new MqttPacketFactories(); readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider(); readonly IMqttNetLogger _rootLogger; IMqttChannelAdapter _adapter; - CancellationTokenSource _backgroundCancellationTokenSource; bool _cleanDisconnectInitiated; volatile int _connectionStatus; - MqttClientDisconnectReason _disconnectReason; string _disconnectReasonString; Task _keepAlivePacketsSenderTask; - DateTime _lastPacketSentTimestamp; + MqttPacketDispatcher _packetDispatcher; Task _packetReceiverTask; - AsyncQueue _publishPacketReceiverQueue; Task _publishPacketReceiverTask; @@ -128,7 +124,7 @@ namespace MQTTnet.Client } _packetIdentifierProvider.Reset(); - _packetDispatcher.CancelAll(); + _packetDispatcher = new MqttPacketDispatcher(); _backgroundCancellationTokenSource = new CancellationTokenSource(); var backgroundCancellationToken = _backgroundCancellationTokenSource.Token; @@ -414,6 +410,10 @@ namespace MQTTnet.Client _publishPacketReceiverQueue = null; _adapter?.Dispose(); + _adapter = null; + + _packetDispatcher?.Dispose(); + _packetDispatcher = null; } } @@ -447,6 +447,8 @@ namespace MQTTnet.Client try { + _packetDispatcher.Dispose(new MqttClientDisconnectedException(exception)); + var receiverTask = WaitForTaskAsync(_packetReceiverTask, sender); var publishPacketReceiverTask = WaitForTaskAsync(_publishPacketReceiverTask, sender); var keepAliveTask = WaitForTaskAsync(_keepAlivePacketsSenderTask, sender); @@ -558,7 +560,7 @@ namespace MQTTnet.Client _disconnectReasonString = disconnectPacket.ReasonString; // 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); } @@ -776,7 +778,7 @@ namespace MQTTnet.Client } else { - _logger.Error(exception, "Error while receiving packets."); + _logger.Error(exception, $"Error while processing received packet ({packet.GetType().Name})."); } _packetDispatcher.FailAll(exception); diff --git a/Source/MQTTnet/Exceptions/MqttCommunicationException.cs b/Source/MQTTnet/Exceptions/MqttCommunicationException.cs index 6c6ba053..a78c1f7c 100644 --- a/Source/MQTTnet/Exceptions/MqttCommunicationException.cs +++ b/Source/MQTTnet/Exceptions/MqttCommunicationException.cs @@ -8,12 +8,8 @@ namespace MQTTnet.Exceptions { public class MqttCommunicationException : Exception { - protected MqttCommunicationException() - { - } - public MqttCommunicationException(Exception innerException) - : base(innerException.Message, innerException) + : base(innerException?.Message ?? "MQTT communication failed.", innerException) { } diff --git a/Source/MQTTnet/MQTTnet.csproj.DotSettings b/Source/MQTTnet/MQTTnet.csproj.DotSettings index 4af65895..a756552c 100644 --- a/Source/MQTTnet/MQTTnet.csproj.DotSettings +++ b/Source/MQTTnet/MQTTnet.csproj.DotSettings @@ -2,6 +2,7 @@ True True True + True True True True diff --git a/Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs b/Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs index dae92f73..85413a2c 100644 --- a/Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs +++ b/Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs @@ -2,48 +2,103 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using MQTTnet.Packets; using System; using System.Collections.Generic; +using MQTTnet.Packets; namespace MQTTnet.PacketDispatcher { - public sealed class MqttPacketDispatcher + public sealed class MqttPacketDispatcher : IDisposable { - readonly List _awaitables = new List(); + readonly List _waiters = new List(); - public void FailAll(Exception exception) + bool _isDisposed; + + public MqttPacketAwaitable AddAwaitable(ushort packetIdentifier) where TResponsePacket : MqttPacket { - if (exception == null) throw new ArgumentNullException(nameof(exception)); + var awaitable = new MqttPacketAwaitable(packetIdentifier, this); - lock (_awaitables) + lock (_waiters) { - foreach (var awaitable in _awaitables) - { - awaitable.Fail(exception); - } - - _awaitables.Clear(); + _waiters.Add(awaitable); } + + return awaitable; } public void CancelAll() { - lock (_awaitables) + lock (_waiters) { - foreach (var awaitable in _awaitables) + foreach (var awaitable in _waiters) { 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) { - if (packet == null) throw new ArgumentNullException(nameof(packet)); - + if (packet == null) + { + throw new ArgumentNullException(nameof(packet)); + } + ushort identifier = 0; if (packet is MqttPacketWithIdentifier packetWithIdentifier) { @@ -51,13 +106,15 @@ namespace MQTTnet.PacketDispatcher } var packetType = packet.GetType(); - var awaitables = new List(); - - lock (_awaitables) + var waiters = new List(); + + 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 // 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; } - - awaitables.Add(entry); - _awaitables.RemoveAt(i); + + waiters.Add(entry); + _waiters.RemoveAt(i); } } - - foreach (var matchingEntry in awaitables) + + foreach (var matchingEntry in waiters) { matchingEntry.Complete(packet); } - return awaitables.Count > 0; - } - - public MqttPacketAwaitable AddAwaitable(ushort packetIdentifier) where TResponsePacket : MqttPacket - { - var awaitable = new MqttPacketAwaitable(packetIdentifier, this); - - lock (_awaitables) - { - _awaitables.Add(awaitable); - } - - return awaitable; + return waiters.Count > 0; } - public void RemoveAwaitable(IMqttPacketAwaitable awaitable) + void ThrowIfDisposed() { - if (awaitable == null) throw new ArgumentNullException(nameof(awaitable)); - - lock (_awaitables) + if (_isDisposed) { - _awaitables.Remove(awaitable); + throw new ObjectDisposedException(nameof(MqttPacketDispatcher)); } } } From 97c46bea0f8b001671a3868e15c0805bf6853c5e Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sun, 6 Nov 2022 10:50:32 +0100 Subject: [PATCH 16/24] Update ReleaseNotes.md --- .github/workflows/ReleaseNotes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index 3ef3a30e..d08fa6b0 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -1,5 +1,5 @@ * [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). +* [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 issue in upgrading and downgrading of QoS levels for subscriptions and retained messages (#1560) * [Server] Fixed memory leak when old sessions are discarded (#1553) \ No newline at end of file From 5c7472838721ac81b48884314510b66b18ba579d Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sun, 6 Nov 2022 13:28:41 +0100 Subject: [PATCH 17/24] Fix client session takeover --- .../Mockups/ConnectionContextMockup.cs | 26 - .../Mockups/ConnectionHandlerMockup.cs | 22 +- .../Mockups/DuplexPipeMockup.cs | 7 +- .../Mockups/LimitedMemoryPool.cs | 2 +- .../Mockups/MemoryOwner.cs | 8 +- .../MqttConnectionContextTest.cs | 115 ++- .../ReaderExtensionsTest.cs | 2 +- ...MqttPacketSerialization_V3_Binary_Tests.cs | 3 +- .../MqttPacketSerialization_V3_Tests.cs | 2 +- .../MqttPacketSerialization_V5_Tests.cs | 2 +- .../Server/MqttSubscriptionsManager_Tests.cs | 4 +- .../Server/Subscription_TopicHash_Tests.cs | 722 +++++++++--------- Source/MQTTnet.Tests/TopicGenerator.cs | 3 +- .../Formatter/MqttUnsubAckPacketFactory.cs | 8 +- Source/MQTTnet/Internal/AsyncEvent.cs | 2 +- Source/MQTTnet/Server/Internal/MqttClient.cs | 35 +- .../Internal/MqttClientSessionsManager.cs | 63 +- .../MqttClientSubscriptionsManager.cs | 66 +- Source/MQTTnet/Server/Internal/MqttSession.cs | 50 +- .../Server/Internal/MqttSubscription.cs | 2 +- ...ubscribeResult.cs => UnsubscribeResult.cs} | 2 +- 21 files changed, 588 insertions(+), 558 deletions(-) delete mode 100644 Source/MQTTnet.AspNetCore.Tests/Mockups/ConnectionContextMockup.cs rename Source/MQTTnet/Server/Internal/{MqttUnsubscribeResult.cs => UnsubscribeResult.cs} (90%) diff --git a/Source/MQTTnet.AspNetCore.Tests/Mockups/ConnectionContextMockup.cs b/Source/MQTTnet.AspNetCore.Tests/Mockups/ConnectionContextMockup.cs deleted file mode 100644 index 0ec946de..00000000 --- a/Source/MQTTnet.AspNetCore.Tests/Mockups/ConnectionContextMockup.cs +++ /dev/null @@ -1,26 +0,0 @@ -// 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.Collections.Generic; -using System.IO.Pipelines; -using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Http.Features; - -namespace MQTTnet.AspNetCore.Tests.Mockups -{ - public class ConnectionContextMockup : ConnectionContext - { - public override string ConnectionId { get; set; } - - public override IFeatureCollection Features { get; } - - public override IDictionary Items { get; set; } - public override IDuplexPipe Transport { get; set; } - - public ConnectionContextMockup() - { - //Transport = new DefaultConnectionContext - } - } -} diff --git a/Source/MQTTnet.AspNetCore.Tests/Mockups/ConnectionHandlerMockup.cs b/Source/MQTTnet.AspNetCore.Tests/Mockups/ConnectionHandlerMockup.cs index 5f372c99..387e0bfb 100644 --- a/Source/MQTTnet.AspNetCore.Tests/Mockups/ConnectionHandlerMockup.cs +++ b/Source/MQTTnet.AspNetCore.Tests/Mockups/ConnectionHandlerMockup.cs @@ -2,20 +2,24 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.AspNetCore.Connections; -using MQTTnet.Adapter; -using MQTTnet.Formatter; -using MQTTnet.Server; using System; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using MQTTnet.Adapter; using MQTTnet.Diagnostics; +using MQTTnet.Formatter; +using MQTTnet.Server; namespace MQTTnet.AspNetCore.Tests.Mockups { - public class ConnectionHandlerMockup: IMqttServerAdapter + public sealed class ConnectionHandlerMockup : IMqttServerAdapter { - public TaskCompletionSource Context { get; } = new TaskCompletionSource(); public Func ClientHandler { get; set; } + public TaskCompletionSource Context { get; } = new TaskCompletionSource(); + + public void Dispose() + { + } public async Task OnConnectedAsync(ConnectionContext connection) { @@ -42,9 +46,5 @@ namespace MQTTnet.AspNetCore.Tests.Mockups { return Task.CompletedTask; } - - public void Dispose() - { - } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.AspNetCore.Tests/Mockups/DuplexPipeMockup.cs b/Source/MQTTnet.AspNetCore.Tests/Mockups/DuplexPipeMockup.cs index 24f85529..831b7b72 100644 --- a/Source/MQTTnet.AspNetCore.Tests/Mockups/DuplexPipeMockup.cs +++ b/Source/MQTTnet.AspNetCore.Tests/Mockups/DuplexPipeMockup.cs @@ -6,7 +6,7 @@ using System.IO.Pipelines; namespace MQTTnet.AspNetCore.Tests.Mockups { - public class DuplexPipeMockup : IDuplexPipe + public sealed class DuplexPipeMockup : IDuplexPipe { public DuplexPipeMockup() { @@ -20,9 +20,8 @@ namespace MQTTnet.AspNetCore.Tests.Mockups PipeWriter IDuplexPipe.Output => Send.Writer; + public Pipe Receive { get; } - public Pipe Receive { get; set; } - - public Pipe Send { get; set; } + public Pipe Send { get; } } } diff --git a/Source/MQTTnet.AspNetCore.Tests/Mockups/LimitedMemoryPool.cs b/Source/MQTTnet.AspNetCore.Tests/Mockups/LimitedMemoryPool.cs index c2f40502..242ba421 100644 --- a/Source/MQTTnet.AspNetCore.Tests/Mockups/LimitedMemoryPool.cs +++ b/Source/MQTTnet.AspNetCore.Tests/Mockups/LimitedMemoryPool.cs @@ -6,7 +6,7 @@ using System.Buffers; namespace MQTTnet.AspNetCore.Tests.Mockups { - public class LimitedMemoryPool : MemoryPool + public sealed class LimitedMemoryPool : MemoryPool { protected override void Dispose(bool disposing) { diff --git a/Source/MQTTnet.AspNetCore.Tests/Mockups/MemoryOwner.cs b/Source/MQTTnet.AspNetCore.Tests/Mockups/MemoryOwner.cs index 55e4e318..aad5e1c6 100644 --- a/Source/MQTTnet.AspNetCore.Tests/Mockups/MemoryOwner.cs +++ b/Source/MQTTnet.AspNetCore.Tests/Mockups/MemoryOwner.cs @@ -7,9 +7,9 @@ using System.Buffers; namespace MQTTnet.AspNetCore.Tests.Mockups { - public class MemoryOwner : IMemoryOwner + public sealed class MemoryOwner : IMemoryOwner { - private readonly byte[] _raw; + readonly byte[] _raw; public MemoryOwner(int size) { @@ -27,11 +27,11 @@ namespace MQTTnet.AspNetCore.Tests.Mockups Memory = _raw; } + public Memory Memory { get; } + public void Dispose() { ArrayPool.Shared.Return(_raw); } - - public Memory Memory { get; } } } \ No newline at end of file diff --git a/Source/MQTTnet.AspNetCore.Tests/MqttConnectionContextTest.cs b/Source/MQTTnet.AspNetCore.Tests/MqttConnectionContextTest.cs index 190ff485..a9b08a87 100644 --- a/Source/MQTTnet.AspNetCore.Tests/MqttConnectionContextTest.cs +++ b/Source/MQTTnet.AspNetCore.Tests/MqttConnectionContextTest.cs @@ -2,22 +2,21 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.VisualStudio.TestTools.UnitTesting; - using System; using System.Net; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Adapter; using MQTTnet.AspNetCore.Tests.Mockups; +using MQTTnet.Client; using MQTTnet.Exceptions; using MQTTnet.Formatter; using MQTTnet.Packets; -using MQTTnet.Client; using MQTTnet.Protocol; using MQTTnet.Tests.Extensions; @@ -26,20 +25,6 @@ namespace MQTTnet.AspNetCore.Tests [TestClass] public class MqttConnectionContextTest { - [TestMethod] - public async Task TestReceivePacketAsyncThrowsWhenReaderCompleted() - { - var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535)); - var pipe = new DuplexPipeMockup(); - var connection = new DefaultConnectionContext(); - connection.Transport = pipe; - var ctx = new MqttConnectionContext(serializer, connection); - - pipe.Receive.Writer.Complete(); - - await Assert.ThrowsExceptionAsync(() => ctx.ReceivePacketAsync(CancellationToken.None)); - } - [TestMethod] public async Task TestCorruptedConnectPacket() { @@ -49,13 +34,43 @@ namespace MQTTnet.AspNetCore.Tests var connection = new DefaultConnectionContext(); connection.Transport = pipe; var ctx = new MqttConnectionContext(serializer, connection); - + await pipe.Receive.Writer.WriteAsync(writer.AddMqttHeader(MqttControlPacketType.Connect, new byte[0])); await Assert.ThrowsExceptionAsync(() => ctx.ReceivePacketAsync(CancellationToken.None)); // the first exception should complete the pipes so if someone tries to use the connection after that it should throw immidiatly - await Assert.ThrowsExceptionAsync(() => ctx.ReceivePacketAsync(CancellationToken.None)); + await Assert.ThrowsExceptionAsync(() => ctx.ReceivePacketAsync(CancellationToken.None)); + } + + [TestMethod] + public async Task TestEndpoint() + { + var mockup = new ConnectionHandlerMockup(); + + + using (var host = new WebHostBuilder().UseKestrel(kestrel => kestrel.ListenLocalhost(1883, listener => listener.Use((ctx, next) => mockup.OnConnectedAsync(ctx)))) + .UseStartup() + .ConfigureServices( + (hostContext, services) => + { + services.AddHostedMqttServer(o => o.WithoutDefaultEndpoint()); + services.AddSingleton(mockup); + }) + .Build()) + + using (var client = new MqttFactory().CreateMqttClient()) + { + host.Start(); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost").Build(), CancellationToken.None); + + var ctx = await mockup.Context.Task; +#if NETCOREAPP3_1 + var ep = IPEndPoint.Parse(ctx.Endpoint); + Assert.IsNotNull(ep); +#endif + Assert.IsNotNull(ctx); + } } // COMMENTED OUT DUE TO DEAD LOCK? OR VERY VERY SLOW PERFORMANCE ON LOCAL DEV MACHINE. TEST WAS STILL RUNNING AFTER SEVERAL MINUTES! @@ -78,7 +93,7 @@ namespace MQTTnet.AspNetCore.Tests // await Task.WhenAll(tasks).ConfigureAwait(false); //} - + [TestMethod] public async Task TestLargePacket() { @@ -94,43 +109,25 @@ namespace MQTTnet.AspNetCore.Tests Assert.IsTrue(readResult.Buffer.Length > 20000); } - private class Startup + [TestMethod] + public async Task TestReceivePacketAsyncThrowsWhenReaderCompleted() + { + var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535)); + var pipe = new DuplexPipeMockup(); + var connection = new DefaultConnectionContext(); + connection.Transport = pipe; + var ctx = new MqttConnectionContext(serializer, connection); + + pipe.Receive.Writer.Complete(); + + await Assert.ThrowsExceptionAsync(() => ctx.ReceivePacketAsync(CancellationToken.None)); + } + + class Startup { public void Configure(IApplicationBuilder app) - { + { } } - - [TestMethod] - public async Task TestEndpoint() - { - var mockup = new ConnectionHandlerMockup(); - - - using (var host = new WebHostBuilder() - .UseKestrel(kestrel => kestrel.ListenLocalhost(1883, listener => listener.Use((ctx, next) => mockup.OnConnectedAsync(ctx)))) - .UseStartup() - .ConfigureServices((hostContext, services) => - { - services.AddHostedMqttServer(o => o.WithoutDefaultEndpoint()); - services.AddSingleton(mockup); - }) - .Build()) - - using (var client = new MqttFactory().CreateMqttClient()) - { - host.Start(); - await client.ConnectAsync(new MqttClientOptionsBuilder() - .WithTcpServer("localhost") - .Build(), CancellationToken.None); - - var ctx = await mockup.Context.Task; -#if NETCOREAPP3_1 - var ep = IPEndPoint.Parse(ctx.Endpoint); - Assert.IsNotNull(ep); -#endif - Assert.IsNotNull(ctx); - } - } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.AspNetCore.Tests/ReaderExtensionsTest.cs b/Source/MQTTnet.AspNetCore.Tests/ReaderExtensionsTest.cs index 056a6abd..d711067b 100644 --- a/Source/MQTTnet.AspNetCore.Tests/ReaderExtensionsTest.cs +++ b/Source/MQTTnet.AspNetCore.Tests/ReaderExtensionsTest.cs @@ -12,7 +12,7 @@ using MQTTnet.Packets; namespace MQTTnet.AspNetCore.Tests { [TestClass] - public class ReaderExtensionsTest + public sealed class ReaderExtensionsTest { [TestMethod] public void TestTryDeserialize() diff --git a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Binary_Tests.cs b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Binary_Tests.cs index 8260b3f4..673e46b3 100644 --- a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Binary_Tests.cs +++ b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Binary_Tests.cs @@ -13,13 +13,12 @@ using MQTTnet.Adapter; using MQTTnet.Diagnostics; using MQTTnet.Exceptions; using MQTTnet.Formatter; -using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; using MQTTnet.Tests.Extensions; using MQTTnet.Tests.Mockups; -namespace MQTTnet.Tests +namespace MQTTnet.Tests.Formatter { [TestClass] public sealed class MqttPacketSerialization_V3_Binary_Tests diff --git a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Tests.cs b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Tests.cs index 5ae86cc1..e1135529 100644 --- a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Tests.cs +++ b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Tests.cs @@ -10,7 +10,7 @@ using MQTTnet.Formatter; using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Tests +namespace MQTTnet.Tests.Formatter { [TestClass] public sealed class MqttPacketSerialization_V3_Tests diff --git a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V5_Tests.cs b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V5_Tests.cs index 1d9e9740..fd7a70e2 100644 --- a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V5_Tests.cs +++ b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V5_Tests.cs @@ -9,7 +9,7 @@ using MQTTnet.Formatter; using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Tests +namespace MQTTnet.Tests.Formatter { [TestClass] public sealed class MqttPacketSerialization_V5_Tests diff --git a/Source/MQTTnet.Tests/Server/MqttSubscriptionsManager_Tests.cs b/Source/MQTTnet.Tests/Server/MqttSubscriptionsManager_Tests.cs index ea3e743c..19a4f427 100644 --- a/Source/MQTTnet.Tests/Server/MqttSubscriptionsManager_Tests.cs +++ b/Source/MQTTnet.Tests/Server/MqttSubscriptionsManager_Tests.cs @@ -130,9 +130,7 @@ namespace MQTTnet.Tests.Server CheckSubscriptionsResult CheckSubscriptions(string topic, MqttQualityOfServiceLevel applicationMessageQoSLevel, string senderClientId) { - ulong topicHashMask; // not needed - bool hasWildcard; // not needed - MqttSubscription.CalculateTopicHash(topic, out var topicHash, out topicHashMask, out hasWildcard); + MqttSubscription.CalculateTopicHash(topic, out var topicHash, out _, out _); return _subscriptionsManager.CheckSubscriptions(topic, topicHash, applicationMessageQoSLevel, senderClientId); } } diff --git a/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs b/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs index 70e9bffd..31e1ab05 100644 --- a/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Packets; using MQTTnet.Protocol; using MQTTnet.Server; using MQTTnet.Tests.Mockups; @@ -13,43 +15,299 @@ namespace MQTTnet.Tests.Server [TestClass] public sealed class SubscriptionTopicHashTests { - enum TopicHashSelector { NoWildcard, SingleWildcard, MultiWildcard }; - MqttSession _clientSession; + /// + /// Even fairly regularly named topics as generated by the topic generator should result in shallow hash buckets + /// + /// [TestMethod] - public void Match_Hash_Test_SingleWildCard() + public void Check_Hash_Bucket_Depth() { - var l0 = "pub0"; - var l1 = "topic1"; - var l2 = "+"; - var l3 = "prop1"; - var topic = $"{l0}/{l1}/{l2}/{l3}"; + const int numPublishers = 5000; + const int numTopicsPerPublisher = 10; + + TopicGenerator.Generate( + numPublishers, + numTopicsPerPublisher, + out var topicsByPublisher, + out _, + out _); + + // There will be many 'similar' topics ending with, i.e. "sensor100", "sensor101", ... + // Hash bucket depths should remain low. + var bucketDepths = new Dictionary(); + var maxBucketDepth = 0; + ulong maxBucketDepthHash = 0; + + var topicsByHash = new Dictionary>(); + + foreach (var t in topicsByPublisher) + { + var topics = t.Value; + foreach (var topic in topics) + { + MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var hashMask, out var hasWildcard); + + bucketDepths.TryGetValue(topicHash, out var currentValue); + ++currentValue; + bucketDepths[topicHash] = currentValue; + + if (currentValue > maxBucketDepth) + { + maxBucketDepth = currentValue; + maxBucketDepthHash = topicHash; + } + + if (!topicsByHash.TryGetValue(topicHash, out var topicList)) + { + topicList = new List(); + topicsByHash.Add(topicHash, topicList); + } + + topicList.Add(topic); + } + } + + var maxDepthTopics = topicsByHash[maxBucketDepthHash]; + + Console.Write("Max bucket depth is " + maxBucketDepth); + + // for the test case the bucket depth should be less than 100 + Assert.IsTrue(maxBucketDepth < 100, "Unexpected high topic hash bucket depth"); + } + + + [TestMethod] + public void Check_Selected_Topic_Hashes() + { + CheckTopicHash("client1/building1/level1/sensor1", 0x655D4AF100000000, 0xFFFFFFFFFFFFFFFF); + CheckTopicHash("client1/building1/+/sensor1", 0x655D00F100000000, 0xFFFF00FFFFFFFFFF); + CheckTopicHash("client1/+/level1/+", 0x65004A0000000000, 0xFF00FF00FFFFFFFF); + CheckTopicHash("client1/building1/level1/#", 0x655D4A0000000000, 0xFFFFFF0000000000); + CheckTopicHash("client1/+/level1/#", 0x65004A0000000000, 0xFF00FF0000000000); + } + + /// + /// Test long topic name with last level being # + /// + [TestMethod] + public void Match_Hash_Test_LongTopic_DetectMultiLevelWildcard() + { + var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/#"; + + MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + + Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); + } + + + /// + /// Test long topic name with last level being + + /// + [TestMethod] + public void Match_Hash_Test_LongTopic_DetectSingleLevelWildcard() + { + var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/+"; + + MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + + Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); + } + + /// + /// Test long topic name with last level being # + /// + [TestMethod] + public void Match_Hash_Test_LongTopic_MultiWildcard() + { + var sb = new StringBuilder(); + const int numLevels = 8; + var levelNames = new string[numLevels]; + for (var i = 0; i < numLevels; ++i) + { + if (i > 0) + { + sb.Append("/"); + } + + string levelName; + if (i == numLevels - 1) + { + // last one is # + levelName = "#"; + } + else + { + levelName = "level" + i; + } + + levelNames[i] = levelName; + sb.Append(levelName); + } + + var topic = sb.ToString(); + + // UInt64 is limited to 8 levels MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); var hashBytes = GetBytes(topicHash); - Assert.AreNotEqual(hashBytes[0], 0, "checksum 0 mismatch"); - Assert.AreNotEqual(hashBytes[1], 0, "checksum 1 mismatch"); - Assert.AreEqual(hashBytes[2], 0, "checksum 2 mismatch"); - Assert.AreNotEqual(hashBytes[3], 0, "checksum 3 mismatch"); - Assert.AreEqual(hashBytes[4], 0, "checksum 4 mismatch"); - Assert.AreEqual(hashBytes[5], 0, "checksum 5 mismatch"); - Assert.AreEqual(hashBytes[6], 0, "checksum 6 mismatch"); - Assert.AreEqual(hashBytes[7], 0, "checksum 7 mismatch"); + // all bytes should contain checksum + var count = 0; + foreach (var h in hashBytes) + { + if (count < 7) + { + Assert.AreNotEqual(h, 0, "checksum mismatch"); + } + else + { + Assert.AreEqual(h, 0, "checksum mismatch"); + } - // The mask should have zeroes where the wildcard and ff at the end + ++count; + } + + // The mask should have ff except for last level var hashMaskBytes = GetBytes(topicHashMask); - Assert.AreEqual(hashMaskBytes[0], 0xff, "mask 0 mismatch"); - Assert.AreEqual(hashMaskBytes[1], 0xff, "mask 1 mismatch"); - Assert.AreEqual(hashMaskBytes[2], 0, "mask 2 mismatch"); - Assert.AreEqual(hashMaskBytes[3], 0xff, "mask 3 mismatch"); - Assert.AreEqual(hashMaskBytes[4], 0xff, "mask 4 mismatch"); - Assert.AreEqual(hashMaskBytes[5], 0xff, "mask 5 mismatch"); - Assert.AreEqual(hashMaskBytes[6], 0xff, "mask 6 mismatch"); - Assert.AreEqual(hashMaskBytes[7], 0xff, "mask 7 mismatch"); + count = 0; + foreach (var h in hashMaskBytes) + { + if (count < 7) + { + Assert.AreEqual(h, 0xff, "mask mismatch"); + } + else + { + Assert.AreEqual(h, 0, "last mask mismatch"); + } + + ++count; + } + } + + /// + /// Test long topic name exceeding 8 levels + /// + [TestMethod] + public void Match_Hash_Test_LongTopic_NoWildCard() + { + var sb = new StringBuilder(); + const int numLevels = 9; + var levelNames = new string[numLevels]; + for (var i = 0; i < numLevels; ++i) + { + if (i > 0) + { + sb.Append("/"); + } + + var levelName = "level" + i; + levelNames[i] = levelName; + sb.Append(levelName); + } + + var topic = sb.ToString(); + + MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + + Assert.IsFalse(topicHasWildcard, "Wildcard detected when not present"); + + + var hashBytes = GetBytes(topicHash); + // all bytes should contain checksum + var count = 0; + foreach (var h in hashBytes) + { + Assert.AreNotEqual(h, 0, "checksum mismatch"); + ++count; + } + + // The mask should have ff + var hashMaskBytes = GetBytes(topicHashMask); + count = 0; + foreach (var h in hashMaskBytes) + { + Assert.AreEqual(h, 0xff, "mask mismatch"); + ++count; + } + } + + /// + /// Test long topic name with last level being + + /// + [TestMethod] + public void Match_Hash_Test_LongTopic_SingleWildCard() + { + var sb = new StringBuilder(); + const int numLevels = 8; + var levelNames = new string[numLevels]; + for (var i = 0; i < numLevels; ++i) + { + if (i > 0) + { + sb.Append("/"); + } + + string levelName; + if (i == numLevels - 1) + { + // last one is + + levelName = "+"; + } + else + { + levelName = "level" + i; + } + + levelNames[i] = levelName; + sb.Append(levelName); + } + + var topic = sb.ToString(); + + MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); + + Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); + + + var hashBytes = GetBytes(topicHash); + // all bytes should contain checksum + var count = 0; + foreach (var h in hashBytes) + { + if (count < 7) + { + Assert.AreNotEqual(h, 0, "checksum mismatch"); + } + else + { + // wildcard position + Assert.AreEqual(h, 0, "checksum mismatch"); + } + + ++count; + } + + // The mask should have ff + var hashMaskBytes = GetBytes(topicHashMask); + count = 0; + foreach (var h in hashMaskBytes) + { + if (count < 7) + { + Assert.AreEqual(h, 0xff, "mask mismatch"); + } + else + { + Assert.AreEqual(h, 0, "last mask mismatch"); + } + + ++count; + } } [TestMethod] @@ -123,205 +381,13 @@ namespace MQTTnet.Tests.Server Assert.AreEqual(hashMaskBytes[7], 0xff, "mask 7 mismatch"); } - /// - /// Test long topic name with last level being # - /// [TestMethod] - public void Match_Hash_Test_LongTopic_MultiWildcard() + public async Task Match_Hash_Test_Search_MultiWildcard() { - var sb = new StringBuilder(); - const int numLevels = 8; - var levelNames = new string[numLevels]; - for (var i = 0; i < numLevels; ++i) - { - if (i > 0) - sb.Append("/"); - string levelName; - if (i == numLevels - 1) - { - // last one is # - levelName = "#"; - } - else - { - levelName = "level" + i; - } - levelNames[i] = levelName; - sb.Append(levelName); - } - var topic = sb.ToString(); - - // UInt64 is limited to 8 levels - - MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); - - Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); - - var hashBytes = GetBytes(topicHash); - // all bytes should contain checksum - int count = 0; - foreach (var h in hashBytes) - { - if (count < 7) - { - Assert.AreNotEqual(h, 0, "checksum mismatch"); - } - else - { - Assert.AreEqual(h, 0, "checksum mismatch"); - } - ++count; - } - // The mask should have ff except for last level - var hashMaskBytes = GetBytes(topicHashMask); - count = 0; - foreach (var h in hashMaskBytes) - { - if (count < 7) - { - Assert.AreEqual(h, 0xff, "mask mismatch"); - } - else - { - Assert.AreEqual(h, 0, "last mask mismatch"); - } - ++count; - } - } - - /// - /// Test long topic name with last level being + - /// - [TestMethod] - public void Match_Hash_Test_LongTopic_SingleWildCard() - { - var sb = new StringBuilder(); - const int numLevels = 8; - var levelNames = new string[numLevels]; - for (var i = 0; i < numLevels; ++i) - { - if (i > 0) - sb.Append("/"); - string levelName; - if (i == numLevels - 1) - { - // last one is + - levelName = "+"; - } - else - { - levelName = "level" + i; - } - levelNames[i] = levelName; - sb.Append(levelName); - } - var topic = sb.ToString(); - - MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); - - Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); - - - var hashBytes = GetBytes(topicHash); - // all bytes should contain checksum - int count = 0; - foreach (var h in hashBytes) - { - if (count < 7) - { - Assert.AreNotEqual(h, 0, "checksum mismatch"); - } - else - { - // wildcard position - Assert.AreEqual(h, 0, "checksum mismatch"); - } - ++count; - } - // The mask should have ff - var hashMaskBytes = GetBytes(topicHashMask); - count = 0; - foreach (var h in hashMaskBytes) - { - if (count < 7) - { - Assert.AreEqual(h, 0xff, "mask mismatch"); - } - else - { - Assert.AreEqual(h, 0, "last mask mismatch"); - } - ++count; - } - } - - /// - /// Test long topic name exceeding 8 levels - /// - [TestMethod] - public void Match_Hash_Test_LongTopic_NoWildCard() - { - var sb = new StringBuilder(); - const int numLevels = 9; - var levelNames = new string[numLevels]; - for(var i=0; i < numLevels; ++i) - { - if (i > 0) - sb.Append("/"); - var levelName = "level" + i; - levelNames[i] = levelName; - sb.Append(levelName); - } - var topic = sb.ToString(); - - MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); - - Assert.IsFalse(topicHasWildcard, "Wildcard detected when not present"); - - - var hashBytes = GetBytes(topicHash); - // all bytes should contain checksum - int count = 0; - foreach(var h in hashBytes) - { - Assert.AreNotEqual(h, 0, "checksum mismatch"); - ++count; - } - // The mask should have ff - var hashMaskBytes = GetBytes(topicHashMask); - count = 0; - foreach (var h in hashMaskBytes) - { - Assert.AreEqual(h, 0xff, "mask mismatch"); - ++count; - } - } - - - /// - /// Test long topic name with last level being + - /// - [TestMethod] - public void Match_Hash_Test_LongTopic_DetectSingleLevelWildcard() - { - var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/+"; - - MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); - - Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); - } - - /// - /// Test long topic name with last level being # - /// - [TestMethod] - public void Match_Hash_Test_LongTopic_DetectMultiLevelWildcard() - { - var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/#"; - - MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); - - Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); + var topics = await PrepareTopicHashSubscriptions(TopicHashSelector.MultiWildcard); + var matchCount = CheckTopicSubscriptions(_clientSession, topics, "Multi Wildcard"); + // Should match all topics + Assert.AreEqual(topics.Count, matchCount, "Topics not matched"); } [TestMethod] @@ -373,77 +439,38 @@ namespace MQTTnet.Tests.Server } [TestMethod] - public async Task Match_Hash_Test_Search_MultiWildcard() + public void Match_Hash_Test_SingleWildCard() { - var topics = await PrepareTopicHashSubscriptions(TopicHashSelector.MultiWildcard); - var matchCount = CheckTopicSubscriptions(_clientSession, topics, "Multi Wildcard"); - // Should match all topics - Assert.AreEqual(topics.Count, matchCount, "Topics not matched"); - } + var l0 = "pub0"; + var l1 = "topic1"; + var l2 = "+"; + var l3 = "prop1"; + var topic = $"{l0}/{l1}/{l2}/{l3}"; - /// - /// Even fairly regularly named topics as generated by the topic generator should result in shallow hash buckets - /// - /// - [TestMethod] - public void Check_Hash_Bucket_Depth() - { - const int numPublishers = 5000; - const int numTopicsPerPublisher = 10; + MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var topicHashMask, out var topicHasWildcard); - TopicGenerator.Generate(numPublishers, numTopicsPerPublisher, out var topicsByPublisher, out var singleWildcardTopicsByPublisher, out var multiWildcardTopicsByPublisher); + Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); - // There will be many 'similar' topics ending with, i.e. "sensor100", "sensor101", ... - // Hash bucket depths should remain low. - var bucketDepths = new Dictionary(); - int maxBucketDepth = 0; - UInt64 maxBucketDepthHash = 0; + var hashBytes = GetBytes(topicHash); + Assert.AreNotEqual(hashBytes[0], 0, "checksum 0 mismatch"); + Assert.AreNotEqual(hashBytes[1], 0, "checksum 1 mismatch"); + Assert.AreEqual(hashBytes[2], 0, "checksum 2 mismatch"); + Assert.AreNotEqual(hashBytes[3], 0, "checksum 3 mismatch"); + Assert.AreEqual(hashBytes[4], 0, "checksum 4 mismatch"); + Assert.AreEqual(hashBytes[5], 0, "checksum 5 mismatch"); + Assert.AreEqual(hashBytes[6], 0, "checksum 6 mismatch"); + Assert.AreEqual(hashBytes[7], 0, "checksum 7 mismatch"); - var topicsByHash = new Dictionary>(); - - foreach (var t in topicsByPublisher) - { - var topics = t.Value; - foreach (var topic in topics) - { - MqttSubscription.CalculateTopicHash(topic, out var topicHash, out var hashMask, out var hasWildcard); - - bucketDepths.TryGetValue(topicHash, out var currentValue); - ++currentValue; - bucketDepths[topicHash] = currentValue; - - if (currentValue > maxBucketDepth) - { - maxBucketDepth = currentValue; - maxBucketDepthHash = topicHash; - } - - if (!topicsByHash.TryGetValue(topicHash, out var topicList)) - { - topicList = new List(); - topicsByHash.Add(topicHash, topicList); - } - topicList.Add(topic); - } - } - - var maxDepthTopics = topicsByHash[maxBucketDepthHash]; - - Console.Write("Max bucket depth is " + maxBucketDepth); - - // for the test case the bucket depth should be less than 100 - Assert.IsTrue(maxBucketDepth < 100, "Unexpected high topic hash bucket depth"); - } - - - [TestMethod] - public void Check_Selected_Topic_Hashes() - { - CheckTopicHash("client1/building1/level1/sensor1", 0x655D4AF100000000, 0xFFFFFFFFFFFFFFFF); - CheckTopicHash("client1/building1/+/sensor1", 0x655D00F100000000, 0xFFFF00FFFFFFFFFF); - CheckTopicHash("client1/+/level1/+", 0x65004A0000000000, 0xFF00FF00FFFFFFFF); - CheckTopicHash("client1/building1/level1/#", 0x655D4A0000000000, 0xFFFFFF0000000000); - CheckTopicHash("client1/+/level1/#", 0x65004A0000000000, 0xFF00FF0000000000); + // The mask should have zeroes where the wildcard and ff at the end + var hashMaskBytes = GetBytes(topicHashMask); + Assert.AreEqual(hashMaskBytes[0], 0xff, "mask 0 mismatch"); + Assert.AreEqual(hashMaskBytes[1], 0xff, "mask 1 mismatch"); + Assert.AreEqual(hashMaskBytes[2], 0, "mask 2 mismatch"); + Assert.AreEqual(hashMaskBytes[3], 0xff, "mask 3 mismatch"); + Assert.AreEqual(hashMaskBytes[4], 0xff, "mask 4 mismatch"); + Assert.AreEqual(hashMaskBytes[5], 0xff, "mask 5 mismatch"); + Assert.AreEqual(hashMaskBytes[6], 0xff, "mask 6 mismatch"); + Assert.AreEqual(hashMaskBytes[7], 0xff, "mask 7 mismatch"); } void CheckTopicHash(string topic, ulong expectedHash, ulong expectedHashMask) @@ -459,13 +486,66 @@ namespace MQTTnet.Tests.Server Assert.AreEqual(expectedHashMask, hashMask, "Topic hash mask not as expected"); } + int CheckTopicSubscriptions(MqttSession clientSession, List topicsToFind, string subject) + { + var matchCount = 0; + + { + var resultCount = 0; + + var stopWatch = new Stopwatch(); + stopWatch.Start(); + var countUp = 0; + var countDown = topicsToFind.Count - 1; + for (; countUp < topicsToFind.Count; ++countUp, --countDown) + { + var topicToFind = topicsToFind[countDown]; + + MqttSubscription.CalculateTopicHash(topicToFind, out var topicHash, out _, out _); + + clientSession.TryCheckSubscriptions(topicToFind, topicHash, MqttQualityOfServiceLevel.AtMostOnce, "OtherClient", out var result); + if (result.IsSubscribed) + { + ++matchCount; + } + + ++resultCount; + } + + stopWatch.Stop(); + + Console.Write("Match count: " + matchCount + "; "); + + Console.WriteLine(subject + " lookup milliseconds: " + stopWatch.ElapsedMilliseconds); + } + + return matchCount; + } + + byte[] GetBytes(ulong value) + { + var bytes = BitConverter.GetBytes(value); + // Ensure that highest byte comes first for comparison left to right + if (BitConverter.IsLittleEndian) + { + return bytes.Reverse().ToArray(); + } + + return bytes; + } + async Task> PrepareTopicHashSubscriptions(TopicHashSelector selector) { const int numPublishers = 1; const int numTopicsPerPublisher = 10000; - TopicGenerator.Generate(numPublishers, numTopicsPerPublisher, out var topicsByPublisher, out var singleWildcardTopicsByPublisher, out var multiWildcardTopicsByPublisher); + TopicGenerator.Generate( + numPublishers, + numTopicsPerPublisher, + out var topicsByPublisher, + out var singleWildcardTopicsByPublisher, + out var multiWildcardTopicsByPublisher); var topics = topicsByPublisher.FirstOrDefault().Value; var singleWildcardTopics = singleWildcardTopicsByPublisher.FirstOrDefault().Value; @@ -477,15 +557,7 @@ namespace MQTTnet.Tests.Server var eventContainer = new MqttServerEventContainer(); var retainedMessagesManager = new MqttRetainedMessagesManager(eventContainer, logger); var sessionManager = new MqttClientSessionsManager(serverOptions, retainedMessagesManager, eventContainer, logger); - _clientSession = new MqttSession( - clientId, - false, - new Dictionary(), - serverOptions, - eventContainer, - retainedMessagesManager, - sessionManager - ); + _clientSession = new MqttSession(clientId, false, new Dictionary(), serverOptions, eventContainer, retainedMessagesManager, sessionManager); List topicsToSubscribe; @@ -501,62 +573,26 @@ namespace MQTTnet.Tests.Server topicsToSubscribe = topics; break; } + foreach (var t in topicsToSubscribe) { - var subPacket = new Packets.MqttSubscribePacket(); - var filter = new Packets.MqttTopicFilter + var subPacket = new MqttSubscribePacket(); + var filter = new MqttTopicFilter { Topic = t }; subPacket.TopicFilters.Add(filter); - await _clientSession.SubscriptionsManager.Subscribe(subPacket, default); + await _clientSession.Subscribe(subPacket, default); } return topics; } - int CheckTopicSubscriptions(MqttSession clientSession, List topicsToFind, string subject) + enum TopicHashSelector { - var matchCount = 0; - - { - int resultCount = 0; - - var stopWatch = new System.Diagnostics.Stopwatch(); - stopWatch.Start(); - var countUp = 0; - var countDown = topicsToFind.Count - 1; - for (; countUp < topicsToFind.Count; ++countUp, --countDown) - { - var topicToFind = topicsToFind[countDown]; - - MqttSubscription.CalculateTopicHash(topicToFind, out var topicHash, out var hashMask, out var hasWildcard); - - var result = clientSession.SubscriptionsManager.CheckSubscriptions(topicToFind, topicHash, MqttQualityOfServiceLevel.AtMostOnce, "OtherClient"); - if (result.IsSubscribed) - { - ++matchCount; - } - ++resultCount; - } - - stopWatch.Stop(); - - Console.Write("Match count: " + matchCount + "; "); - - Console.WriteLine(subject + " lookup milliseconds: " + stopWatch.ElapsedMilliseconds); - } - - return matchCount; - } - - byte[] GetBytes(UInt64 value) - { - var bytes = BitConverter.GetBytes(value); - // Ensure that highest byte comes first for comparison left to right - if (BitConverter.IsLittleEndian) - return bytes.Reverse().ToArray(); - return bytes; + NoWildcard, + SingleWildcard, + MultiWildcard } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/TopicGenerator.cs b/Source/MQTTnet.Tests/TopicGenerator.cs index 64ae1144..ddfbe709 100644 --- a/Source/MQTTnet.Tests/TopicGenerator.cs +++ b/Source/MQTTnet.Tests/TopicGenerator.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; -using System.Text; -namespace MQTTnet.Tests.Server +namespace MQTTnet.Tests { public class TopicGenerator { diff --git a/Source/MQTTnet/Formatter/MqttUnsubAckPacketFactory.cs b/Source/MQTTnet/Formatter/MqttUnsubAckPacketFactory.cs index 18da46a1..88cbbbc6 100644 --- a/Source/MQTTnet/Formatter/MqttUnsubAckPacketFactory.cs +++ b/Source/MQTTnet/Formatter/MqttUnsubAckPacketFactory.cs @@ -10,16 +10,16 @@ namespace MQTTnet.Formatter { public sealed class MqttUnsubAckPacketFactory { - public MqttUnsubAckPacket Create(MqttUnsubscribePacket unsubscribePacket, MqttUnsubscribeResult mqttUnsubscribeResult) + public MqttUnsubAckPacket Create(MqttUnsubscribePacket unsubscribePacket, UnsubscribeResult unsubscribeResult) { if (unsubscribePacket == null) { throw new ArgumentNullException(nameof(unsubscribePacket)); } - if (mqttUnsubscribeResult == null) + if (unsubscribeResult == null) { - throw new ArgumentNullException(nameof(mqttUnsubscribeResult)); + throw new ArgumentNullException(nameof(unsubscribeResult)); } var unsubAckPacket = new MqttUnsubAckPacket @@ -28,7 +28,7 @@ namespace MQTTnet.Formatter }; // MQTTv5.0.0 only. - unsubAckPacket.ReasonCodes = mqttUnsubscribeResult.ReasonCodes; + unsubAckPacket.ReasonCodes = unsubscribeResult.ReasonCodes; return unsubAckPacket; } diff --git a/Source/MQTTnet/Internal/AsyncEvent.cs b/Source/MQTTnet/Internal/AsyncEvent.cs index 8e988d75..e7adc7b5 100644 --- a/Source/MQTTnet/Internal/AsyncEvent.cs +++ b/Source/MQTTnet/Internal/AsyncEvent.cs @@ -123,7 +123,7 @@ namespace MQTTnet.Internal } catch (Exception exception) { - logger.Warning(exception, $"Error while invoking event ({typeof(TEventArgs)})."); + logger.Warning(exception, $"Error while invoking event with arguments of type {typeof(TEventArgs)}."); } } } diff --git a/Source/MQTTnet/Server/Internal/MqttClient.cs b/Source/MQTTnet/Server/Internal/MqttClient.cs index 1bc51152..674a262f 100644 --- a/Source/MQTTnet/Server/Internal/MqttClient.cs +++ b/Source/MQTTnet/Server/Internal/MqttClient.cs @@ -33,6 +33,7 @@ namespace MQTTnet.Server readonly Dictionary _topicAlias = new Dictionary(); CancellationTokenSource _cancellationToken; + bool _disconnectPacketSent; public MqttClient( MqttConnectPacket connectPacket, @@ -146,19 +147,22 @@ namespace MQTTnet.Server { IsRunning = false; - // Sending DISCONNECT packets from the server to the client is only supported when using MQTTv5+. - if (ChannelAdapter.PacketFormatterAdapter.ProtocolVersion == MqttProtocolVersion.V500) + if (!_disconnectPacketSent) { - // The Client or Server MAY send a DISCONNECT packet before closing the Network Connection. - // This library does not sent a DISCONNECT packet for a normal disconnection. Maybe adding - // a configuration option is requested in the future. - if (reason != MqttDisconnectReasonCode.NormalDisconnection) + // Sending DISCONNECT packets from the server to the client is only supported when using MQTTv5+. + if (ChannelAdapter.PacketFormatterAdapter.ProtocolVersion == MqttProtocolVersion.V500) { - // Is is very important to send the DISCONNECT packet here BEFORE cancelling the - // token because the entire connection is closed (disposed) as soon as the cancellation - // token is cancelled. To there is no chance that the DISCONNECT packet will ever arrive - // at the client! - await TrySendDisconnectPacket(reason).ConfigureAwait(false); + // The Client or Server MAY send a DISCONNECT packet before closing the Network Connection. + // This library does not sent a DISCONNECT packet for a normal disconnection. Maybe adding + // a configuration option is requested in the future. + if (reason != MqttDisconnectReasonCode.NormalDisconnection) + { + // Is is very important to send the DISCONNECT packet here BEFORE cancelling the + // token because the entire connection is closed (disposed) as soon as the cancellation + // token is cancelled. To there is no chance that the DISCONNECT packet will ever arrive + // at the client! + await TrySendDisconnectPacket(reason).ConfigureAwait(false); + } } } @@ -282,7 +286,7 @@ namespace MQTTnet.Server async Task HandleIncomingSubscribePacket(MqttSubscribePacket subscribePacket, CancellationToken cancellationToken) { - var subscribeResult = await Session.SubscriptionsManager.Subscribe(subscribePacket, cancellationToken).ConfigureAwait(false); + var subscribeResult = await Session.Subscribe(subscribePacket, cancellationToken).ConfigureAwait(false); var subAckPacket = _packetFactories.SubAck.Create(subscribePacket, subscribeResult); @@ -306,7 +310,7 @@ namespace MQTTnet.Server async Task HandleIncomingUnsubscribePacket(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken) { - var unsubscribeResult = await Session.SubscriptionsManager.Unsubscribe(unsubscribePacket, cancellationToken).ConfigureAwait(false); + var unsubscribeResult = await Session.Unsubscribe(unsubscribePacket, cancellationToken).ConfigureAwait(false); var unsubAckPacket = _packetFactories.UnsubAck.Create(unsubscribePacket, unsubscribeResult); @@ -484,7 +488,7 @@ namespace MQTTnet.Server try { - while (!cancellationToken.IsCancellationRequested) + while (!cancellationToken.IsCancellationRequested && !IsTakenOver && IsRunning) { packetBusItem = await Session.DequeuePacketAsync(cancellationToken).ConfigureAwait(false); @@ -566,6 +570,9 @@ namespace MQTTnet.Server { try { + // This also indicates that it was tried at least! + _disconnectPacketSent = true; + var disconnectPacket = _packetFactories.Disconnect.Create(reasonCode); using (var timeout = new CancellationTokenSource(_serverOptions.DefaultCommunicationTimeout)) diff --git a/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs b/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs index 87fcdd34..186a15ca 100644 --- a/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs +++ b/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs @@ -150,11 +150,17 @@ namespace MQTTnet.Server foreach (var session in subscriberSessions) { - var checkSubscriptionsResult = session.SubscriptionsManager.CheckSubscriptions( + if(!session.TryCheckSubscriptions( applicationMessage.Topic, topicHash, applicationMessage.QualityOfServiceLevel, - senderId); + senderId, + out var checkSubscriptionsResult)) + { + // Checking the subscriptions has failed for the session. The session + // will be ignored. + continue; + } if (!checkSubscriptionsResult.IsSubscribed) { @@ -301,7 +307,7 @@ namespace MQTTnet.Server channelAdapter.Endpoint, client.Session.Items); - await _eventContainer.ClientConnectedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + await _eventContainer.ClientConnectedEvent.TryInvokeAsync(eventArgs, _logger).ConfigureAwait(false); } await client.RunAsync().ConfigureAwait(false); @@ -414,7 +420,7 @@ namespace MQTTnet.Server var clientSession = GetClientSession(clientId); - var subscribeResult = await clientSession.SubscriptionsManager.Subscribe(fakeSubscribePacket, CancellationToken.None).ConfigureAwait(false); + var subscribeResult = await clientSession.Subscribe(fakeSubscribePacket, CancellationToken.None).ConfigureAwait(false); if (subscribeResult.RetainedMessages != null) { @@ -441,7 +447,7 @@ namespace MQTTnet.Server var fakeUnsubscribePacket = new MqttUnsubscribePacket(); fakeUnsubscribePacket.TopicFilters.AddRange(topicFilters); - return GetClientSession(clientId).SubscriptionsManager.Unsubscribe(fakeUnsubscribePacket, CancellationToken.None); + return GetClientSession(clientId).Unsubscribe(fakeUnsubscribePacket, CancellationToken.None); } MqttClient CreateClient(MqttConnectPacket connectPacket, IMqttChannelAdapter channelAdapter, MqttSession session) @@ -484,10 +490,14 @@ namespace MQTTnet.Server using (await _createConnectionSyncRoot.EnterAsync().ConfigureAwait(false)) { - MqttSession session; MqttSession oldSession; + MqttClient oldClient; + lock (_sessionsManagementLock) { + MqttSession session; + + // Create a new session (if required). if (!_sessions.TryGetValue(connectPacket.ClientId, out oldSession)) { session = CreateSession(connectPacket.ClientId, validatingConnectionEventArgs.SessionItems, sessionShouldPersist); @@ -505,6 +515,7 @@ namespace MQTTnet.Server _logger.Verbose("Reusing existing session of client '{0}'.", connectPacket.ClientId); session = oldSession; oldSession = null; + // Session persistence could change for MQTT 5 clients that reconnect with different SessionExpiryInterval session.IsPersistent = sessionShouldPersist; connAckPacket.IsSessionPresent = true; @@ -513,39 +524,41 @@ namespace MQTTnet.Server } _sessions[connectPacket.ClientId] = session; + + // Create a new client (always required). + _clients.TryGetValue(connectPacket.ClientId, out oldClient); + if (oldClient != null) + { + // This will stop the current client from sending and receiving but remains the connection active + // for a later DISCONNECT packet. + oldClient.IsTakenOver = true; + } + + client = CreateClient(connectPacket, channelAdapter, session); + + _clients[connectPacket.ClientId] = client; } if (!connAckPacket.IsSessionPresent) { // TODO: This event is not yet final. It can already be used but restoring sessions from storage will be added later! var preparingSessionEventArgs = new PreparingSessionEventArgs(); - await _eventContainer.PreparingSessionEvent.InvokeAsync(preparingSessionEventArgs).ConfigureAwait(false); + await _eventContainer.PreparingSessionEvent.TryInvokeAsync(preparingSessionEventArgs, _logger).ConfigureAwait(false); } - - MqttClient existingClient; - - lock (_clients) + + if (oldClient != null) { - _clients.TryGetValue(connectPacket.ClientId, out existingClient); - client = CreateClient(connectPacket, channelAdapter, session); - - _clients[connectPacket.ClientId] = client; - } - - if (existingClient != null) - { - existingClient.IsTakenOver = true; - await existingClient.StopAsync(MqttDisconnectReasonCode.SessionTakenOver).ConfigureAwait(false); + await oldClient.StopAsync(MqttDisconnectReasonCode.SessionTakenOver).ConfigureAwait(false); if (_eventContainer.ClientDisconnectedEvent.HasHandlers) { var eventArgs = new ClientDisconnectedEventArgs( - existingClient.Id, + oldClient.Id, MqttClientDisconnectType.Takeover, - existingClient.Endpoint, - existingClient.Session.Items); + oldClient.Endpoint, + oldClient.Session.Items); - await _eventContainer.ClientDisconnectedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + await _eventContainer.ClientDisconnectedEvent.TryInvokeAsync(eventArgs, _logger).ConfigureAwait(false); } } diff --git a/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs b/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs index a6542d14..6fb45371 100644 --- a/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs +++ b/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; @@ -30,7 +31,7 @@ namespace MQTTnet.Server readonly Dictionary _subscriptions = new Dictionary(); // Use subscription lock to maintain consistency across subscriptions and topic hash dictionaries - readonly SemaphoreSlim _subscriptionsLock = new SemaphoreSlim(1); + readonly AsyncLock _subscriptionsLock = new AsyncLock(); readonly Dictionary _wildcardSubscriptionsByTopicHash = new Dictionary(); public MqttClientSubscriptionsManager( @@ -45,13 +46,12 @@ namespace MQTTnet.Server _subscriptionChangedNotification = subscriptionChangedNotification; } - public CheckSubscriptionsResult CheckSubscriptions(string topic, ulong topicHash, MqttQualityOfServiceLevel applicationMessageQoSLevel, string senderClientId) + public CheckSubscriptionsResult CheckSubscriptions(string topic, ulong topicHash, MqttQualityOfServiceLevel qualityOfServiceLevel, string senderId) { var possibleSubscriptions = new List(); // Check for possible subscriptions. They might have collisions but this is fine. - _subscriptionsLock.Wait(); - try + using (_subscriptionsLock.EnterAsync(CancellationToken.None).GetAwaiter().GetResult()) { if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var noWildcardSubscriptions)) { @@ -63,18 +63,14 @@ namespace MQTTnet.Server var wildcardSubscriptions = wcs.Value; var subscriptionHash = wcs.Key; var subscriptionHashMask = wildcardSubscriptions.HashMask; - + if ((topicHash & subscriptionHashMask) == subscriptionHash) { possibleSubscriptions.AddRange(wildcardSubscriptions.Subscriptions.ToList()); } } } - finally - { - _subscriptionsLock.Release(); - } - + // The pre check has evaluated that nothing is subscribed. // If there were some possible candidates they get checked below // again to avoid collisions. @@ -83,7 +79,7 @@ namespace MQTTnet.Server return CheckSubscriptionsResult.NotSubscribed; } - var senderIsReceiver = string.Equals(senderClientId, _session.Id); + var senderIsReceiver = string.Equals(senderId, _session.Id); var maxQoSLevel = -1; // Not subscribed. HashSet subscriptionIdentifiers = null; @@ -136,7 +132,7 @@ namespace MQTTnet.Server SubscriptionIdentifiers = subscriptionIdentifiers?.ToList() ?? EmptySubscriptionIdentifiers, // Start with the same QoS as the publisher. - QualityOfServiceLevel = applicationMessageQoSLevel + QualityOfServiceLevel = qualityOfServiceLevel }; // Now downgrade if required. @@ -148,7 +144,7 @@ namespace MQTTnet.Server // Subscribing to a Topic Filter at QoS 2 is equivalent to saying "I would like to receive Messages matching this filter at the QoS with which they were published". // This means a publisher is responsible for determining the maximum QoS a Message can be delivered at, but a subscriber is able to require that the Server // downgrades the QoS to one more suitable for its usage. - if (maxQoSLevel < (int)applicationMessageQoSLevel) + if (maxQoSLevel < (int)qualityOfServiceLevel) { result.QualityOfServiceLevel = (MqttQualityOfServiceLevel)maxQoSLevel; } @@ -209,7 +205,7 @@ namespace MQTTnet.Server // This call will add the new subscription to the internal storage. // So the event _ClientSubscribedTopicEvent_ must be called afterwards. _subscriptionChangedNotification?.OnSubscriptionsAdded(_session, addedSubscriptions); - + if (_eventContainer.ClientSubscribedTopicEvent.HasHandlers) { foreach (var finalTopicFilter in finalTopicFilters) @@ -218,23 +214,22 @@ namespace MQTTnet.Server await _eventContainer.ClientSubscribedTopicEvent.InvokeAsync(eventArgs).ConfigureAwait(false); } } - + return result; } - public async Task Unsubscribe(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken) + public async Task Unsubscribe(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken) { if (unsubscribePacket == null) { throw new ArgumentNullException(nameof(unsubscribePacket)); } - var result = new MqttUnsubscribeResult(); + var result = new UnsubscribeResult(); var removedSubscriptions = new List(); - await _subscriptionsLock.WaitAsync(cancellationToken).ConfigureAwait(false); - try + using (await _subscriptionsLock.EnterAsync(cancellationToken).ConfigureAwait(false)) { foreach (var topicFilter in unsubscribePacket.TopicFilters) { @@ -265,7 +260,7 @@ namespace MQTTnet.Server if (existingSubscription != null) { var topicHash = existingSubscription.TopicHash; - + if (existingSubscription.TopicHasWildcard) { if (_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions)) @@ -294,11 +289,8 @@ namespace MQTTnet.Server } } } - finally - { - _subscriptionsLock.Release(); - _subscriptionChangedNotification?.OnSubscriptionsRemoved(_session, removedSubscriptions); - } + + _subscriptionChangedNotification?.OnSubscriptionsRemoved(_session, removedSubscriptions); if (_eventContainer.ClientUnsubscribedTopicEvent.HasHandlers) { @@ -345,8 +337,7 @@ namespace MQTTnet.Server // Add to subscriptions and maintain topic hash dictionaries - _subscriptionsLock.Wait(); - try + using (_subscriptionsLock.EnterAsync(CancellationToken.None).GetAwaiter().GetResult()) { MqttSubscription.CalculateTopicHash(topicFilter.Topic, out var topicHash, out var topicHashMask, out var hasWildcard); @@ -396,10 +387,6 @@ namespace MQTTnet.Server subscriptions.Add(subscription); } } - finally - { - _subscriptionsLock.Release(); - } return new CreateSubscriptionResult { @@ -493,23 +480,20 @@ namespace MQTTnet.Server async Task InterceptUnsubscribe(string topicFilter, MqttSubscription mqttSubscription, CancellationToken cancellationToken) { - var clientUnsubscribingTopicEventArgs = new InterceptingUnsubscriptionEventArgs(cancellationToken, topicFilter, _session.Items, topicFilter); - - if (mqttSubscription == null) + var clientUnsubscribingTopicEventArgs = new InterceptingUnsubscriptionEventArgs(cancellationToken, topicFilter, _session.Items, topicFilter) { - clientUnsubscribingTopicEventArgs.Response.ReasonCode = MqttUnsubscribeReasonCode.NoSubscriptionExisted; - } - else - { - clientUnsubscribingTopicEventArgs.Response.ReasonCode = MqttUnsubscribeReasonCode.Success; - } + Response = + { + ReasonCode = mqttSubscription == null ? MqttUnsubscribeReasonCode.NoSubscriptionExisted : MqttUnsubscribeReasonCode.Success + } + }; await _eventContainer.InterceptingUnsubscriptionEvent.InvokeAsync(clientUnsubscribingTopicEventArgs).ConfigureAwait(false); return clientUnsubscribingTopicEventArgs; } - public sealed class CreateSubscriptionResult + sealed class CreateSubscriptionResult { public bool IsNewSubscription { get; set; } diff --git a/Source/MQTTnet/Server/Internal/MqttSession.cs b/Source/MQTTnet/Server/Internal/MqttSession.cs index de9ff74d..a5a045bf 100644 --- a/Source/MQTTnet/Server/Internal/MqttSession.cs +++ b/Source/MQTTnet/Server/Internal/MqttSession.cs @@ -23,6 +23,7 @@ namespace MQTTnet.Server readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider(); readonly MqttServerOptions _serverOptions; + readonly MqttClientSubscriptionsManager _subscriptionsManager; readonly ConcurrentDictionary _unacknowledgedPublishPackets = new ConcurrentDictionary(); @@ -45,7 +46,7 @@ namespace MQTTnet.Server _serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions)); _clientSessionsManager = clientSessionsManager ?? throw new ArgumentNullException(nameof(clientSessionsManager)); - SubscriptionsManager = new MqttClientSubscriptionsManager(this, eventContainer, retainedMessagesManager, clientSessionsManager); + _subscriptionsManager = new MqttClientSubscriptionsManager(this, eventContainer, retainedMessagesManager, clientSessionsManager); } public DateTime CreatedTimestamp { get; } = DateTime.UtcNow; @@ -67,18 +68,8 @@ namespace MQTTnet.Server public long PendingDataPacketsCount => _packetBus.PartitionItemsCount(MqttPacketBusPartition.Data); - public MqttClientSubscriptionsManager SubscriptionsManager { get; } - public bool WillMessageSent { get; set; } - public MqttPublishPacket PeekAcknowledgePublishPacket(ushort packetIdentifier) - { - // This will only return the matching PUBLISH packet but does not remove it. - // This is required for QoS 2. - _unacknowledgedPublishPackets.TryGetValue(packetIdentifier, out var publishPacket); - return publishPacket; - } - public MqttPublishPacket AcknowledgePublishPacket(ushort packetIdentifier) { _unacknowledgedPublishPackets.TryRemove(packetIdentifier, out var publishPacket); @@ -107,8 +98,8 @@ namespace MQTTnet.Server public void Dispose() { - _packetBus?.Dispose(); - SubscriptionsManager.Dispose(); + _packetBus.Dispose(); + _subscriptionsManager.Dispose(); } public void EnqueueControlPacket(MqttPacketBusItem packetBusItem) @@ -150,6 +141,14 @@ namespace MQTTnet.Server _packetBus.EnqueueItem(packetBusItem, MqttPacketBusPartition.Health); } + public MqttPublishPacket PeekAcknowledgePublishPacket(ushort packetIdentifier) + { + // This will only return the matching PUBLISH packet but does not remove it. + // This is required for QoS 2. + _unacknowledgedPublishPackets.TryGetValue(packetIdentifier, out var publishPacket); + return publishPacket; + } + public void Recover() { // TODO: Keep the bus and only insert pending items again. @@ -167,5 +166,30 @@ namespace MQTTnet.Server { _subscribedTopics?.Remove(topic); } + + public Task Subscribe(MqttSubscribePacket subscribePacket, CancellationToken cancellationToken) + { + return _subscriptionsManager.Subscribe(subscribePacket, cancellationToken); + } + + public bool TryCheckSubscriptions(string topic, ulong topicHash, MqttQualityOfServiceLevel qualityOfServiceLevel, string senderId, out CheckSubscriptionsResult result) + { + result = null; + + try + { + result = _subscriptionsManager.CheckSubscriptions(topic, topicHash, qualityOfServiceLevel, senderId); + return true; + } + catch + { + return false; + } + } + + public Task Unsubscribe(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken) + { + return _subscriptionsManager.Unsubscribe(unsubscribePacket, cancellationToken); + } } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/MqttSubscription.cs b/Source/MQTTnet/Server/Internal/MqttSubscription.cs index ec532425..930d3c72 100644 --- a/Source/MQTTnet/Server/Internal/MqttSubscription.cs +++ b/Source/MQTTnet/Server/Internal/MqttSubscription.cs @@ -179,7 +179,7 @@ namespace MQTTnet.Server RetainAsPublished = retainAsPublished; GrantedQualityOfServiceLevel = qualityOfServiceLevel; Identifier = identifier; - + CalculateTopicHash(Topic, out var hash, out var hashMask, out var hasWildcard); TopicHash = hash; TopicHashMask = hashMask; diff --git a/Source/MQTTnet/Server/Internal/MqttUnsubscribeResult.cs b/Source/MQTTnet/Server/Internal/UnsubscribeResult.cs similarity index 90% rename from Source/MQTTnet/Server/Internal/MqttUnsubscribeResult.cs rename to Source/MQTTnet/Server/Internal/UnsubscribeResult.cs index 9e482dcf..5a93d997 100644 --- a/Source/MQTTnet/Server/Internal/MqttUnsubscribeResult.cs +++ b/Source/MQTTnet/Server/Internal/UnsubscribeResult.cs @@ -7,7 +7,7 @@ using MQTTnet.Protocol; namespace MQTTnet.Server { - public sealed class MqttUnsubscribeResult + public sealed class UnsubscribeResult { public List ReasonCodes { get; } = new List(128); From 5ee0bb4b65a3ce63c9867176e231c6edd33fa978 Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Wed, 9 Nov 2022 19:56:29 +0100 Subject: [PATCH 18/24] 1565 high cpu load from asyncsignal (#1566) --- .../Internal/AsyncSignal_Tests.cs | 16 +++++++++- Source/MQTTnet.Tests/Server/Load_Tests.cs | 2 +- Source/MQTTnet.Tests/Server/Session_Tests.cs | 4 +-- Source/MQTTnet/Internal/AsyncQueue.cs | 30 +++++++++++-------- .../Internal/AsyncQueueDequeueResult.cs | 9 +++++- Source/MQTTnet/Internal/AsyncSignal.cs | 26 ++++++++++++++-- 6 files changed, 67 insertions(+), 20 deletions(-) diff --git a/Source/MQTTnet.Tests/Internal/AsyncSignal_Tests.cs b/Source/MQTTnet.Tests/Internal/AsyncSignal_Tests.cs index 88ec56fa..9adac5dc 100644 --- a/Source/MQTTnet.Tests/Internal/AsyncSignal_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/AsyncSignal_Tests.cs @@ -52,12 +52,26 @@ namespace MQTTnet.Tests.Internal } [TestMethod] - public async Task Loop_Signal() + public async Task Reset_Signal() { var asyncSignal = new AsyncSignal(); + // WaitAsync should fail because no signal is available. for (var i = 0; i < 10; i++) { + try + { + using (var timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(100))) + { + await asyncSignal.WaitAsync(timeout.Token); + } + + Assert.Fail("This must fail because the signal is not yet set."); + } + catch (OperationCanceledException) + { + } + asyncSignal.Set(); // WaitAsync should return directly because the signal is available. diff --git a/Source/MQTTnet.Tests/Server/Load_Tests.cs b/Source/MQTTnet.Tests/Server/Load_Tests.cs index 7c446ee0..112d1d80 100644 --- a/Source/MQTTnet.Tests/Server/Load_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Load_Tests.cs @@ -55,7 +55,7 @@ namespace MQTTnet.Tests.Server }); } - SpinWait.SpinUntil(() => receivedMessages == 100000, TimeSpan.FromSeconds(90)); + SpinWait.SpinUntil(() => receivedMessages == 100000, TimeSpan.FromSeconds(120)); Assert.AreEqual(100000, receivedMessages); } diff --git a/Source/MQTTnet.Tests/Server/Session_Tests.cs b/Source/MQTTnet.Tests/Server/Session_Tests.cs index d86d2e8d..6b9af994 100644 --- a/Source/MQTTnet.Tests/Server/Session_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Session_Tests.cs @@ -150,7 +150,7 @@ namespace MQTTnet.Tests.Server // Try to connect 50 clients at the same time. var clients = await Task.WhenAll(Enumerable.Range(0, 50).Select(i => ConnectAndSubscribe(testEnvironment, options, OnReceive))); - await Task.Delay(5000); + await Task.Delay(TimeSpan.FromSeconds(10)); var connectedClients = clients.Where(c => c?.TryPingAsync().GetAwaiter().GetResult() == true).ToList(); @@ -162,7 +162,7 @@ namespace MQTTnet.Tests.Server var sendClient = await testEnvironment.ConnectClient(option2); await sendClient.PublishStringAsync("aaa", "1"); - await Task.Delay(3000); + await Task.Delay(TimeSpan.FromSeconds(5)); Assert.AreEqual(true, hasReceive); } diff --git a/Source/MQTTnet/Internal/AsyncQueue.cs b/Source/MQTTnet/Internal/AsyncQueue.cs index c54f5467..52381a32 100644 --- a/Source/MQTTnet/Internal/AsyncQueue.cs +++ b/Source/MQTTnet/Internal/AsyncQueue.cs @@ -29,7 +29,7 @@ namespace MQTTnet.Internal { lock (_syncRoot) { - _signal?.Dispose(); + _signal.Dispose(); _isDisposed = true; } @@ -48,10 +48,10 @@ namespace MQTTnet.Internal { if (_queue.TryDequeue(out var item)) { - return new AsyncQueueDequeueResult(true, item); + return AsyncQueueDequeueResult.Success(item); } - return new AsyncQueueDequeueResult(false, default); + return AsyncQueueDequeueResult.NonSuccess; } public async Task> TryDequeueAsync(CancellationToken cancellationToken) @@ -60,36 +60,42 @@ namespace MQTTnet.Internal { try { - Task task; + Task task = null; lock (_syncRoot) { if (_isDisposed) { - return new AsyncQueueDequeueResult(false, default); + return AsyncQueueDequeueResult.NonSuccess; } - task = _signal.WaitAsync(cancellationToken); + if (_queue.IsEmpty) + { + task = _signal.WaitAsync(cancellationToken); + } } - await task.ConfigureAwait(false); - + if (task != null) + { + await task.ConfigureAwait(false); + } + if (cancellationToken.IsCancellationRequested) { - return new AsyncQueueDequeueResult(false, default); + return AsyncQueueDequeueResult.NonSuccess; } if (_queue.TryDequeue(out var item)) { - return new AsyncQueueDequeueResult(true, item); + return AsyncQueueDequeueResult.Success(item); } } catch (OperationCanceledException) { - return new AsyncQueueDequeueResult(false, default); + return AsyncQueueDequeueResult.NonSuccess; } } - return new AsyncQueueDequeueResult(false, default); + return AsyncQueueDequeueResult.NonSuccess; } } } \ No newline at end of file diff --git a/Source/MQTTnet/Internal/AsyncQueueDequeueResult.cs b/Source/MQTTnet/Internal/AsyncQueueDequeueResult.cs index 6848e1fd..09543238 100644 --- a/Source/MQTTnet/Internal/AsyncQueueDequeueResult.cs +++ b/Source/MQTTnet/Internal/AsyncQueueDequeueResult.cs @@ -4,8 +4,10 @@ namespace MQTTnet.Internal { - public class AsyncQueueDequeueResult + public sealed class AsyncQueueDequeueResult { + public static readonly AsyncQueueDequeueResult NonSuccess = new AsyncQueueDequeueResult(false, default); + public AsyncQueueDequeueResult(bool isSuccess, TItem item) { IsSuccess = isSuccess; @@ -15,5 +17,10 @@ namespace MQTTnet.Internal public bool IsSuccess { get; } public TItem Item { get; } + + public static AsyncQueueDequeueResult Success(TItem item) + { + return new AsyncQueueDequeueResult(true, item); + } } } \ No newline at end of file diff --git a/Source/MQTTnet/Internal/AsyncSignal.cs b/Source/MQTTnet/Internal/AsyncSignal.cs index cde8532c..217a5cce 100644 --- a/Source/MQTTnet/Internal/AsyncSignal.cs +++ b/Source/MQTTnet/Internal/AsyncSignal.cs @@ -33,9 +33,17 @@ namespace MQTTnet.Internal { _isSignaled = true; + Cleanup(); + // If there is already a waiting task let it run. - _waiter?.Approve(); - _waiter = null; + if (_waiter != null) + { + _waiter.Approve(); + _waiter = null; + + // Since we already got a waiter the signal must be reset right now! + _isSignaled = false; + } } } @@ -47,8 +55,11 @@ namespace MQTTnet.Internal { ThrowIfDisposed(); + Cleanup(); + if (_isSignaled) { + _isSignaled = false; return CompletedTask.Instance; } @@ -58,7 +69,7 @@ namespace MQTTnet.Internal { throw new InvalidOperationException("Only one waiting task is permitted per async signal."); } - + _waiter.Dispose(); } @@ -67,6 +78,15 @@ namespace MQTTnet.Internal } } + void Cleanup() + { + // Cleanup if the previous waiter was cancelled. + if (_waiter != null && _waiter.Task.IsCanceled) + { + _waiter = null; + } + } + void ThrowIfDisposed() { if (_isDisposed) From 56f0a2c31ca817cbd1eceed44b54333afbc33863 Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Wed, 9 Nov 2022 20:13:59 +0100 Subject: [PATCH 19/24] Fix validation of topic in managed client (#1567) --- .github/workflows/ReleaseNotes.md | 3 ++- Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index d08fa6b0..1614de15 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -1,5 +1,6 @@ * [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) +* [ManagedClient] The topic is no longer declared as invalid if a topic alias is used (#1395). * [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 memory leak when old sessions are discarded (#1553) \ No newline at end of file +* [Server] Fixed memory leak when old sessions are discarded (#1553) diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs index 2583a605..c979627c 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs @@ -160,7 +160,7 @@ namespace MQTTnet.Extensions.ManagedClient throw new InvalidOperationException("call StartAsync before publishing messages"); } - MqttTopicValidator.ThrowIfInvalid(applicationMessage.ApplicationMessage.Topic); + MqttTopicValidator.ThrowIfInvalid(applicationMessage.ApplicationMessage); ManagedMqttApplicationMessage removedMessage = null; ApplicationMessageSkippedEventArgs applicationMessageSkippedEventArgs = null; From bfec9dc5ad58bc08a64432b1497f4e2cbc3f1346 Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Wed, 9 Nov 2022 21:14:53 +0100 Subject: [PATCH 20/24] 802 client isnt being notified if message is not delivered (#1568) --- .github/workflows/ReleaseNotes.md | 1 + Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs | 4 +- Source/MQTTnet.Tests/Server/Publish_Tests.cs | 58 +++++++++++++++++++ .../Publishing/MqttClientPublishResult.cs | 31 ++++++---- .../Formatter/MqttPubAckPacketFactory.cs | 38 ++++++------ .../Formatter/MqttPubRecPacketFactory.cs | 10 +++- .../MQTTnet/Protocol/MqttPubAckReasonCode.cs | 5 ++ .../DispatchApplicationMessageResult.cs | 16 +++++ Source/MQTTnet/Server/Internal/MqttClient.cs | 7 ++- .../Internal/MqttClientSessionsManager.cs | 25 ++++---- 10 files changed, 146 insertions(+), 49 deletions(-) create mode 100644 Source/MQTTnet.Tests/Server/Publish_Tests.cs create mode 100644 Source/MQTTnet/Server/Internal/DispatchApplicationMessageResult.cs diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index 1614de15..608f1589 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -4,3 +4,4 @@ * [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 memory leak when old sessions are discarded (#1553) +* [Server] When using MQTTv5 the reason code _NoMatchingSubscribers_ is returned when no client is subscribed to a published topic (#802) diff --git a/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs b/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs index ac7b8792..29fd74e9 100644 --- a/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs +++ b/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs @@ -154,7 +154,7 @@ namespace MQTTnet.Tests.MQTTv5 var result = await client.PublishStringAsync("a", "b", MqttQualityOfServiceLevel.AtLeastOnce); await client.DisconnectAsync(); - Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); + Assert.AreEqual(MqttClientPublishReasonCode.NoMatchingSubscribers, result.ReasonCode); } } @@ -169,7 +169,7 @@ namespace MQTTnet.Tests.MQTTv5 var result = await client.PublishStringAsync("a", "b", MqttQualityOfServiceLevel.ExactlyOnce); await client.DisconnectAsync(); - Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); + Assert.AreEqual(MqttClientPublishReasonCode.NoMatchingSubscribers, result.ReasonCode); } } diff --git a/Source/MQTTnet.Tests/Server/Publish_Tests.cs b/Source/MQTTnet.Tests/Server/Publish_Tests.cs new file mode 100644 index 00000000..c2293f0a --- /dev/null +++ b/Source/MQTTnet.Tests/Server/Publish_Tests.cs @@ -0,0 +1,58 @@ +// 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.Formatter; +using MQTTnet.Protocol; + +namespace MQTTnet.Tests.Server +{ + [TestClass] + public sealed class Publish_Tests : BaseTestClass + { + [TestMethod] + public async Task Return_Success_When_Subscribed() + { + using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) + { + await testEnvironment.StartServer(); + + var sender = await testEnvironment.ConnectClient(); + var receiver = await testEnvironment.ConnectClient(); + + await receiver.SubscribeAsync("A"); + + // AtLeastOnce is required to get an ACK packet from the server. + var publishResult = await sender.PublishStringAsync("A", "Payload", MqttQualityOfServiceLevel.AtLeastOnce); + + Assert.AreEqual(MqttClientPublishReasonCode.Success, publishResult.ReasonCode); + + Assert.AreEqual(true, publishResult.IsSuccess); + } + } + + [TestMethod] + public async Task Return_NoMatchingSubscribers_When_Not_Subscribed() + { + using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) + { + await testEnvironment.StartServer(); + + var sender = await testEnvironment.ConnectClient(); + var receiver = await testEnvironment.ConnectClient(); + + await receiver.SubscribeAsync("A"); + + // AtLeastOnce is required to get an ACK packet from the server. + var publishResult = await sender.PublishStringAsync("B", "Payload", MqttQualityOfServiceLevel.AtLeastOnce); + + Assert.AreEqual(MqttClientPublishReasonCode.NoMatchingSubscribers, publishResult.ReasonCode); + + Assert.AreEqual(true, publishResult.IsSuccess); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Publishing/MqttClientPublishResult.cs b/Source/MQTTnet/Client/Publishing/MqttClientPublishResult.cs index 47d338eb..6fd2dcd8 100644 --- a/Source/MQTTnet/Client/Publishing/MqttClientPublishResult.cs +++ b/Source/MQTTnet/Client/Publishing/MqttClientPublishResult.cs @@ -10,29 +10,38 @@ namespace MQTTnet.Client public sealed class MqttClientPublishResult { /// - /// Gets the packet identifier which was used for this publish. + /// Returns if the overall status of the publish is a success. This can be the reason code _Success_ or + /// _NoMatchingSubscribers_. _NoMatchingSubscribers_ usually indicates only that no other client is interested in the topic but overall transmit + /// to the server etc. was a success. + /// + public bool IsSuccess => ReasonCode == MqttClientPublishReasonCode.Success || ReasonCode == MqttClientPublishReasonCode.NoMatchingSubscribers; + + /// + /// Gets the packet identifier which was used for this publish. /// public ushort? PacketIdentifier { get; set; } /// - /// Gets or sets the reason code. - /// Hint: MQTT 5 feature only. + /// Gets or sets the reason code. + /// Hint: MQTT 5 feature only. /// public MqttClientPublishReasonCode ReasonCode { get; set; } = MqttClientPublishReasonCode.Success; /// - /// Gets or sets the reason string. - /// Hint: MQTT 5 feature only. + /// Gets or sets the reason string. + /// Hint: MQTT 5 feature only. /// public string ReasonString { get; set; } /// - /// Gets or sets the user properties. - /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT packet. - /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add metadata to MQTT messages and pass information between publisher, broker, and subscriber. - /// The feature is very similar to the HTTP header concept. - /// Hint: MQTT 5 feature only. + /// Gets or sets the user properties. + /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT + /// packet. + /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add + /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. + /// The feature is very similar to the HTTP header concept. + /// Hint: MQTT 5 feature only. /// public IReadOnlyCollection UserProperties { get; internal set; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttPubAckPacketFactory.cs b/Source/MQTTnet/Formatter/MqttPubAckPacketFactory.cs index 4d6c6870..536ec011 100644 --- a/Source/MQTTnet/Formatter/MqttPubAckPacketFactory.cs +++ b/Source/MQTTnet/Formatter/MqttPubAckPacketFactory.cs @@ -12,7 +12,10 @@ namespace MQTTnet.Formatter { public sealed class MqttPubAckPacketFactory { - public MqttPubAckPacket Create(MqttPublishPacket publishPacket, InterceptingPublishEventArgs interceptingPublishEventArgs) + public MqttPubAckPacket Create( + MqttPublishPacket publishPacket, + InterceptingPublishEventArgs interceptingPublishEventArgs, + DispatchApplicationMessageResult dispatchApplicationMessageResult) { if (publishPacket == null) { @@ -21,10 +24,19 @@ namespace MQTTnet.Formatter var pubAckPacket = new MqttPubAckPacket { - PacketIdentifier = publishPacket.PacketIdentifier, - ReasonCode = MqttPubAckReasonCode.Success + PacketIdentifier = publishPacket.PacketIdentifier }; + if (dispatchApplicationMessageResult.MatchingSubscribersCount == 0) + { + // NoMatchingSubscribers is ONLY sent by the server! + pubAckPacket.ReasonCode = MqttPubAckReasonCode.NoMatchingSubscribers; + } + else + { + pubAckPacket.ReasonCode = MqttPubAckReasonCode.Success; + } + if (interceptingPublishEventArgs != null) { pubAckPacket.ReasonCode = (MqttPubAckReasonCode)(int)interceptingPublishEventArgs.Response.ReasonCode; @@ -42,24 +54,12 @@ namespace MQTTnet.Formatter throw new ArgumentNullException(nameof(applicationMessageReceivedEventArgs)); } - var pubAckPacket = Create(applicationMessageReceivedEventArgs.PublishPacket, applicationMessageReceivedEventArgs.ReasonCode); - pubAckPacket.UserProperties = applicationMessageReceivedEventArgs.ResponseUserProperties; - pubAckPacket.ReasonString = applicationMessageReceivedEventArgs.ResponseReasonString; - - return pubAckPacket; - } - - static MqttPubAckPacket Create(MqttPublishPacket publishPacket, MqttApplicationMessageReceivedReasonCode applicationMessageReceivedReasonCode) - { - if (publishPacket == null) - { - throw new ArgumentNullException(nameof(publishPacket)); - } - var pubAckPacket = new MqttPubAckPacket { - PacketIdentifier = publishPacket.PacketIdentifier, - ReasonCode = (MqttPubAckReasonCode)(int)applicationMessageReceivedReasonCode + PacketIdentifier = applicationMessageReceivedEventArgs.PublishPacket.PacketIdentifier, + ReasonCode = (MqttPubAckReasonCode)(int)applicationMessageReceivedEventArgs.ReasonCode, + UserProperties = applicationMessageReceivedEventArgs.ResponseUserProperties, + ReasonString = applicationMessageReceivedEventArgs.ResponseReasonString }; return pubAckPacket; diff --git a/Source/MQTTnet/Formatter/MqttPubRecPacketFactory.cs b/Source/MQTTnet/Formatter/MqttPubRecPacketFactory.cs index 5c0726d8..0a6bba24 100644 --- a/Source/MQTTnet/Formatter/MqttPubRecPacketFactory.cs +++ b/Source/MQTTnet/Formatter/MqttPubRecPacketFactory.cs @@ -25,7 +25,10 @@ namespace MQTTnet.Formatter return pubRecPacket; } - public MqttPacket Create(MqttPublishPacket publishPacket, InterceptingPublishEventArgs interceptingPublishEventArgs) + public MqttPacket Create( + MqttPublishPacket publishPacket, + InterceptingPublishEventArgs interceptingPublishEventArgs, + DispatchApplicationMessageResult dispatchApplicationMessageResult) { if (publishPacket == null) { @@ -38,6 +41,11 @@ namespace MQTTnet.Formatter ReasonCode = MqttPubRecReasonCode.Success }; + if (dispatchApplicationMessageResult.MatchingSubscribersCount == 0) + { + pubRecPacket.ReasonCode = MqttPubRecReasonCode.NoMatchingSubscribers; + } + if (interceptingPublishEventArgs != null) { pubRecPacket.ReasonCode = (MqttPubRecReasonCode)(int)interceptingPublishEventArgs.Response.ReasonCode; diff --git a/Source/MQTTnet/Protocol/MqttPubAckReasonCode.cs b/Source/MQTTnet/Protocol/MqttPubAckReasonCode.cs index 9eded18c..50de497f 100644 --- a/Source/MQTTnet/Protocol/MqttPubAckReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttPubAckReasonCode.cs @@ -7,7 +7,12 @@ namespace MQTTnet.Protocol public enum MqttPubAckReasonCode { Success = 0, + + /// + /// The message is accepted but there are no subscribers. This is sent only by the Server. If the Server knows that there are no matching subscribers, it MAY use this Reason Code instead of 0x00 (Success). + /// NoMatchingSubscribers = 16, + UnspecifiedError = 128, ImplementationSpecificError = 131, NotAuthorized = 135, diff --git a/Source/MQTTnet/Server/Internal/DispatchApplicationMessageResult.cs b/Source/MQTTnet/Server/Internal/DispatchApplicationMessageResult.cs new file mode 100644 index 00000000..bc4637a1 --- /dev/null +++ b/Source/MQTTnet/Server/Internal/DispatchApplicationMessageResult.cs @@ -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. + +namespace MQTTnet.Server +{ + public sealed class DispatchApplicationMessageResult + { + public DispatchApplicationMessageResult(int matchingSubscribersCount) + { + MatchingSubscribersCount = matchingSubscribersCount; + } + + public int MatchingSubscribersCount { get; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/MqttClient.cs b/Source/MQTTnet/Server/Internal/MqttClient.cs index 674a262f..f9f7e695 100644 --- a/Source/MQTTnet/Server/Internal/MqttClient.cs +++ b/Source/MQTTnet/Server/Internal/MqttClient.cs @@ -237,9 +237,10 @@ namespace MQTTnet.Server return; } + DispatchApplicationMessageResult dispatchResult = null; if (processPublish && applicationMessage != null) { - await _sessionsManager.DispatchApplicationMessage(Id, applicationMessage).ConfigureAwait(false); + dispatchResult = await _sessionsManager.DispatchApplicationMessage(Id, applicationMessage).ConfigureAwait(false); } switch (publishPacket.QualityOfServiceLevel) @@ -251,13 +252,13 @@ namespace MQTTnet.Server } case MqttQualityOfServiceLevel.AtLeastOnce: { - var pubAckPacket = _packetFactories.PubAck.Create(publishPacket, interceptingPublishEventArgs); + var pubAckPacket = _packetFactories.PubAck.Create(publishPacket, interceptingPublishEventArgs, dispatchResult); Session.EnqueueControlPacket(new MqttPacketBusItem(pubAckPacket)); break; } case MqttQualityOfServiceLevel.ExactlyOnce: { - var pubRecPacket = _packetFactories.PubRec.Create(publishPacket, interceptingPublishEventArgs); + var pubRecPacket = _packetFactories.PubRec.Create(publishPacket, interceptingPublishEventArgs, dispatchResult); Session.EnqueueControlPacket(new MqttPacketBusItem(pubRecPacket)); break; } diff --git a/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs b/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs index 186a15ca..9fb9ca18 100644 --- a/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs +++ b/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs @@ -127,21 +127,20 @@ namespace MQTTnet.Server _logger.Verbose("Session for client '{0}' deleted.", clientId); } - - public async Task DispatchApplicationMessage(string senderId, MqttApplicationMessage applicationMessage) + + public async Task DispatchApplicationMessage(string senderId, MqttApplicationMessage applicationMessage) { + var matchingSubscribersCount = 0; try { if (applicationMessage.Retain) { await _retainedMessagesManager.UpdateMessage(senderId, applicationMessage).ConfigureAwait(false); } - - var deliveryCount = 0; + List subscriberSessions; lock (_sessionsManagementLock) { - // only subscriber clients are of interest here. subscriberSessions = _subscriberSessions.ToList(); } @@ -187,17 +186,22 @@ namespace MQTTnet.Server } session.EnqueueDataPacket(new MqttPacketBusItem(newPublishPacket)); - deliveryCount++; + matchingSubscribersCount++; _logger.Verbose("Client '{0}': Queued PUBLISH packet with topic '{1}'.", session.Id, applicationMessage.Topic); } - await FireApplicationMessageNotConsumedEvent(applicationMessage, deliveryCount, senderId); + if (matchingSubscribersCount == 0) + { + await FireApplicationMessageNotConsumedEvent(applicationMessage, senderId).ConfigureAwait(false); + } } catch (Exception exception) { _logger.Error(exception, "Unhandled exception while processing next queued application message."); } + + return new DispatchApplicationMessageResult(matchingSubscribersCount); } public void Dispose() @@ -575,13 +579,8 @@ namespace MQTTnet.Server return new MqttSession(clientId, isPersistent, sessionItems, _options, _eventContainer, _retainedMessagesManager, this); } - async Task FireApplicationMessageNotConsumedEvent(MqttApplicationMessage applicationMessage, int deliveryCount, string senderId) + async Task FireApplicationMessageNotConsumedEvent(MqttApplicationMessage applicationMessage, string senderId) { - if (deliveryCount > 0) - { - return; - } - if (!_eventContainer.ApplicationMessageNotConsumedEvent.HasHandlers) { return; From db1fc7625d9ae5c512bd42747c4aa901db75035b Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Fri, 11 Nov 2022 08:14:15 +0100 Subject: [PATCH 21/24] 1546 possible bug in server repeatedly sending the same message to client subscribed with cleansession=false (#1569) --- .github/workflows/ReleaseNotes.md | 3 +- .../IManagedMqttClient.cs | 2 +- .../ManagedMqttClient.cs | 12 +- .../Client/LowLevelMqttClient_Tests.cs | 10 +- .../Client/ManagedMqttClient_Tests.cs | 61 ++++++- .../Internal/MqttPacketBusItem_Tests.cs | 43 +++++ .../Internal/MqttPacketBus_Tests.cs | 168 +++++++++--------- .../TestApplicationMessageReceivedHandler.cs | 29 ++- .../MQTTnet.Tests/Mockups/TestEnvironment.cs | 21 ++- Source/MQTTnet.Tests/Server/QoS_Tests.cs | 51 ++++++ Source/MQTTnet/Adapter/MqttChannelAdapter.cs | 4 +- Source/MQTTnet/Client/MqttClientExtensions.cs | 51 +++--- .../Formatter/ReadFixedHeaderResult.cs | 6 +- Source/MQTTnet/Internal/MqttPacketBusItem.cs | 48 +++-- Source/MQTTnet/Server/Internal/MqttClient.cs | 8 +- Source/MQTTnet/Server/Internal/MqttSession.cs | 50 +++++- Source/MQTTnet/Server/MqttServerExtensions.cs | 78 +++++++- .../Server/Status/MqttSessionStatus.cs | 2 +- 18 files changed, 475 insertions(+), 172 deletions(-) create mode 100644 Source/MQTTnet.Tests/Internal/MqttPacketBusItem_Tests.cs diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index 608f1589..ace201e0 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -1,7 +1,8 @@ * [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) -* [ManagedClient] The topic is no longer declared as invalid if a topic alias is used (#1395). +* [ManagedClient] The topic is no longer declared as invalid if a topic alias is used (#1395) * [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 memory leak when old sessions are discarded (#1553) * [Server] When using MQTTv5 the reason code _NoMatchingSubscribers_ is returned when no client is subscribed to a published topic (#802) +* [Server] Fix sending of duplicate messages with QoS > 0 when session is recovered and session is reused (#1546) diff --git a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs index 20f51d83..49add6a3 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs @@ -43,7 +43,7 @@ namespace MQTTnet.Extensions.ManagedClient Task StartAsync(ManagedMqttClientOptions options); - Task StopAsync(); + Task StopAsync(bool cleanDisconnect = true); Task SubscribeAsync(ICollection topicFilters); diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs index c979627c..0c04c663 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs @@ -62,6 +62,7 @@ namespace MQTTnet.Extensions.ManagedClient CancellationTokenSource _publishingCancellationToken; ManagedMqttClientStorageManager _storageManager; + bool _isCleanDisconnect; public ManagedMqttClient(IMqttClient mqttClient, IMqttNetLogger logger) { @@ -255,10 +256,12 @@ namespace MQTTnet.Extensions.ManagedClient _logger.Info("Started"); } - public async Task StopAsync() + public async Task StopAsync(bool cleanDisconnect = true) { ThrowIfDisposed(); + _isCleanDisconnect = cleanDisconnect; + StopPublishing(); StopMaintainingConnection(); @@ -382,9 +385,12 @@ namespace MQTTnet.Extensions.ManagedClient { try { - using (var disconnectTimeout = new CancellationTokenSource(Options.ClientOptions.Timeout)) + if (_isCleanDisconnect) { - await InternalClient.DisconnectAsync(new MqttClientDisconnectOptions(), disconnectTimeout.Token).ConfigureAwait(false); + using (var disconnectTimeout = new CancellationTokenSource(Options.ClientOptions.Timeout)) + { + await InternalClient.DisconnectAsync(new MqttClientDisconnectOptions(), disconnectTimeout.Token).ConfigureAwait(false); + } } } catch (OperationCanceledException) diff --git a/Source/MQTTnet.Tests/Client/LowLevelMqttClient_Tests.cs b/Source/MQTTnet.Tests/Client/LowLevelMqttClient_Tests.cs index ae8fcebe..e7366d3e 100644 --- a/Source/MQTTnet.Tests/Client/LowLevelMqttClient_Tests.cs +++ b/Source/MQTTnet.Tests/Client/LowLevelMqttClient_Tests.cs @@ -64,7 +64,7 @@ namespace MQTTnet.Tests.Client await client.ConnectAsync(options, CancellationToken.None).ConfigureAwait(false); } - + [TestMethod] [ExpectedException(typeof(MqttCommunicationException))] public async Task Connect_To_Wrong_Host() @@ -80,6 +80,8 @@ namespace MQTTnet.Tests.Client { using (var testEnvironment = CreateTestEnvironment()) { + testEnvironment.IgnoreServerLogErrors = true; + testEnvironment.ServerPort = 8364; var server = await testEnvironment.StartServer(); @@ -116,6 +118,8 @@ namespace MQTTnet.Tests.Client { using (var testEnvironment = CreateTestEnvironment()) { + testEnvironment.IgnoreServerLogErrors = true; + var server = await testEnvironment.StartServer(); using (var lowLevelClient = testEnvironment.CreateLowLevelClient()) @@ -123,7 +127,7 @@ namespace MQTTnet.Tests.Client Assert.IsFalse(lowLevelClient.IsConnected); var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).WithTimeout(TimeSpan.FromSeconds(1)).Build(); - + await lowLevelClient.ConnectAsync(clientOptions, CancellationToken.None); Assert.IsTrue(lowLevelClient.IsConnected); @@ -148,7 +152,7 @@ namespace MQTTnet.Tests.Client catch { } - + Assert.IsFalse(lowLevelClient.IsConnected); } } diff --git a/Source/MQTTnet.Tests/Client/ManagedMqttClient_Tests.cs b/Source/MQTTnet.Tests/Client/ManagedMqttClient_Tests.cs index 554cc663..bfa662ac 100644 --- a/Source/MQTTnet.Tests/Client/ManagedMqttClient_Tests.cs +++ b/Source/MQTTnet.Tests/Client/ManagedMqttClient_Tests.cs @@ -11,6 +11,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; using MQTTnet.Diagnostics; using MQTTnet.Extensions.ManagedClient; +using MQTTnet.Formatter; using MQTTnet.Internal; using MQTTnet.Protocol; using MQTTnet.Server; @@ -21,6 +22,65 @@ namespace MQTTnet.Tests.Client [TestClass] public sealed class ManagedMqttClient_Tests : BaseTestClass { + [TestMethod] + public async Task Receive_While_Not_Cleanly_Disconnected() + { + using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) + { + await testEnvironment.StartServer(o => o.WithPersistentSessions()); + + var senderClient = await testEnvironment.ConnectClient(); + + // Prepare managed client. + var managedClient = testEnvironment.Factory.CreateManagedMqttClient(); + await managedClient.SubscribeAsync("#"); + var receivedMessages = testEnvironment.CreateApplicationMessageHandler(managedClient); + + var managedClientOptions = new ManagedMqttClientOptions + { + ClientOptions = testEnvironment.Factory.CreateClientOptionsBuilder() + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) + .WithClientId(nameof(Receive_While_Not_Cleanly_Disconnected) + "_managed") + .WithCleanSession(false) + .Build() + }; + + await managedClient.StartAsync(managedClientOptions); + await LongTestDelay(); + + // Send test data. + await senderClient.PublishStringAsync("topic1"); + await LongTestDelay(); + + receivedMessages.AssertReceivedCountEquals(1); + + // Stop the managed client but keep session at server (not clean disconnect required). + await managedClient.StopAsync(false); + await LongTestDelay(); + + // Send new messages in the meantime. + await senderClient.PublishStringAsync("topic2", qualityOfServiceLevel: MqttQualityOfServiceLevel.ExactlyOnce); + await LongTestDelay(); + + // Start the managed client, it should receive the new message. + await managedClient.StartAsync(managedClientOptions); + await LongTestDelay(); + + receivedMessages.AssertReceivedCountEquals(2); + + // Stop and start again, no new message should be received. + for (var i = 0; i < 3; i++) + { + await managedClient.StopAsync(false); + await LongTestDelay(); + await managedClient.StartAsync(managedClientOptions); + await LongTestDelay(); + } + + receivedMessages.AssertReceivedCountEquals(2); + } + } + [TestMethod] public async Task Connect_To_Invalid_Server() { @@ -33,7 +93,6 @@ namespace MQTTnet.Tests.Client var managedClient = testEnvironment.Factory.CreateManagedMqttClient(mqttClient); ConnectingFailedEventArgs connectingFailedEventArgs = null; - managedClient.ConnectingFailedAsync += args => { connectingFailedEventArgs = args; diff --git a/Source/MQTTnet.Tests/Internal/MqttPacketBusItem_Tests.cs b/Source/MQTTnet.Tests/Internal/MqttPacketBusItem_Tests.cs new file mode 100644 index 00000000..912d38a0 --- /dev/null +++ b/Source/MQTTnet.Tests/Internal/MqttPacketBusItem_Tests.cs @@ -0,0 +1,43 @@ +// 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.Internal; +using MQTTnet.Packets; + +namespace MQTTnet.Tests.Internal +{ + [TestClass] + public sealed class MqttPacketBusItem_Tests + { + [TestMethod] + public void Fire_Completed_Event() + { + var eventFired = false; + + var item = new MqttPacketBusItem(new MqttPublishPacket()); + item.Completed += (_, __) => + { + eventFired = true; + }; + + item.Complete(); + + Assert.IsTrue(eventFired); + } + + [TestMethod] + [ExpectedException(typeof(TaskCanceledException))] + public async Task Wait_Packet_Bus_Item_After_Already_Canceled() + { + var item = new MqttPacketBusItem(new MqttPublishPacket()); + + // Finish the item before the actual + item.Cancel(); + + await item.WaitAsync(); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs b/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs index b09159fd..40ff02aa 100644 --- a/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs @@ -13,69 +13,26 @@ namespace MQTTnet.Tests.Internal { [TestClass] public sealed class MqttPacketBus_Tests - { - [TestMethod] - [ExpectedException(typeof(TaskCanceledException))] - public async Task Wait_With_Empty_Bus() - { - var bus = new MqttPacketBus(); - - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(1))) - { - await bus.DequeueItemAsync(timeout.Token); - } - } - - [TestMethod] - public async Task Fill_From_Different_Task() - { - const int MessageCount = 500; - - var delayRandom = new Random(); - - var bus = new MqttPacketBus(); - - _ = Task.Run( - () => - { - for (int i = 0; i < MessageCount; i++) - { - bus.EnqueueItem(new MqttPacketBusItem(MqttPingReqPacket.Instance), MqttPacketBusPartition.Health); - - Thread.Sleep(delayRandom.Next(0, 10)); - } - }); - - for (var i = 0; i < MessageCount; i++) - { - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10))) - { - await bus.DequeueItemAsync(timeout.Token); - } - } - - Assert.AreEqual(0, bus.TotalItemsCount); - } - + { [TestMethod] public void Alternate_Priorities() { var bus = new MqttPacketBus(); - + bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); - + bus.EnqueueItem(new MqttPacketBusItem(new MqttSubAckPacket()), MqttPacketBusPartition.Control); bus.EnqueueItem(new MqttPacketBusItem(new MqttSubAckPacket()), MqttPacketBusPartition.Control); bus.EnqueueItem(new MqttPacketBusItem(new MqttSubAckPacket()), MqttPacketBusPartition.Control); - + bus.EnqueueItem(new MqttPacketBusItem(new MqttPingRespPacket()), MqttPacketBusPartition.Health); bus.EnqueueItem(new MqttPacketBusItem(new MqttPingRespPacket()), MqttPacketBusPartition.Health); bus.EnqueueItem(new MqttPacketBusItem(new MqttPingRespPacket()), MqttPacketBusPartition.Health); Assert.AreEqual(9, bus.TotalItemsCount); - + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPublishPacket)); Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttSubAckPacket)); Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPingRespPacket)); @@ -85,33 +42,10 @@ namespace MQTTnet.Tests.Internal Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPublishPacket)); Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttSubAckPacket)); Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPingRespPacket)); - + Assert.AreEqual(0, bus.TotalItemsCount); } - - [TestMethod] - public void Export_Packets_Without_Dequeue() - { - var bus = new MqttPacketBus(); - bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); - bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); - bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); - - Assert.AreEqual(3, bus.TotalItemsCount); - - var exportedPackets = bus.ExportPackets(MqttPacketBusPartition.Control); - Assert.AreEqual(0, exportedPackets.Count); - - exportedPackets = bus.ExportPackets(MqttPacketBusPartition.Health); - Assert.AreEqual(0, exportedPackets.Count); - - exportedPackets = bus.ExportPackets(MqttPacketBusPartition.Data); - Assert.AreEqual(3, exportedPackets.Count); - - Assert.AreEqual(3, bus.TotalItemsCount); - } - [TestMethod] public void Await_Single_Packet() { @@ -121,31 +55,97 @@ namespace MQTTnet.Tests.Internal var item1 = new MqttPacketBusItem(new MqttPublishPacket()); var item2 = new MqttPacketBusItem(new MqttPublishPacket()); - + var item3 = new MqttPacketBusItem(new MqttPublishPacket()); - item3.Delivered += (_, __) => + item3.Completed += (_, __) => { delivered = true; }; - + bus.EnqueueItem(item1, MqttPacketBusPartition.Data); bus.EnqueueItem(item2, MqttPacketBusPartition.Data); bus.EnqueueItem(item3, MqttPacketBusPartition.Data); - + Assert.IsFalse(delivered); - bus.DequeueItemAsync(CancellationToken.None).Result.MarkAsDelivered(); - + bus.DequeueItemAsync(CancellationToken.None).Result.Complete(); + Assert.IsFalse(delivered); - - bus.DequeueItemAsync(CancellationToken.None).Result.MarkAsDelivered(); - + + bus.DequeueItemAsync(CancellationToken.None).Result.Complete(); + Assert.IsFalse(delivered); - - bus.DequeueItemAsync(CancellationToken.None).Result.MarkAsDelivered(); - + + bus.DequeueItemAsync(CancellationToken.None).Result.Complete(); + // The third packet has the event attached. Assert.IsTrue(delivered); } + + [TestMethod] + public void Export_Packets_Without_Dequeue() + { + var bus = new MqttPacketBus(); + + bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); + bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); + bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); + + Assert.AreEqual(3, bus.TotalItemsCount); + + var exportedPackets = bus.ExportPackets(MqttPacketBusPartition.Control); + Assert.AreEqual(0, exportedPackets.Count); + + exportedPackets = bus.ExportPackets(MqttPacketBusPartition.Health); + Assert.AreEqual(0, exportedPackets.Count); + + exportedPackets = bus.ExportPackets(MqttPacketBusPartition.Data); + Assert.AreEqual(3, exportedPackets.Count); + + Assert.AreEqual(3, bus.TotalItemsCount); + } + + [TestMethod] + public async Task Fill_From_Different_Task() + { + const int MessageCount = 500; + + var delayRandom = new Random(); + + var bus = new MqttPacketBus(); + + _ = Task.Run( + () => + { + for (var i = 0; i < MessageCount; i++) + { + bus.EnqueueItem(new MqttPacketBusItem(MqttPingReqPacket.Instance), MqttPacketBusPartition.Health); + + Thread.Sleep(delayRandom.Next(0, 10)); + } + }); + + for (var i = 0; i < MessageCount; i++) + { + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10))) + { + await bus.DequeueItemAsync(timeout.Token); + } + } + + Assert.AreEqual(0, bus.TotalItemsCount); + } + + [TestMethod] + [ExpectedException(typeof(OperationCanceledException))] + public async Task Wait_With_Empty_Bus() + { + var bus = new MqttPacketBus(); + + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(1))) + { + await bus.DequeueItemAsync(timeout.Token); + } + } } } \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Mockups/TestApplicationMessageReceivedHandler.cs b/Source/MQTTnet.Tests/Mockups/TestApplicationMessageReceivedHandler.cs index c1a7c482..7556709a 100644 --- a/Source/MQTTnet.Tests/Mockups/TestApplicationMessageReceivedHandler.cs +++ b/Source/MQTTnet.Tests/Mockups/TestApplicationMessageReceivedHandler.cs @@ -2,8 +2,10 @@ // 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 System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; @@ -17,7 +19,12 @@ namespace MQTTnet.Tests.Mockups public TestApplicationMessageReceivedHandler(IMqttClient mqttClient) { - mqttClient.ApplicationMessageReceivedAsync += MqttClientOnApplicationMessageReceivedAsync; + if (mqttClient == null) + { + throw new ArgumentNullException(nameof(mqttClient)); + } + + mqttClient.ApplicationMessageReceivedAsync += OnApplicationMessageReceivedAsync; } public List ReceivedEventArgs @@ -39,7 +46,25 @@ namespace MQTTnet.Tests.Mockups } } - Task MqttClientOnApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs) + public string GeneratePayloadSequence() + { + var sequence = new StringBuilder(); + + lock (_receivedEventArgs) + { + foreach (var receivedEventArg in _receivedEventArgs) + { + var payload = receivedEventArg.ApplicationMessage.ConvertPayloadToString(); + + // An empty payload is not part of the sequence! + sequence.Append(payload); + } + } + + return sequence.ToString(); + } + + Task OnApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs) { lock (_receivedEventArgs) { diff --git a/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs b/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs index 6b79a963..44d692d9 100644 --- a/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs +++ b/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; using MQTTnet.Diagnostics; +using MQTTnet.Extensions.ManagedClient; using MQTTnet.Extensions.Rpc; using MQTTnet.Formatter; using MQTTnet.Internal; @@ -109,7 +110,7 @@ namespace MQTTnet.Tests.Mockups configureOptions.Invoke(optionsBuilder); var options = optionsBuilder.Build(); - + var client = CreateClient(); if (timeout == TimeSpan.Zero) @@ -137,7 +138,7 @@ namespace MQTTnet.Tests.Mockups options = options.WithTcpServer("127.0.0.1", ServerPort); var client = CreateClient(); - + if (timeout == TimeSpan.Zero) { await client.ConnectAsync(options.Build()).ConfigureAwait(false); @@ -161,7 +162,7 @@ namespace MQTTnet.Tests.Mockups } var client = CreateClient(); - + if (timeout == TimeSpan.Zero) { await client.ConnectAsync(options).ConfigureAwait(false); @@ -199,6 +200,16 @@ namespace MQTTnet.Tests.Mockups return new TestApplicationMessageReceivedHandler(mqttClient); } + public TestApplicationMessageReceivedHandler CreateApplicationMessageHandler(IManagedMqttClient managedClient) + { + if (managedClient == null) + { + throw new ArgumentNullException(nameof(managedClient)); + } + + return new TestApplicationMessageReceivedHandler(managedClient.InternalClient); + } + public IMqttClient CreateClient() { lock (_clients) @@ -325,7 +336,7 @@ namespace MQTTnet.Tests.Mockups var options = optionsBuilder.Build(); var server = CreateServer(options); await server.StartAsync(); - + // The OS has chosen the port to we have to properly expose it to the tests. ServerPort = options.DefaultEndpointOptions.Port; return server; @@ -344,7 +355,7 @@ namespace MQTTnet.Tests.Mockups var options = optionsBuilder.Build(); var server = CreateServer(options); await server.StartAsync(); - + // The OS has chosen the port to we have to properly expose it to the tests. ServerPort = options.DefaultEndpointOptions.Port; return server; diff --git a/Source/MQTTnet.Tests/Server/QoS_Tests.cs b/Source/MQTTnet.Tests/Server/QoS_Tests.cs index 7e5a9491..d6c9e644 100644 --- a/Source/MQTTnet.Tests/Server/QoS_Tests.cs +++ b/Source/MQTTnet.Tests/Server/QoS_Tests.cs @@ -15,6 +15,57 @@ namespace MQTTnet.Tests.Server [TestClass] public sealed class QoS_Tests : BaseTestClass { + [TestMethod] + public async Task Preserve_Message_Order_For_Queued_Messages() + { + using (var testEnvironment = CreateTestEnvironment()) + { + var server = await testEnvironment.StartServer(o => o.WithPersistentSessions()); + + // Create a session which will contain the messages. + var dummyClient = await testEnvironment.ConnectClient(o => o.WithClientId("A").WithCleanSession(false)); + await dummyClient.SubscribeAsync("#", MqttQualityOfServiceLevel.AtLeastOnce); + dummyClient.Dispose(); + + // Now inject messages which are appended to the queue of the client. + await server.InjectApplicationMessage("T", "0", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce); + + await server.InjectApplicationMessage("T", "2", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce); + await server.InjectApplicationMessage("T", "1", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce); + + await server.InjectApplicationMessage("T", "4", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce); + await server.InjectApplicationMessage("T", "3", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce); + + await server.InjectApplicationMessage("T", "6", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce); + await server.InjectApplicationMessage("T", "5", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce); + + await server.InjectApplicationMessage("T", "8", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce); + await server.InjectApplicationMessage("T", "7", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce); + + await server.InjectApplicationMessage("T", "9", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtLeastOnce); + + await LongTestDelay(); + + // Create a new client for the existing message. + var client = await testEnvironment.ConnectClient(o => o.WithClientId("A").WithCleanSession(false)); + var messages = testEnvironment.CreateApplicationMessageHandler(client); + + await LongTestDelay(); + + var payloadSequence = messages.GeneratePayloadSequence(); + Assert.AreEqual("0214365879", payloadSequence); + + // Disconnect and reconnect to make sure that the server will not send the messages twice. + await client.DisconnectAsync(); + await LongTestDelay(); + await client.ReconnectAsync(); + await LongTestDelay(); + + payloadSequence = messages.GeneratePayloadSequence(); + Assert.AreEqual("0214365879", payloadSequence); + } + } + [TestMethod] public async Task Fire_Event_On_Client_Acknowledges_QoS_0() { diff --git a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs index 4b030150..308d5477 100644 --- a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs +++ b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs @@ -86,7 +86,7 @@ namespace MQTTnet.Adapter await Task.WhenAny(connectTask, timeout.Task).ConfigureAwait(false); if (timeout.Task.IsCompleted && !connectTask.IsCompleted) { - throw new OperationCanceledException("MQTT connect cancelled.", cancellationToken); + throw new OperationCanceledException("MQTT connect canceled.", cancellationToken); } // Make sure that the exception from the connect task gets thrown. @@ -317,7 +317,7 @@ namespace MQTTnet.Adapter if (cancellationToken.IsCancellationRequested) { - return ReadFixedHeaderResult.Cancelled; + return ReadFixedHeaderResult.Canceled; } if (bytesRead == 0) diff --git a/Source/MQTTnet/Client/MqttClientExtensions.cs b/Source/MQTTnet/Client/MqttClientExtensions.cs index 9f936bdd..ba917aad 100644 --- a/Source/MQTTnet/Client/MqttClientExtensions.cs +++ b/Source/MQTTnet/Client/MqttClientExtensions.cs @@ -14,26 +14,6 @@ namespace MQTTnet.Client { public static class MqttClientExtensions { - public static async Task TryPingAsync(this IMqttClient client, CancellationToken cancellationToken = default) - { - if (client == null) - { - throw new ArgumentNullException(nameof(client)); - } - - try - { - await client.PingAsync(cancellationToken).ConfigureAwait(false); - return true; - } - catch - { - // Ignore errors. - } - - return false; - } - public static Task DisconnectAsync(this IMqttClient client, MqttClientDisconnectReason reason = MqttClientDisconnectReason.NormalDisconnection, string reasonString = null) { if (client == null) @@ -89,6 +69,17 @@ namespace MQTTnet.Client return mqttClient.PublishBinaryAsync(topic, payloadBuffer, qualityOfServiceLevel, retain, cancellationToken); } + public static Task ReconnectAsync(this IMqttClient client, CancellationToken cancellationToken = default) + { + if (client.Options == null) + { + throw new InvalidOperationException( + "The MQTT client was not connected before. A reconnect is only permitted when the client was already connected or at least tried to."); + } + + return client.ConnectAsync(client.Options, cancellationToken); + } + public static Task SendExtendedAuthenticationExchangeDataAsync(this IMqttClient client, MqttExtendedAuthenticationExchangeData data) { if (client == null) @@ -137,6 +128,26 @@ namespace MQTTnet.Client return mqttClient.SubscribeAsync(subscribeOptions, cancellationToken); } + public static async Task TryPingAsync(this IMqttClient client, CancellationToken cancellationToken = default) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + try + { + await client.PingAsync(cancellationToken).ConfigureAwait(false); + return true; + } + catch + { + // Ignore errors. + } + + return false; + } + public static Task UnsubscribeAsync(this IMqttClient mqttClient, string topic, CancellationToken cancellationToken = default) { if (mqttClient == null) diff --git a/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs b/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs index 58663933..a471a6a7 100644 --- a/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs +++ b/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs @@ -6,9 +6,9 @@ namespace MQTTnet.Formatter { public struct ReadFixedHeaderResult { - public static ReadFixedHeaderResult Cancelled { get; } = new ReadFixedHeaderResult + public static ReadFixedHeaderResult Canceled { get; } = new ReadFixedHeaderResult { - IsCancelled = true + IsCanceled = true }; public static ReadFixedHeaderResult ConnectionClosed { get; } = new ReadFixedHeaderResult @@ -16,7 +16,7 @@ namespace MQTTnet.Formatter IsConnectionClosed = true }; - public bool IsCancelled { get; set; } + public bool IsCanceled { get; set; } public bool IsConnectionClosed { get; set; } diff --git a/Source/MQTTnet/Internal/MqttPacketBusItem.cs b/Source/MQTTnet/Internal/MqttPacketBusItem.cs index 91224533..b94654cf 100644 --- a/Source/MQTTnet/Internal/MqttPacketBusItem.cs +++ b/Source/MQTTnet/Internal/MqttPacketBusItem.cs @@ -10,40 +10,36 @@ namespace MQTTnet.Internal { public sealed class MqttPacketBusItem { - readonly AsyncTaskCompletionSource _promise = new AsyncTaskCompletionSource(); - + readonly AsyncTaskCompletionSource _promise = new AsyncTaskCompletionSource(); + public MqttPacketBusItem(MqttPacket packet) { Packet = packet ?? throw new ArgumentNullException(nameof(packet)); } - + + public event EventHandler Completed; + public MqttPacket Packet { get; } - public event EventHandler Delivered; - - public Task WaitForDeliveryAsync() - { - return _promise.Task; - } - - public void MarkAsDelivered() - { - if (_promise.TrySetResult(0)) - { - Delivered?.Invoke(this, EventArgs.Empty); - } - } - - public void MarkAsFailed(Exception exception) - { - if (exception == null) throw new ArgumentNullException(nameof(exception)); - - _promise.TrySetException(exception); - } - - public void MarkAsCancelled() + public void Cancel() { _promise.TrySetCanceled(); } + + public void Complete() + { + _promise.TrySetResult(true); + Completed?.Invoke(this, EventArgs.Empty); + } + + public void Fail(Exception exception) + { + _promise.TrySetException(exception); + } + + public Task WaitAsync() + { + return _promise.Task; + } } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/MqttClient.cs b/Source/MQTTnet/Server/Internal/MqttClient.cs index f9f7e695..f505b00c 100644 --- a/Source/MQTTnet/Server/Internal/MqttClient.cs +++ b/Source/MQTTnet/Server/Internal/MqttClient.cs @@ -507,15 +507,15 @@ namespace MQTTnet.Server try { await SendPacketAsync(packetBusItem.Packet, cancellationToken).ConfigureAwait(false); - packetBusItem.MarkAsDelivered(); + packetBusItem.Complete(); } catch (OperationCanceledException) { - packetBusItem.MarkAsCancelled(); + packetBusItem.Cancel(); } catch (Exception exception) { - packetBusItem.MarkAsFailed(exception); + packetBusItem.Fail(exception); } finally { @@ -563,7 +563,7 @@ namespace MQTTnet.Server catch (ObjectDisposedException) { // This can happen when connections are created and dropped very quickly. - // It is not an issue if the cancellation token cannot be cancelled multiple times. + // It is not an issue if the cancellation token cannot be canceled multiple times. } } diff --git a/Source/MQTTnet/Server/Internal/MqttSession.cs b/Source/MQTTnet/Server/Internal/MqttSession.cs index a5a045bf..b40c2645 100644 --- a/Source/MQTTnet/Server/Internal/MqttSession.cs +++ b/Source/MQTTnet/Server/Internal/MqttSession.cs @@ -4,7 +4,6 @@ using System; using System.Collections; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -25,7 +24,8 @@ namespace MQTTnet.Server readonly MqttServerOptions _serverOptions; readonly MqttClientSubscriptionsManager _subscriptionsManager; - readonly ConcurrentDictionary _unacknowledgedPublishPackets = new ConcurrentDictionary(); + // Do not use a dictionary in order to keep the ordering of the messages. + readonly List _unacknowledgedPublishPackets = new List(); // Bookkeeping to know if this is a subscribing client; lazy initialize later. HashSet _subscribedTopics; @@ -72,7 +72,14 @@ namespace MQTTnet.Server public MqttPublishPacket AcknowledgePublishPacket(ushort packetIdentifier) { - _unacknowledgedPublishPackets.TryRemove(packetIdentifier, out var publishPacket); + MqttPublishPacket publishPacket; + + lock (_unacknowledgedPublishPackets) + { + publishPacket = _unacknowledgedPublishPackets.FirstOrDefault(p => p.PacketIdentifier.Equals(packetIdentifier)); + _unacknowledgedPublishPackets.Remove(publishPacket); + } + return publishPacket; } @@ -130,7 +137,10 @@ namespace MQTTnet.Server { publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier(); - _unacknowledgedPublishPackets[publishPacket.PacketIdentifier] = publishPacket; + lock (_unacknowledgedPublishPackets) + { + _unacknowledgedPublishPackets.Add(publishPacket); + } } _packetBus.EnqueueItem(packetBusItem, MqttPacketBusPartition.Data); @@ -145,8 +155,10 @@ namespace MQTTnet.Server { // This will only return the matching PUBLISH packet but does not remove it. // This is required for QoS 2. - _unacknowledgedPublishPackets.TryGetValue(packetIdentifier, out var publishPacket); - return publishPacket; + lock (_unacknowledgedPublishPackets) + { + return _unacknowledgedPublishPackets.FirstOrDefault(p => p.PacketIdentifier.Equals(packetIdentifier)); + } } public void Recover() @@ -154,9 +166,33 @@ namespace MQTTnet.Server // TODO: Keep the bus and only insert pending items again. // TODO: Check if packet identifier must be restarted or not. // TODO: Recover package identifier. + + /* + The Session state in the Client consists of: + · QoS 1 and QoS 2 messages which have been sent to the Server, but have not been completely acknowledged. + · QoS 2 messages which have been received from the Server, but have not been completely acknowledged. + + The Session state in the Server consists of: + · The existence of a Session, even if the rest of the Session state is empty. + · The Client’s subscriptions. + · QoS 1 and QoS 2 messages which have been sent to the Client, but have not been completely acknowledged. + · QoS 1 and QoS 2 messages pending transmission to the Client. + · QoS 2 messages which have been received from the Client, but have not been completely acknowledged. + · Optionally, QoS 0 messages pending transmission to the Client. + */ + + // Create a copy of all currently unacknowledged publish packets and clear the storage. + // We must re-enqueue them in order to trigger other code. + List unacknowledgedPublishPackets; + lock (_unacknowledgedPublishPackets) + { + unacknowledgedPublishPackets = _unacknowledgedPublishPackets.ToList(); + _unacknowledgedPublishPackets.Clear(); + } + _packetBus.Clear(); - foreach (var publishPacket in _unacknowledgedPublishPackets.Values.ToList()) + foreach (var publishPacket in unacknowledgedPublishPackets) { EnqueueDataPacket(new MqttPacketBusItem(publishPacket)); } diff --git a/Source/MQTTnet/Server/MqttServerExtensions.cs b/Source/MQTTnet/Server/MqttServerExtensions.cs index 22d8538c..13746e50 100644 --- a/Source/MQTTnet/Server/MqttServerExtensions.cs +++ b/Source/MQTTnet/Server/MqttServerExtensions.cs @@ -3,29 +3,89 @@ // See the LICENSE file in the project root for more information. using System; +using System.Text; using System.Threading.Tasks; +using MQTTnet.Internal; using MQTTnet.Packets; +using MQTTnet.Protocol; namespace MQTTnet.Server { public static class MqttServerExtensions { + public static Task InjectApplicationMessage( + this MqttServer server, + string topic, + string payload = null, + MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce, + bool retain = false) + { + if (server == null) + { + throw new ArgumentNullException(nameof(server)); + } + + if (topic == null) + { + throw new ArgumentNullException(nameof(topic)); + } + + var payloadBuffer = EmptyBuffer.Array; + if (payload is string stringPayload) + { + payloadBuffer = Encoding.UTF8.GetBytes(stringPayload); + } + + return server.InjectApplicationMessage( + new InjectedMqttApplicationMessage( + new MqttApplicationMessage + { + Topic = topic, + Payload = payloadBuffer, + QualityOfServiceLevel = qualityOfServiceLevel, + Retain = retain + })); + } + public static Task SubscribeAsync(this MqttServer server, string clientId, params MqttTopicFilter[] topicFilters) { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (clientId == null) throw new ArgumentNullException(nameof(clientId)); - if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + if (server == null) + { + throw new ArgumentNullException(nameof(server)); + } + + if (clientId == null) + { + throw new ArgumentNullException(nameof(clientId)); + } + + if (topicFilters == null) + { + throw new ArgumentNullException(nameof(topicFilters)); + } return server.SubscribeAsync(clientId, topicFilters); } - + public static Task SubscribeAsync(this MqttServer server, string clientId, string topic) { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (clientId == null) throw new ArgumentNullException(nameof(clientId)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); + if (server == null) + { + throw new ArgumentNullException(nameof(server)); + } - return server.SubscribeAsync(clientId, new MqttTopicFilterBuilder().WithTopic(topic).Build()); + if (clientId == null) + { + throw new ArgumentNullException(nameof(clientId)); + } + + if (topic == null) + { + throw new ArgumentNullException(nameof(topic)); + } + + var topicFilters = new MqttTopicFilterBuilder().WithTopic(topic).Build(); + return server.SubscribeAsync(clientId, topicFilters); } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Status/MqttSessionStatus.cs b/Source/MQTTnet/Server/Status/MqttSessionStatus.cs index 6cea318b..c6df1da5 100644 --- a/Source/MQTTnet/Server/Status/MqttSessionStatus.cs +++ b/Source/MQTTnet/Server/Status/MqttSessionStatus.cs @@ -45,7 +45,7 @@ namespace MQTTnet.Server var packetBusItem = new MqttPacketBusItem(publishPacketFactory.Create(applicationMessage)); _session.EnqueueDataPacket(packetBusItem); - return packetBusItem.WaitForDeliveryAsync(); + return packetBusItem.WaitAsync(); } public Task DeleteAsync() From 4eb6e77eb50b4ec6e42cf597e63ea47a6ff3e7a3 Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Fri, 11 Nov 2022 14:41:28 +0100 Subject: [PATCH 22/24] Fix build errors due to merge conflicts --- Source/MQTTnet/Adapter/MqttChannelAdapter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs index 308d5477..8e08dd0f 100644 --- a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs +++ b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs @@ -298,7 +298,7 @@ namespace MQTTnet.Adapter // Check two times for cancellation because the call to _ReadAsync_ might block for some time. if (cancellationToken.IsCancellationRequested) { - return ReadFixedHeaderResult.Cancelled; + return ReadFixedHeaderResult.Canceled; } int bytesRead; @@ -308,7 +308,7 @@ namespace MQTTnet.Adapter } catch (OperationCanceledException) { - return ReadFixedHeaderResult.Cancelled; + return ReadFixedHeaderResult.Canceled; } catch (SocketException) { From e4b07f2b8caa3295b80b4580263f2d61fa775202 Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sat, 12 Nov 2022 09:43:55 +0100 Subject: [PATCH 23/24] Fix build errors --- Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs b/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs index 40ff02aa..62f547ef 100644 --- a/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs @@ -137,7 +137,7 @@ namespace MQTTnet.Tests.Internal } [TestMethod] - [ExpectedException(typeof(OperationCanceledException))] + [ExpectedException(typeof(TaskCanceledException))] public async Task Wait_With_Empty_Bus() { var bus = new MqttPacketBus(); From e88b280eb0fac04267487555399bff7241af4349 Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sat, 12 Nov 2022 12:53:54 +0100 Subject: [PATCH 24/24] Reset release notes --- .github/workflows/ReleaseNotes.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index ace201e0..8b137891 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -1,8 +1 @@ -* [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) -* [ManagedClient] The topic is no longer declared as invalid if a topic alias is used (#1395) -* [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 memory leak when old sessions are discarded (#1553) -* [Server] When using MQTTv5 the reason code _NoMatchingSubscribers_ is returned when no client is subscribed to a published topic (#802) -* [Server] Fix sending of duplicate messages with QoS > 0 when session is recovered and session is reused (#1546) +