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:
Anton Smolkov
2025-01-02 11:51:46 +01:00
committed by GitHub
co-authored by Anton Smolkov christian
parent e7de2b913a
commit 27771f0146
11 changed files with 522 additions and 70 deletions
@@ -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; }
}
+11 -4
View File
@@ -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;
} }
} }
+4 -3
View File
@@ -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) =>
{ {
+412 -44
View File
@@ -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; }
}
-8
View File
@@ -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)
+4 -4
View File
@@ -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;
} }
+2
View File
@@ -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)