mirror of
https://github.com/dotnet/MQTTnet.git
synced 2026-10-15 00:15:15 +08:00
Improve session message injection (#2117)
* Improve session message injection methods * Add more tests for session injection * Change return type from MqttPublishPacket to MqttPacketWithIdentifier * Use dedicated type for MattApplicationMessage injection result * Code review minor fixes * Apply project code style * Update release notes --------- Co-authored-by: Anton Smolkov <anton.smolkov@idp.zyfra.com> Co-authored-by: christian <6939810+chkr1011@users.noreply.github.com>
This commit is contained in:
co-authored by
Anton Smolkov
christian
parent
e7de2b913a
commit
27771f0146
@@ -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.
|
||||||
|
|
||||||
|
namespace MQTTnet.Server.Exceptions;
|
||||||
|
|
||||||
|
public class MqttPendingMessagesOverflowException : Exception
|
||||||
|
{
|
||||||
|
public MqttPendingMessagesOverflowException(string sessionId, MqttPendingMessagesOverflowStrategy overflowStrategy) : base(
|
||||||
|
$"Send buffer max pending messages overflow occurred for session '{sessionId}'. Strategy: {overflowStrategy}.")
|
||||||
|
{
|
||||||
|
SessionId = sessionId;
|
||||||
|
OverflowStrategy = overflowStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MqttPendingMessagesOverflowStrategy OverflowStrategy { get; }
|
||||||
|
|
||||||
|
public string SessionId { get; }
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ using System.Collections;
|
|||||||
using MQTTnet.Internal;
|
using MQTTnet.Internal;
|
||||||
using MQTTnet.Packets;
|
using MQTTnet.Packets;
|
||||||
using MQTTnet.Protocol;
|
using MQTTnet.Protocol;
|
||||||
|
using MQTTnet.Server.Exceptions;
|
||||||
|
|
||||||
namespace MQTTnet.Server.Internal;
|
namespace MQTTnet.Server.Internal;
|
||||||
|
|
||||||
@@ -111,10 +112,11 @@ public sealed class MqttSession : IDisposable
|
|||||||
|
|
||||||
public EnqueueDataPacketResult EnqueueDataPacket(MqttPacketBusItem packetBusItem)
|
public EnqueueDataPacketResult EnqueueDataPacket(MqttPacketBusItem packetBusItem)
|
||||||
{
|
{
|
||||||
if (_packetBus.ItemsCount(MqttPacketBusPartition.Data) >= _serverOptions.MaxPendingMessagesPerClient)
|
if (PendingDataPacketsCount >= _serverOptions.MaxPendingMessagesPerClient)
|
||||||
{
|
{
|
||||||
if (_serverOptions.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropNewMessage)
|
if (_serverOptions.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropNewMessage)
|
||||||
{
|
{
|
||||||
|
packetBusItem.Fail(new MqttPendingMessagesOverflowException(Id, _serverOptions.PendingMessagesOverflowStrategy));
|
||||||
return EnqueueDataPacketResult.Dropped;
|
return EnqueueDataPacketResult.Dropped;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,10 +125,15 @@ public sealed class MqttSession : IDisposable
|
|||||||
// Only drop from the data partition. Dropping from control partition might break the connection
|
// Only drop from the data partition. Dropping from control partition might break the connection
|
||||||
// because the client does not receive PINGREQ packets etc. any longer.
|
// because the client does not receive PINGREQ packets etc. any longer.
|
||||||
var firstItem = _packetBus.DropFirstItem(MqttPacketBusPartition.Data);
|
var firstItem = _packetBus.DropFirstItem(MqttPacketBusPartition.Data);
|
||||||
if (firstItem != null && _eventContainer.QueuedApplicationMessageOverwrittenEvent.HasHandlers)
|
if (firstItem != null)
|
||||||
{
|
{
|
||||||
var eventArgs = new QueueMessageOverwrittenEventArgs(Id, firstItem.Packet);
|
firstItem.Fail(new MqttPendingMessagesOverflowException(Id, _serverOptions.PendingMessagesOverflowStrategy));
|
||||||
_eventContainer.QueuedApplicationMessageOverwrittenEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
|
||||||
|
if (_eventContainer.QueuedApplicationMessageOverwrittenEvent.HasHandlers)
|
||||||
|
{
|
||||||
|
var eventArgs = new QueueMessageOverwrittenEventArgs(Id, firstItem.Packet);
|
||||||
|
_eventContainer.QueuedApplicationMessageOverwrittenEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,12 @@ namespace MQTTnet.Server
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MqttServerOptionsBuilder WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy value)
|
||||||
|
{
|
||||||
|
_options.PendingMessagesOverflowStrategy = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public MqttServerOptionsBuilder WithoutDefaultEndpoint()
|
public MqttServerOptionsBuilder WithoutDefaultEndpoint()
|
||||||
{
|
{
|
||||||
_options.DefaultEndpointOptions.IsEnabled = false;
|
_options.DefaultEndpointOptions.IsEnabled = false;
|
||||||
|
|||||||
@@ -40,22 +40,65 @@ public sealed class MqttSessionStatus
|
|||||||
return _session.DeleteAsync();
|
return _session.DeleteAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task DeliverApplicationMessageAsync(MqttApplicationMessage applicationMessage)
|
/// <summary>
|
||||||
|
/// Delivers an application message immediately to the session.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="applicationMessage">The application message to deliver.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A task that represents the asynchronous operation.
|
||||||
|
/// The result contains the <see cref="InjectMqttApplicationMessageResult"/> that includes the packet identifier of the enqueued message.
|
||||||
|
/// </returns>
|
||||||
|
public async Task<InjectMqttApplicationMessageResult> DeliverApplicationMessageAsync(MqttApplicationMessage applicationMessage)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(applicationMessage);
|
ArgumentNullException.ThrowIfNull(applicationMessage);
|
||||||
|
|
||||||
var packetBusItem = new MqttPacketBusItem(MqttPublishPacketFactory.Create(applicationMessage));
|
var publishPacket = MqttPublishPacketFactory.Create(applicationMessage);
|
||||||
|
var packetBusItem = new MqttPacketBusItem(publishPacket);
|
||||||
_session.EnqueueDataPacket(packetBusItem);
|
_session.EnqueueDataPacket(packetBusItem);
|
||||||
|
|
||||||
return packetBusItem.WaitAsync();
|
await packetBusItem.WaitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
var injectResult = new InjectMqttApplicationMessageResult()
|
||||||
|
{
|
||||||
|
PacketIdentifier = publishPacket.PacketIdentifier
|
||||||
|
};
|
||||||
|
|
||||||
|
return injectResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task EnqueueApplicationMessageAsync(MqttApplicationMessage applicationMessage)
|
/// <summary>
|
||||||
|
/// Attempts to enqueue an application message to the session's send buffer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="applicationMessage">The application message to enqueue.</param>
|
||||||
|
/// <param name="injectResult"><see cref="InjectMqttApplicationMessageResult"/> that includes the packet identifier of the enqueued message.</param>
|
||||||
|
/// <returns><c>true</c> if the message was successfully enqueued; otherwise, <c>false</c>.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// When <see cref="MqttServerOptions.PendingMessagesOverflowStrategy"/> is set to <see cref="MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage"/>,
|
||||||
|
/// this method always returns <c>true</c>.
|
||||||
|
/// However, an existing message in the queue may be <b>dropped later</b> to make room for the newly enqueued message.
|
||||||
|
/// Such dropped messages can be tracked by subscribing to <see cref="MqttServer.QueuedApplicationMessageOverwrittenAsync"/> event.
|
||||||
|
/// </remarks>
|
||||||
|
public bool TryEnqueueApplicationMessage(MqttApplicationMessage applicationMessage, out InjectMqttApplicationMessageResult injectResult)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(applicationMessage);
|
ArgumentNullException.ThrowIfNull(applicationMessage);
|
||||||
|
|
||||||
_session.EnqueueDataPacket(new MqttPacketBusItem(MqttPublishPacketFactory.Create(applicationMessage)));
|
var publishPacket = MqttPublishPacketFactory.Create(applicationMessage);
|
||||||
|
var enqueueDataPacketResult = _session.EnqueueDataPacket(new MqttPacketBusItem(publishPacket));
|
||||||
|
|
||||||
|
if (enqueueDataPacketResult != EnqueueDataPacketResult.Enqueued)
|
||||||
|
{
|
||||||
|
injectResult = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
injectResult = new InjectMqttApplicationMessageResult() { PacketIdentifier = publishPacket.PacketIdentifier };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Obsolete("This method is obsolete. Use TryEnqueueApplicationMessage instead.")]
|
||||||
|
public Task EnqueueApplicationMessageAsync(MqttApplicationMessage applicationMessage)
|
||||||
|
{
|
||||||
|
TryEnqueueApplicationMessage(applicationMessage, out _);
|
||||||
return CompletedTask.Instance;
|
return CompletedTask.Instance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,10 +13,11 @@ namespace MQTTnet.Tests
|
|||||||
public abstract class BaseTestClass
|
public abstract class BaseTestClass
|
||||||
{
|
{
|
||||||
public TestContext TestContext { get; set; }
|
public TestContext TestContext { get; set; }
|
||||||
|
|
||||||
protected TestEnvironment CreateTestEnvironment(MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311)
|
protected TestEnvironment CreateTestEnvironment(
|
||||||
|
MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311, bool trackUnobservedTaskException = true)
|
||||||
{
|
{
|
||||||
return new TestEnvironment(TestContext, protocolVersion);
|
return new TestEnvironment(TestContext, protocolVersion, trackUnobservedTaskException);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Task LongTestDelay()
|
protected Task LongTestDelay()
|
||||||
|
|||||||
@@ -32,12 +32,16 @@ namespace MQTTnet.Tests.Mockups
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestEnvironment(TestContext testContext, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311)
|
public TestEnvironment(
|
||||||
|
TestContext testContext, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311, bool trackUnobservedTaskException = true)
|
||||||
{
|
{
|
||||||
_protocolVersion = protocolVersion;
|
_protocolVersion = protocolVersion;
|
||||||
TestContext = testContext;
|
TestContext = testContext;
|
||||||
|
|
||||||
TaskScheduler.UnobservedTaskException += TrackUnobservedTaskException;
|
if (trackUnobservedTaskException)
|
||||||
|
{
|
||||||
|
TaskScheduler.UnobservedTaskException += TrackUnobservedTaskException;
|
||||||
|
}
|
||||||
|
|
||||||
ServerLogger.LogMessagePublished += (s, e) =>
|
ServerLogger.LogMessagePublished += (s, e) =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
using MQTTnet.Internal;
|
using MQTTnet.Internal;
|
||||||
|
using MQTTnet.Packets;
|
||||||
|
using MQTTnet.Protocol;
|
||||||
using MQTTnet.Server;
|
using MQTTnet.Server;
|
||||||
|
using MQTTnet.Server.Exceptions;
|
||||||
|
|
||||||
namespace MQTTnet.Tests.Server
|
namespace MQTTnet.Tests.Server
|
||||||
{
|
{
|
||||||
@@ -9,79 +13,443 @@ namespace MQTTnet.Tests.Server
|
|||||||
public sealed class Injection_Tests : BaseTestClass
|
public sealed class Injection_Tests : BaseTestClass
|
||||||
{
|
{
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task Inject_Application_Message_At_Session_Level()
|
public async Task Enqueue_Application_Message_At_Session_Level()
|
||||||
{
|
{
|
||||||
using (var testEnvironment = CreateTestEnvironment())
|
using var testEnvironment = CreateTestEnvironment();
|
||||||
|
|
||||||
|
var server = await testEnvironment.StartServer();
|
||||||
|
var receiver1 = await testEnvironment.ConnectClient();
|
||||||
|
var receiver2 = await testEnvironment.ConnectClient();
|
||||||
|
var messageReceivedHandler1 = testEnvironment.CreateApplicationMessageHandler(receiver1);
|
||||||
|
var messageReceivedHandler2 = testEnvironment.CreateApplicationMessageHandler(receiver2);
|
||||||
|
|
||||||
|
var status = await server.GetSessionsAsync();
|
||||||
|
var clientStatus = status[0];
|
||||||
|
|
||||||
|
await receiver1.SubscribeAsync("#");
|
||||||
|
await receiver2.SubscribeAsync("#");
|
||||||
|
|
||||||
|
var message = new MqttApplicationMessageBuilder()
|
||||||
|
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
|
||||||
|
.WithTopic("InjectedOne").Build();
|
||||||
|
|
||||||
|
var enqueued = clientStatus.TryEnqueueApplicationMessage(message, out var injectResult);
|
||||||
|
|
||||||
|
Assert.IsTrue(enqueued);
|
||||||
|
|
||||||
|
await LongTestDelay();
|
||||||
|
|
||||||
|
Assert.AreEqual(1, messageReceivedHandler1.ReceivedEventArgs.Count);
|
||||||
|
Assert.AreEqual(injectResult.PacketIdentifier, messageReceivedHandler1.ReceivedEventArgs[0].PacketIdentifier);
|
||||||
|
Assert.AreEqual("InjectedOne", messageReceivedHandler1.ReceivedEventArgs[0].ApplicationMessage.Topic);
|
||||||
|
|
||||||
|
// The second receiver should NOT receive the message.
|
||||||
|
Assert.AreEqual(0, messageReceivedHandler2.ReceivedEventArgs.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task Enqueue_Application_Message_At_Session_Level_QueueOverflow_DropNewMessageStrategy()
|
||||||
|
{
|
||||||
|
using var testEnvironment = CreateTestEnvironment(trackUnobservedTaskException: false);
|
||||||
|
|
||||||
|
var server = await testEnvironment.StartServer(
|
||||||
|
builder => builder
|
||||||
|
.WithMaxPendingMessagesPerClient(1)
|
||||||
|
.WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropNewMessage));
|
||||||
|
|
||||||
|
var receiver = await testEnvironment.ConnectClient();
|
||||||
|
|
||||||
|
var firstMessageOutboundPacketInterceptedTcs = new TaskCompletionSource();
|
||||||
|
server.InterceptingOutboundPacketAsync += async args =>
|
||||||
{
|
{
|
||||||
var server = await testEnvironment.StartServer();
|
// - The first message is dequeued normally and calls this delay
|
||||||
var receiver1 = await testEnvironment.ConnectClient();
|
// - The second message fills the outbound queue
|
||||||
var receiver2 = await testEnvironment.ConnectClient();
|
// - The third message overflows the outbound queue
|
||||||
var messageReceivedHandler1 = testEnvironment.CreateApplicationMessageHandler(receiver1);
|
if (args.Packet is MqttPublishPacket)
|
||||||
var messageReceivedHandler2 = testEnvironment.CreateApplicationMessageHandler(receiver2);
|
{
|
||||||
|
firstMessageOutboundPacketInterceptedTcs.SetResult();
|
||||||
|
await Task.Delay(TimeSpan.FromDays(1), args.CancellationToken);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var status = await server.GetSessionsAsync();
|
var firstMessageEvicted = false;
|
||||||
var clientStatus = status[0];
|
var secondMessageEvicted = false;
|
||||||
|
var thirdMessageEvicted = false;
|
||||||
|
|
||||||
await receiver1.SubscribeAsync("#");
|
server.QueuedApplicationMessageOverwrittenAsync += args =>
|
||||||
await receiver2.SubscribeAsync("#");
|
{
|
||||||
|
if (args.Packet is not MqttPublishPacket publishPacket)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
await clientStatus.EnqueueApplicationMessageAsync(new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build());
|
switch (publishPacket.Topic)
|
||||||
|
{
|
||||||
|
case "InjectedOne":
|
||||||
|
firstMessageEvicted = true;
|
||||||
|
break;
|
||||||
|
case "InjectedTwo":
|
||||||
|
secondMessageEvicted = true;
|
||||||
|
break;
|
||||||
|
case "InjectedThree":
|
||||||
|
thirdMessageEvicted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
await LongTestDelay();
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
Assert.AreEqual(1, messageReceivedHandler1.ReceivedEventArgs.Count);
|
var status = await server.GetSessionsAsync();
|
||||||
Assert.AreEqual("InjectedOne", messageReceivedHandler1.ReceivedEventArgs[0].ApplicationMessage.Topic);
|
var clientStatus = status[0];
|
||||||
|
await receiver.SubscribeAsync("#");
|
||||||
|
|
||||||
// The second receiver should NOT receive the message.
|
var firstMessageEnqueued = clientStatus.TryEnqueueApplicationMessage(
|
||||||
Assert.AreEqual(0, messageReceivedHandler2.ReceivedEventArgs.Count);
|
new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build(), out _);
|
||||||
}
|
await firstMessageOutboundPacketInterceptedTcs.Task;
|
||||||
|
|
||||||
|
var secondMessageEnqueued = clientStatus.TryEnqueueApplicationMessage(
|
||||||
|
new MqttApplicationMessageBuilder().WithTopic("InjectedTwo").Build(), out _);
|
||||||
|
|
||||||
|
var thirdMessageEnqueued = clientStatus.TryEnqueueApplicationMessage(
|
||||||
|
new MqttApplicationMessageBuilder().WithTopic("InjectedThree").Build(), out _);
|
||||||
|
|
||||||
|
// Due to the DropNewMessage strategy the third message will not be enqueued.
|
||||||
|
// As a result, no existing messages in the queue will be dropped (evicted).
|
||||||
|
Assert.IsTrue(firstMessageEnqueued);
|
||||||
|
Assert.IsTrue(secondMessageEnqueued);
|
||||||
|
Assert.IsFalse(thirdMessageEnqueued);
|
||||||
|
|
||||||
|
Assert.IsFalse(firstMessageEvicted);
|
||||||
|
Assert.IsFalse(secondMessageEvicted);
|
||||||
|
Assert.IsFalse(thirdMessageEvicted);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task Enqueue_Application_Message_At_Session_Level_QueueOverflow_DropOldestQueuedMessageStrategy()
|
||||||
|
{
|
||||||
|
using var testEnvironment = CreateTestEnvironment(trackUnobservedTaskException: false);
|
||||||
|
|
||||||
|
var server = await testEnvironment.StartServer(
|
||||||
|
builder => builder
|
||||||
|
.WithMaxPendingMessagesPerClient(1)
|
||||||
|
.WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage));
|
||||||
|
|
||||||
|
var receiver = await testEnvironment.ConnectClient();
|
||||||
|
|
||||||
|
var firstMessageOutboundPacketInterceptedTcs = new TaskCompletionSource();
|
||||||
|
server.InterceptingOutboundPacketAsync += async args =>
|
||||||
|
{
|
||||||
|
// - The first message is dequeued normally and calls this delay
|
||||||
|
// - The second message fills the outbound queue
|
||||||
|
// - The third message overflows the outbound queue
|
||||||
|
if (args.Packet is MqttPublishPacket)
|
||||||
|
{
|
||||||
|
firstMessageOutboundPacketInterceptedTcs.SetResult();
|
||||||
|
await Task.Delay(TimeSpan.FromDays(1), args.CancellationToken);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var firstMessageEvicted = false;
|
||||||
|
var secondMessageEvicted = false;
|
||||||
|
var thirdMessageEvicted = false;
|
||||||
|
|
||||||
|
server.QueuedApplicationMessageOverwrittenAsync += args =>
|
||||||
|
{
|
||||||
|
if (args.Packet is not MqttPublishPacket publishPacket)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (publishPacket.Topic)
|
||||||
|
{
|
||||||
|
case "InjectedOne":
|
||||||
|
firstMessageEvicted = true;
|
||||||
|
break;
|
||||||
|
case "InjectedTwo":
|
||||||
|
secondMessageEvicted = true;
|
||||||
|
break;
|
||||||
|
case "InjectedThree":
|
||||||
|
thirdMessageEvicted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
var status = await server.GetSessionsAsync();
|
||||||
|
var clientStatus = status[0];
|
||||||
|
await receiver.SubscribeAsync("#");
|
||||||
|
|
||||||
|
var firstMessageEnqueued = clientStatus.TryEnqueueApplicationMessage(
|
||||||
|
new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build(), out _);
|
||||||
|
await firstMessageOutboundPacketInterceptedTcs.Task;
|
||||||
|
|
||||||
|
var secondMessageEnqueued = clientStatus.TryEnqueueApplicationMessage(
|
||||||
|
new MqttApplicationMessageBuilder().WithTopic("InjectedTwo").Build(), out _);
|
||||||
|
|
||||||
|
var thirdMessageEnqueued = clientStatus.TryEnqueueApplicationMessage(
|
||||||
|
new MqttApplicationMessageBuilder().WithTopic("InjectedThree").Build(), out _);
|
||||||
|
|
||||||
|
// Due to the DropOldestQueuedMessage strategy, all messages will be enqueued initially.
|
||||||
|
// But the second message will eventually be dropped (evicted) to make room for the third one.
|
||||||
|
Assert.IsTrue(firstMessageEnqueued);
|
||||||
|
Assert.IsTrue(secondMessageEnqueued);
|
||||||
|
Assert.IsTrue(thirdMessageEnqueued);
|
||||||
|
|
||||||
|
Assert.IsFalse(firstMessageEvicted);
|
||||||
|
Assert.IsTrue(secondMessageEvicted);
|
||||||
|
Assert.IsFalse(thirdMessageEvicted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task Deliver_Application_Message_At_Session_Level()
|
||||||
|
{
|
||||||
|
using var testEnvironment = CreateTestEnvironment();
|
||||||
|
|
||||||
|
var server = await testEnvironment.StartServer();
|
||||||
|
var receiver1 = await testEnvironment.ConnectClient();
|
||||||
|
var receiver2 = await testEnvironment.ConnectClient();
|
||||||
|
var messageReceivedHandler1 = testEnvironment.CreateApplicationMessageHandler(receiver1);
|
||||||
|
var messageReceivedHandler2 = testEnvironment.CreateApplicationMessageHandler(receiver2);
|
||||||
|
|
||||||
|
var status = await server.GetSessionsAsync();
|
||||||
|
var clientStatus = status[0];
|
||||||
|
|
||||||
|
await receiver1.SubscribeAsync("#");
|
||||||
|
await receiver2.SubscribeAsync("#");
|
||||||
|
|
||||||
|
var mqttApplicationMessage = new MqttApplicationMessageBuilder()
|
||||||
|
.WithTopic("InjectedOne")
|
||||||
|
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
|
||||||
|
.Build();
|
||||||
|
var injectResult = await clientStatus.DeliverApplicationMessageAsync(mqttApplicationMessage);
|
||||||
|
|
||||||
|
await LongTestDelay();
|
||||||
|
|
||||||
|
Assert.AreEqual(1, messageReceivedHandler1.ReceivedEventArgs.Count);
|
||||||
|
Assert.AreEqual(injectResult.PacketIdentifier, messageReceivedHandler1.ReceivedEventArgs[0].PacketIdentifier);
|
||||||
|
Assert.AreEqual("InjectedOne", messageReceivedHandler1.ReceivedEventArgs[0].ApplicationMessage.Topic);
|
||||||
|
|
||||||
|
// The second receiver should NOT receive the message.
|
||||||
|
Assert.AreEqual(0, messageReceivedHandler2.ReceivedEventArgs.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task Deliver_Application_Message_At_Session_Level_QueueOverflow_DropNewMessageStrategy()
|
||||||
|
{
|
||||||
|
using var testEnvironment = CreateTestEnvironment();
|
||||||
|
|
||||||
|
var server = await testEnvironment.StartServer(
|
||||||
|
builder => builder
|
||||||
|
.WithMaxPendingMessagesPerClient(1)
|
||||||
|
.WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropNewMessage));
|
||||||
|
|
||||||
|
var receiver = await testEnvironment.ConnectClient();
|
||||||
|
|
||||||
|
var firstMessageOutboundPacketInterceptedTcs = new TaskCompletionSource();
|
||||||
|
server.InterceptingOutboundPacketAsync += async args =>
|
||||||
|
{
|
||||||
|
// - The first message is dequeued normally and calls this delay
|
||||||
|
// - The second message fills the outbound queue
|
||||||
|
// - The third message overflows the outbound queue
|
||||||
|
if (args.Packet is MqttPublishPacket)
|
||||||
|
{
|
||||||
|
firstMessageOutboundPacketInterceptedTcs.SetResult();
|
||||||
|
await Task.Delay(TimeSpan.FromDays(1), args.CancellationToken);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var firstMessageEvicted = false;
|
||||||
|
var secondMessageEvicted = false;
|
||||||
|
var thirdMessageEvicted = false;
|
||||||
|
|
||||||
|
server.QueuedApplicationMessageOverwrittenAsync += args =>
|
||||||
|
{
|
||||||
|
if (args.Packet is not MqttPublishPacket publishPacket)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (publishPacket.Topic)
|
||||||
|
{
|
||||||
|
case "InjectedOne":
|
||||||
|
firstMessageEvicted = true;
|
||||||
|
break;
|
||||||
|
case "InjectedTwo":
|
||||||
|
secondMessageEvicted = true;
|
||||||
|
break;
|
||||||
|
case "InjectedThree":
|
||||||
|
thirdMessageEvicted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
var status = await server.GetSessionsAsync();
|
||||||
|
var clientStatus = status[0];
|
||||||
|
await receiver.SubscribeAsync("#");
|
||||||
|
|
||||||
|
var firstMessageTask = Task.Run(
|
||||||
|
() => clientStatus.DeliverApplicationMessageAsync(
|
||||||
|
new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build()));
|
||||||
|
await LongTestDelay();
|
||||||
|
await firstMessageOutboundPacketInterceptedTcs.Task;
|
||||||
|
|
||||||
|
var secondMessageTask = Task.Run(
|
||||||
|
() => clientStatus.DeliverApplicationMessageAsync(
|
||||||
|
new MqttApplicationMessageBuilder().WithTopic("InjectedTwo").Build()));
|
||||||
|
await LongTestDelay();
|
||||||
|
|
||||||
|
var thirdMessageTask = Task.Run(
|
||||||
|
() => clientStatus.DeliverApplicationMessageAsync(
|
||||||
|
new MqttApplicationMessageBuilder().WithTopic("InjectedThree").Build()));
|
||||||
|
await LongTestDelay();
|
||||||
|
|
||||||
|
Task.WaitAny(firstMessageTask, secondMessageTask, thirdMessageTask);
|
||||||
|
|
||||||
|
// Due to the DropNewMessage strategy the third message delivery will fail.
|
||||||
|
// As a result, no existing messages in the queue will be dropped (evicted).
|
||||||
|
Assert.AreEqual(firstMessageTask.Status, TaskStatus.WaitingForActivation);
|
||||||
|
Assert.AreEqual(secondMessageTask.Status, TaskStatus.WaitingForActivation);
|
||||||
|
Assert.AreEqual(thirdMessageTask.Status, TaskStatus.Faulted);
|
||||||
|
Assert.IsTrue(thirdMessageTask.Exception?.InnerException is MqttPendingMessagesOverflowException);
|
||||||
|
|
||||||
|
Assert.IsFalse(firstMessageEvicted);
|
||||||
|
Assert.IsFalse(secondMessageEvicted);
|
||||||
|
Assert.IsFalse(thirdMessageEvicted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task Deliver_Application_Message_At_Session_Level_QueueOverflow_DropOldestQueuedMessageStrategy()
|
||||||
|
{
|
||||||
|
using var testEnvironment = CreateTestEnvironment(trackUnobservedTaskException: false);
|
||||||
|
|
||||||
|
var server = await testEnvironment.StartServer(
|
||||||
|
builder => builder
|
||||||
|
.WithMaxPendingMessagesPerClient(1)
|
||||||
|
.WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage));
|
||||||
|
|
||||||
|
var receiver = await testEnvironment.ConnectClient();
|
||||||
|
|
||||||
|
var firstMessageOutboundPacketInterceptedTcs = new TaskCompletionSource();
|
||||||
|
server.InterceptingOutboundPacketAsync += async args =>
|
||||||
|
{
|
||||||
|
// - The first message is dequeued normally and calls this delay
|
||||||
|
// - The second message fills the outbound queue
|
||||||
|
// - The third message overflows the outbound queue
|
||||||
|
if (args.Packet is MqttPublishPacket)
|
||||||
|
{
|
||||||
|
firstMessageOutboundPacketInterceptedTcs.SetResult();
|
||||||
|
await Task.Delay(TimeSpan.FromDays(1), args.CancellationToken);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var firstMessageEvicted = false;
|
||||||
|
var secondMessageEvicted = false;
|
||||||
|
var thirdMessageEvicted = false;
|
||||||
|
|
||||||
|
server.QueuedApplicationMessageOverwrittenAsync += args =>
|
||||||
|
{
|
||||||
|
if (args.Packet is not MqttPublishPacket publishPacket)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (publishPacket.Topic)
|
||||||
|
{
|
||||||
|
case "InjectedOne":
|
||||||
|
firstMessageEvicted = true;
|
||||||
|
break;
|
||||||
|
case "InjectedTwo":
|
||||||
|
secondMessageEvicted = true;
|
||||||
|
break;
|
||||||
|
case "InjectedThree":
|
||||||
|
thirdMessageEvicted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
var status = await server.GetSessionsAsync();
|
||||||
|
var clientStatus = status[0];
|
||||||
|
await receiver.SubscribeAsync("#");
|
||||||
|
|
||||||
|
var firstMessageTask = Task.Run(
|
||||||
|
() => clientStatus.DeliverApplicationMessageAsync(
|
||||||
|
new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build()));
|
||||||
|
await LongTestDelay();
|
||||||
|
await firstMessageOutboundPacketInterceptedTcs.Task;
|
||||||
|
|
||||||
|
var secondMessageTask = Task.Run(
|
||||||
|
() => clientStatus.DeliverApplicationMessageAsync(
|
||||||
|
new MqttApplicationMessageBuilder().WithTopic("InjectedTwo").Build()));
|
||||||
|
await LongTestDelay();
|
||||||
|
|
||||||
|
var thirdMessageTask = Task.Run(
|
||||||
|
() => clientStatus.DeliverApplicationMessageAsync(
|
||||||
|
new MqttApplicationMessageBuilder().WithTopic("InjectedThree").Build()));
|
||||||
|
await LongTestDelay();
|
||||||
|
|
||||||
|
Task.WaitAny(firstMessageTask, secondMessageTask, thirdMessageTask);
|
||||||
|
|
||||||
|
// Due to the DropOldestQueuedMessage strategy, the second message delivery will fail
|
||||||
|
// to make room for the third one.
|
||||||
|
Assert.AreEqual(firstMessageTask.Status, TaskStatus.WaitingForActivation);
|
||||||
|
Assert.AreEqual(secondMessageTask.Status, TaskStatus.Faulted);
|
||||||
|
Assert.IsTrue(secondMessageTask.Exception?.InnerException is MqttPendingMessagesOverflowException);
|
||||||
|
Assert.AreEqual(thirdMessageTask.Status, TaskStatus.WaitingForActivation);
|
||||||
|
|
||||||
|
Assert.IsFalse(firstMessageEvicted);
|
||||||
|
Assert.IsTrue(secondMessageEvicted);
|
||||||
|
Assert.IsFalse(thirdMessageEvicted);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task Inject_ApplicationMessage_At_Server_Level()
|
public async Task Inject_ApplicationMessage_At_Server_Level()
|
||||||
{
|
{
|
||||||
using (var testEnvironment = CreateTestEnvironment())
|
using var testEnvironment = CreateTestEnvironment();
|
||||||
{
|
|
||||||
var server = await testEnvironment.StartServer();
|
|
||||||
|
|
||||||
var receiver = await testEnvironment.ConnectClient();
|
var server = await testEnvironment.StartServer();
|
||||||
|
|
||||||
var messageReceivedHandler = testEnvironment.CreateApplicationMessageHandler(receiver);
|
var receiver = await testEnvironment.ConnectClient();
|
||||||
|
|
||||||
await receiver.SubscribeAsync("#");
|
var messageReceivedHandler = testEnvironment.CreateApplicationMessageHandler(receiver);
|
||||||
|
|
||||||
var injectedApplicationMessage = new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build();
|
await receiver.SubscribeAsync("#");
|
||||||
|
|
||||||
await server.InjectApplicationMessage(new InjectedMqttApplicationMessage(injectedApplicationMessage));
|
var injectedApplicationMessage = new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build();
|
||||||
|
|
||||||
await LongTestDelay();
|
await server.InjectApplicationMessage(new InjectedMqttApplicationMessage(injectedApplicationMessage));
|
||||||
|
|
||||||
Assert.AreEqual(1, messageReceivedHandler.ReceivedEventArgs.Count);
|
await LongTestDelay();
|
||||||
Assert.AreEqual("InjectedOne", messageReceivedHandler.ReceivedEventArgs[0].ApplicationMessage.Topic);
|
|
||||||
}
|
Assert.AreEqual(1, messageReceivedHandler.ReceivedEventArgs.Count);
|
||||||
|
Assert.AreEqual("InjectedOne", messageReceivedHandler.ReceivedEventArgs[0].ApplicationMessage.Topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task Intercept_Injected_Application_Message()
|
public async Task Intercept_Injected_Application_Message()
|
||||||
{
|
{
|
||||||
using (var testEnvironment = CreateTestEnvironment())
|
using var testEnvironment = CreateTestEnvironment();
|
||||||
|
|
||||||
|
var server = await testEnvironment.StartServer();
|
||||||
|
|
||||||
|
MqttApplicationMessage interceptedMessage = null;
|
||||||
|
server.InterceptingPublishAsync += eventArgs =>
|
||||||
{
|
{
|
||||||
var server = await testEnvironment.StartServer();
|
interceptedMessage = eventArgs.ApplicationMessage;
|
||||||
|
return CompletedTask.Instance;
|
||||||
|
};
|
||||||
|
|
||||||
MqttApplicationMessage interceptedMessage = null;
|
var injectedApplicationMessage = new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build();
|
||||||
server.InterceptingPublishAsync += eventArgs =>
|
await server.InjectApplicationMessage(new InjectedMqttApplicationMessage(injectedApplicationMessage));
|
||||||
{
|
|
||||||
interceptedMessage = eventArgs.ApplicationMessage;
|
|
||||||
return CompletedTask.Instance;
|
|
||||||
};
|
|
||||||
|
|
||||||
var injectedApplicationMessage = new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build();
|
await LongTestDelay();
|
||||||
await server.InjectApplicationMessage(new InjectedMqttApplicationMessage(injectedApplicationMessage));
|
|
||||||
|
|
||||||
await LongTestDelay();
|
Assert.IsNotNull(interceptedMessage);
|
||||||
|
|
||||||
Assert.IsNotNull(interceptedMessage);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
public class InjectMqttApplicationMessageResult
|
||||||
|
{
|
||||||
|
public ushort PacketIdentifier { get; init; }
|
||||||
|
}
|
||||||
@@ -141,14 +141,6 @@ namespace MQTTnet.Internal
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int ItemsCount(MqttPacketBusPartition partition)
|
|
||||||
{
|
|
||||||
lock (_syncRoot)
|
|
||||||
{
|
|
||||||
return _partitions[(int)partition].Count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int PartitionItemsCount(MqttPacketBusPartition partition)
|
public int PartitionItemsCount(MqttPacketBusPartition partition)
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ namespace MQTTnet.Internal
|
|||||||
{
|
{
|
||||||
public sealed class MqttPacketBusItem
|
public sealed class MqttPacketBusItem
|
||||||
{
|
{
|
||||||
readonly AsyncTaskCompletionSource<bool> _promise = new AsyncTaskCompletionSource<bool>();
|
readonly AsyncTaskCompletionSource<MqttPacket> _promise = new AsyncTaskCompletionSource<MqttPacket>();
|
||||||
|
|
||||||
public MqttPacketBusItem(MqttPacket packet)
|
public MqttPacketBusItem(MqttPacket packet)
|
||||||
{
|
{
|
||||||
Packet = packet ?? throw new ArgumentNullException(nameof(packet));
|
Packet = packet ?? throw new ArgumentNullException(nameof(packet));
|
||||||
@@ -28,7 +28,7 @@ namespace MQTTnet.Internal
|
|||||||
|
|
||||||
public void Complete()
|
public void Complete()
|
||||||
{
|
{
|
||||||
_promise.TrySetResult(true);
|
_promise.TrySetResult(Packet);
|
||||||
Completed?.Invoke(this, EventArgs.Empty);
|
Completed?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ namespace MQTTnet.Internal
|
|||||||
_promise.TrySetException(exception);
|
_promise.TrySetException(exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WaitAsync()
|
public Task<MqttPacket> WaitAsync()
|
||||||
{
|
{
|
||||||
return _promise.Task;
|
return _promise.Task;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
* Removal of Managed Client **(BREAKING CHANGE)**
|
* Removal of Managed Client **(BREAKING CHANGE)**
|
||||||
* Fixed missing release notes in nuget packages.
|
* Fixed missing release notes in nuget packages.
|
||||||
|
|
||||||
|
|
||||||
* Client: MQTT 5.0.0 is now the default version when connecting with a server **(BREAKING CHANGE)**
|
* Client: MQTT 5.0.0 is now the default version when connecting with a server **(BREAKING CHANGE)**
|
||||||
* Client: Fixed enhanced authentication.
|
* Client: Fixed enhanced authentication.
|
||||||
* Client: Exposed WebSocket compression options in MQTT client options (thanks to @victornor, #2127)
|
* Client: Exposed WebSocket compression options in MQTT client options (thanks to @victornor, #2127)
|
||||||
@@ -19,3 +20,4 @@
|
|||||||
* Server: Set SSL version to "None" which will let the OS choose the version **(BREAKING CHANGE)**
|
* Server: Set SSL version to "None" which will let the OS choose the version **(BREAKING CHANGE)**
|
||||||
* Server: Added API for getting a single session (thanks to @AntonSmolkov, #2131)
|
* Server: Added API for getting a single session (thanks to @AntonSmolkov, #2131)
|
||||||
* Server: Fixed "TryPrivate" (Mosquitto feature) handling (thanks to @victornor, #2125) **(BREAKING CHANGE)**
|
* Server: Fixed "TryPrivate" (Mosquitto feature) handling (thanks to @victornor, #2125) **(BREAKING CHANGE)**
|
||||||
|
* Server: Fixed dead lock when awaiting a packet transmission but the packet gets dropped due to quotas (#2117, thanks to @AntonSmolkov)
|
||||||
|
|||||||
Reference in New Issue
Block a user