mirror of
https://github.com/dotnet/MQTTnet.git
synced 2026-10-30 18:56:30 +08:00
When a publish, subscribe or unsubscribe request was cancelled or timed out client-side, the corresponding awaitable was removed from the packet dispatcher. A subsequent PUBACK / PUBCOMP / SUBACK / UNSUBACK arriving from the broker then fell through to the default case and raised an MqttProtocolViolationException, which forced a disconnect. Under high publish load (issue #2079) and from multiple threads (issue #2078) this manifested as sporadic connection drops. Match these acknowledgement packets explicitly and log a warning when no awaiter is registered, instead of treating it as a protocol violation. The connection is now preserved. Also documents the thread-safety guarantees of IMqttClient: publish, subscribe, unsubscribe and ping operations are safe to invoke concurrently. Connect / disconnect must still be coordinated. Co-authored-by: meisa <meisa@MAFDEL> Co-authored-by: christian <6939810+chkr1011@users.noreply.github.com>
This commit is contained in:
co-authored by
meisa
christian
parent
745d1d25fe
commit
c1b46b0ae7
@@ -870,4 +870,109 @@ public sealed class MqttClient_Tests : BaseTestClass
|
||||
Assert.IsTrue(client1.TryPingAsync().GetAwaiter().GetResult());
|
||||
Assert.IsFalse(disconnectedFired);
|
||||
}
|
||||
|
||||
// Regression test for issue #2079.
|
||||
// A late PUBACK (one whose matching publish request was already cancelled or timed
|
||||
// out client-side) must NOT raise a protocol violation and must NOT disconnect the
|
||||
// client. Before the fix, the client received the late PUBACK, failed to find a
|
||||
// waiter and threw a MqttProtocolViolationException, tearing down the connection.
|
||||
[TestMethod]
|
||||
[DataRow(MqttQualityOfServiceLevel.AtLeastOnce)]
|
||||
[DataRow(MqttQualityOfServiceLevel.ExactlyOnce)]
|
||||
public async Task Cancelled_Publish_Does_Not_Disconnect_Client(MqttQualityOfServiceLevel qos)
|
||||
{
|
||||
using var testEnvironment = new TestEnvironment(TestContext);
|
||||
var server = await testEnvironment.StartServer();
|
||||
|
||||
// Delay the broker's acknowledgement long enough for the publish caller to give
|
||||
// up. When the caller's cancellation token fires the awaitable is removed; the
|
||||
// (slightly later) PUBACK / PUBREC then arrives without a matching waiter.
|
||||
server.InterceptingPublishAsync += async e =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), CancellationToken.None);
|
||||
};
|
||||
|
||||
var client = await testEnvironment.ConnectClient();
|
||||
|
||||
var disconnected = false;
|
||||
client.DisconnectedAsync += _ =>
|
||||
{
|
||||
disconnected = true;
|
||||
return CompletedTask.Instance;
|
||||
};
|
||||
|
||||
var message = new MqttApplicationMessageBuilder().WithTopic("late-ack").WithQualityOfServiceLevel(qos).Build();
|
||||
|
||||
using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(250)))
|
||||
{
|
||||
await Assert.ThrowsExactlyAsync<MqttCommunicationTimedOutException>(() => client.PublishAsync(message, cts.Token));
|
||||
}
|
||||
|
||||
// Wait long enough for the delayed broker acknowledgement to come back.
|
||||
await Task.Delay(TimeSpan.FromSeconds(3));
|
||||
|
||||
Assert.IsFalse(disconnected, "Client must not disconnect when a late acknowledgement arrives.");
|
||||
Assert.IsTrue(client.IsConnected, "Client must still be connected after a late acknowledgement.");
|
||||
|
||||
// The connection must remain usable for further publishes.
|
||||
var followUp = new MqttApplicationMessageBuilder().WithTopic("late-ack").WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce).Build();
|
||||
await client.PublishAsync(followUp);
|
||||
}
|
||||
|
||||
// Regression test for issue #2078.
|
||||
// The MqttClient must support concurrent publishes from multiple threads without
|
||||
// dropping messages, mismatching acknowledgements or disconnecting the client.
|
||||
[TestMethod]
|
||||
[DataRow(MqttQualityOfServiceLevel.AtMostOnce)]
|
||||
[DataRow(MqttQualityOfServiceLevel.AtLeastOnce)]
|
||||
[DataRow(MqttQualityOfServiceLevel.ExactlyOnce)]
|
||||
public async Task Concurrent_Publish_From_Multiple_Threads(MqttQualityOfServiceLevel qos)
|
||||
{
|
||||
const int Threads = 8;
|
||||
const int MessagesPerThread = 250;
|
||||
|
||||
using var testEnvironment = new TestEnvironment(TestContext);
|
||||
await testEnvironment.StartServer();
|
||||
|
||||
var publisher = await testEnvironment.ConnectClient();
|
||||
|
||||
var subscriber = await testEnvironment.ConnectClient();
|
||||
var receivedCount = 0;
|
||||
subscriber.ApplicationMessageReceivedAsync += _ =>
|
||||
{
|
||||
Interlocked.Increment(ref receivedCount);
|
||||
return CompletedTask.Instance;
|
||||
};
|
||||
await subscriber.SubscribeAsync("concurrent/#", qos);
|
||||
|
||||
var disconnected = false;
|
||||
publisher.DisconnectedAsync += _ =>
|
||||
{
|
||||
disconnected = true;
|
||||
return CompletedTask.Instance;
|
||||
};
|
||||
|
||||
var tasks = Enumerable.Range(0, Threads).Select(threadIndex => Task.Run(async () =>
|
||||
{
|
||||
for (var i = 0; i < MessagesPerThread; i++)
|
||||
{
|
||||
var message = new MqttApplicationMessageBuilder()
|
||||
.WithTopic($"concurrent/{threadIndex}/{i}")
|
||||
.WithPayload(BitConverter.GetBytes(i))
|
||||
.WithQualityOfServiceLevel(qos)
|
||||
.Build();
|
||||
|
||||
await publisher.PublishAsync(message);
|
||||
}
|
||||
})).ToArray();
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
// Allow the broker to deliver any in-flight messages to the subscriber.
|
||||
SpinWait.SpinUntil(() => receivedCount >= Threads * MessagesPerThread, TimeSpan.FromSeconds(30));
|
||||
|
||||
Assert.IsFalse(disconnected, "Publisher must not disconnect during concurrent publishes.");
|
||||
Assert.IsTrue(publisher.IsConnected);
|
||||
Assert.AreEqual(Threads * MessagesPerThread, receivedCount);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user