Refactor package object model.

This commit is contained in:
Christian Kratky
2020-10-28 23:11:12 +01:00
parent adce5662eb
commit b7fb0c1002
71 changed files with 761 additions and 582 deletions
@@ -12,6 +12,7 @@ namespace MQTTnet.Server
MqttPendingMessagesOverflowStrategy PendingMessagesOverflowStrategy { get; }
TimeSpan DefaultCommunicationTimeout { get; }
TimeSpan KeepAliveMonitorInterval { get; }
IMqttServerConnectionValidator ConnectionValidator { get; }
IMqttServerSubscriptionInterceptor SubscriptionInterceptor { get; }
+16 -36
View File
@@ -23,7 +23,6 @@ namespace MQTTnet.Server
readonly CancellationTokenSource _cancellationToken = new CancellationTokenSource();
readonly IMqttRetainedMessagesManager _retainedMessagesManager;
readonly MqttClientKeepAliveMonitor _keepAliveMonitor;
readonly MqttClientSessionsManager _sessionsManager;
readonly IMqttNetScopedLogger _logger;
@@ -36,7 +35,6 @@ namespace MQTTnet.Server
readonly DateTime _connectedTimestamp;
volatile Task _packageReceiverTask;
DateTime _lastPacketReceivedTimestamp;
DateTime _lastNonKeepAlivePacketReceivedTimestamp;
long _receivedPacketsCount;
@@ -69,17 +67,19 @@ namespace MQTTnet.Server
if (logger == null) throw new ArgumentNullException(nameof(logger));
_logger = logger.CreateScopedLogger(nameof(MqttClientConnection));
_keepAliveMonitor = new MqttClientKeepAliveMonitor(ConnectPacket.ClientId, () => StopAsync(), logger);
_connectedTimestamp = DateTime.UtcNow;
_lastPacketReceivedTimestamp = _connectedTimestamp;
_lastNonKeepAlivePacketReceivedTimestamp = _lastPacketReceivedTimestamp;
LastPacketReceivedTimestamp = _connectedTimestamp;
_lastNonKeepAlivePacketReceivedTimestamp = LastPacketReceivedTimestamp;
}
public MqttConnectPacket ConnectPacket { get; }
public string ClientId => ConnectPacket.ClientId;
public bool IsReadingPacket => _channelAdapter.IsReadingPacket;
public DateTime LastPacketReceivedTimestamp { get; private set; }
public MqttClientSession Session { get; }
public Task StopAsync(bool isTakeover = false)
@@ -115,7 +115,7 @@ namespace MQTTnet.Server
status.SentPacketsCount = Interlocked.Read(ref _sentPacketsCount);
status.ConnectedTimestamp = _connectedTimestamp;
status.LastPacketReceivedTimestamp = _lastPacketReceivedTimestamp;
status.LastPacketReceivedTimestamp = LastPacketReceivedTimestamp;
status.LastNonKeepAlivePacketReceivedTimestamp = _lastNonKeepAlivePacketReceivedTimestamp;
status.BytesSent = _channelAdapter.BytesSent;
@@ -140,16 +140,10 @@ namespace MQTTnet.Server
{
_logger.Info("Client '{0}': Session started.", ClientId);
_channelAdapter.ReadingPacketStartedCallback = OnAdapterReadingPacketStarted;
_channelAdapter.ReadingPacketCompletedCallback = OnAdapterReadingPacketCompleted;
Session.WillMessage = ConnectPacket.WillMessage;
Task.Run(() => SendPendingPacketsAsync(_cancellationToken.Token), _cancellationToken.Token).Forget(_logger);
// TODO: Change to single thread in SessionManager. Or use SessionManager and stats from KeepAliveMonitor.
_keepAliveMonitor.Start(ConnectPacket.KeepAlivePeriod, _cancellationToken.Token);
await SendAsync(_channelAdapter.PacketFormatterAdapter.DataConverter.CreateConnAckPacket(_connectionValidatorContext)).ConfigureAwait(false);
Session.IsCleanSession = false;
@@ -164,15 +158,13 @@ namespace MQTTnet.Server
}
Interlocked.Increment(ref _sentPacketsCount);
_lastPacketReceivedTimestamp = DateTime.UtcNow;
LastPacketReceivedTimestamp = DateTime.UtcNow;
if (!(packet is MqttPingReqPacket || packet is MqttPingRespPacket))
{
_lastNonKeepAlivePacketReceivedTimestamp = _lastPacketReceivedTimestamp;
_lastNonKeepAlivePacketReceivedTimestamp = LastPacketReceivedTimestamp;
}
_keepAliveMonitor.PacketReceived();
if (packet is MqttPublishPacket publishPacket)
{
await HandleIncomingPublishPacketAsync(publishPacket).ConfigureAwait(false);
@@ -252,9 +244,6 @@ namespace MQTTnet.Server
_packetDispatcher.Reset();
_channelAdapter.ReadingPacketStartedCallback = null;
_channelAdapter.ReadingPacketCompletedCallback = null;
_packageReceiverTask = null;
if (_isTakeover)
@@ -302,10 +291,10 @@ namespace MQTTnet.Server
async Task HandleIncomingSubscribePacketAsync(MqttSubscribePacket subscribePacket)
{
// TODO: Let the channel adapter create the packet.
var subscribeResult = await Session.SubscriptionsManager.SubscribeAsync(subscribePacket, ConnectPacket).ConfigureAwait(false);
var subAckPacket = _channelAdapter.PacketFormatterAdapter.DataConverter.CreateSubAckPacket(subscribePacket, subscribeResult);
await SendAsync(subscribeResult.ResponsePacket).ConfigureAwait(false);
await SendAsync(subAckPacket).ConfigureAwait(false);
if (subscribeResult.CloseConnection)
{
@@ -318,9 +307,10 @@ namespace MQTTnet.Server
async Task HandleIncomingUnsubscribePacketAsync(MqttUnsubscribePacket unsubscribePacket)
{
// TODO: Let the channel adapter create the packet.
var unsubscribeResult = await Session.SubscriptionsManager.UnsubscribeAsync(unsubscribePacket).ConfigureAwait(false);
await SendAsync(unsubscribeResult).ConfigureAwait(false);
var reasonCodes = await Session.SubscriptionsManager.UnsubscribeAsync(unsubscribePacket).ConfigureAwait(false);
var unsubAckPacket = _channelAdapter.PacketFormatterAdapter.DataConverter.CreateUnsubAckPacket(unsubscribePacket, reasonCodes);
await SendAsync(unsubAckPacket).ConfigureAwait(false);
}
Task HandleIncomingPublishPacketAsync(MqttPublishPacket publishPacket)
@@ -389,7 +379,7 @@ namespace MQTTnet.Server
{
while (!cancellationToken.IsCancellationRequested)
{
queuedApplicationMessage = await Session.ApplicationMessagesQueue.TakeAsync(cancellationToken).ConfigureAwait(false);
queuedApplicationMessage = await Session.ApplicationMessagesQueue.DequeueAsync(cancellationToken).ConfigureAwait(false);
if (queuedApplicationMessage == null)
{
return;
@@ -503,15 +493,5 @@ namespace MQTTnet.Server
Interlocked.Increment(ref _receivedApplicationMessagesCount);
}
}
void OnAdapterReadingPacketCompleted()
{
_keepAliveMonitor?.Resume();
}
void OnAdapterReadingPacketStarted()
{
_keepAliveMonitor?.Pause();
}
}
}
@@ -1,93 +0,0 @@
using MQTTnet.Diagnostics;
using MQTTnet.Internal;
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace MQTTnet.Server
{
public sealed class MqttClientKeepAliveMonitor
{
readonly Stopwatch _lastPacketReceivedTracker = new Stopwatch();
readonly string _clientId;
readonly Func<Task> _keepAliveElapsedCallback;
readonly IMqttNetScopedLogger _logger;
bool _isPaused;
public MqttClientKeepAliveMonitor(string clientId, Func<Task> keepAliveElapsedCallback, IMqttNetLogger logger)
{
_clientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
_keepAliveElapsedCallback = keepAliveElapsedCallback ?? throw new ArgumentNullException(nameof(keepAliveElapsedCallback));
if (logger == null) throw new ArgumentNullException(nameof(logger));
_logger = logger.CreateScopedLogger(nameof(MqttClientKeepAliveMonitor));
}
public void Start(int keepAlivePeriod, CancellationToken cancellationToken)
{
if (keepAlivePeriod == 0)
{
return;
}
Task.Run(() => RunAsync(keepAlivePeriod, cancellationToken), cancellationToken).Forget(_logger);
}
public void Pause()
{
_isPaused = true;
}
public void Resume()
{
_isPaused = false;
}
public void PacketReceived()
{
_lastPacketReceivedTracker.Restart();
}
async Task RunAsync(int keepAlivePeriod, CancellationToken cancellationToken)
{
try
{
_lastPacketReceivedTracker.Restart();
while (!cancellationToken.IsCancellationRequested)
{
// Values described here: [MQTT-3.1.2-24].
// If the client sends 5 sec. the server will allow up to 7.5 seconds.
// If the client sends 1 sec. the server will allow up to 1.5 seconds.
if (!_isPaused && _lastPacketReceivedTracker.Elapsed.TotalSeconds >= keepAlivePeriod * 1.5D)
{
_logger.Warning(null, "Client '{0}': Did not receive any packet or keep alive signal.", _clientId);
await _keepAliveElapsedCallback().ConfigureAwait(false);
return;
}
// The server checks the keep alive timeout every 50 % of the overall keep alive timeout
// because the server allows 1.5 times the keep alive value. This means that a value of 5 allows
// up to 7.5 seconds. With an interval of 2.5 (5 / 2) the 7.5 is also affected. Waiting the whole
// keep alive time will hit at 10 instead of 7.5 (but only one time instead of two times).
await Task.Delay(TimeSpan.FromSeconds(keepAlivePeriod * 0.5D), cancellationToken).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
}
catch (Exception exception)
{
_logger.Error(exception, "Client '{0}': Unhandled exception while checking keep alive timeouts.", _clientId);
}
finally
{
_logger.Verbose("Client '{0}': Stopped checking keep alive timeout.", _clientId);
}
}
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ using System.Threading.Tasks;
namespace MQTTnet.Server
{
public class MqttClientSession
public sealed class MqttClientSession
{
readonly IMqttNetScopedLogger _logger;
@@ -6,11 +6,11 @@ using System.Threading.Tasks;
namespace MQTTnet.Server
{
public class MqttClientSessionApplicationMessagesQueue : Disposable
public sealed class MqttClientSessionApplicationMessagesQueue : IDisposable
{
private readonly AsyncQueue<MqttQueuedApplicationMessage> _messageQueue = new AsyncQueue<MqttQueuedApplicationMessage>();
private readonly IMqttServerOptions _options;
readonly AsyncQueue<MqttQueuedApplicationMessage> _messageQueue = new AsyncQueue<MqttQueuedApplicationMessage>();
readonly IMqttServerOptions _options;
public MqttClientSessionApplicationMessagesQueue(IMqttServerOptions options)
{
@@ -32,22 +32,6 @@ namespace MQTTnet.Server
});
}
public void Clear()
{
_messageQueue.Clear();
}
public async Task<MqttQueuedApplicationMessage> TakeAsync(CancellationToken cancellationToken)
{
var dequeueResult = await _messageQueue.TryDequeueAsync(cancellationToken).ConfigureAwait(false);
if (!dequeueResult.IsSuccess)
{
return null;
}
return dequeueResult.Item;
}
public void Enqueue(MqttQueuedApplicationMessage queuedApplicationMessage)
{
if (queuedApplicationMessage == null) throw new ArgumentNullException(nameof(queuedApplicationMessage));
@@ -71,14 +55,25 @@ namespace MQTTnet.Server
}
}
protected override void Dispose(bool disposing)
public async Task<MqttQueuedApplicationMessage> DequeueAsync(CancellationToken cancellationToken)
{
if (disposing)
var dequeueResult = await _messageQueue.TryDequeueAsync(cancellationToken).ConfigureAwait(false);
if (!dequeueResult.IsSuccess)
{
_messageQueue.Dispose();
return null;
}
base.Dispose(disposing);
return dequeueResult.Item;
}
public void Clear()
{
_messageQueue.Clear();
}
public void Dispose()
{
_messageQueue?.Dispose();
}
}
}
@@ -9,6 +9,7 @@ using MQTTnet.Server.Status;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -63,6 +64,11 @@ namespace MQTTnet.Server
}
}
public List<MqttClientConnection> GetConnections()
{
return _connections.Values.ToList();
}
public Task HandleClientConnectionAsync(IMqttChannelAdapter clientAdapter)
{
if (clientAdapter is null) throw new ArgumentNullException(nameof(clientAdapter));
@@ -1,10 +1,13 @@
using MQTTnet.Packets;
using System.Collections.Generic;
using MQTTnet.Protocol;
namespace MQTTnet.Server
{
public class MqttClientSubscribeResult
public sealed class MqttClientSubscribeResult
{
public MqttSubAckPacket ResponsePacket { get; set; }
public List<MqttSubscribeReturnCode> ReturnCodes { get; } = new List<MqttSubscribeReturnCode>();
public List<MqttSubscribeReasonCode> ReasonCodes { get; } = new List<MqttSubscribeReasonCode>();
public bool CloseConnection { get; set; }
}
@@ -7,12 +7,12 @@ using System.Threading.Tasks;
namespace MQTTnet.Server
{
public class MqttClientSubscriptionsManager
public sealed class MqttClientSubscriptionsManager
{
private readonly Dictionary<string, MqttTopicFilter> _subscriptions = new Dictionary<string, MqttTopicFilter>();
private readonly MqttClientSession _clientSession;
private readonly IMqttServerOptions _serverOptions;
private readonly MqttServerEventDispatcher _eventDispatcher;
readonly Dictionary<string, MqttTopicFilter> _subscriptions = new Dictionary<string, MqttTopicFilter>();
readonly MqttClientSession _clientSession;
readonly IMqttServerOptions _serverOptions;
readonly MqttServerEventDispatcher _eventDispatcher;
public MqttClientSubscriptionsManager(MqttClientSession clientSession, MqttServerEventDispatcher eventDispatcher, IMqttServerOptions serverOptions)
{
@@ -28,15 +28,7 @@ namespace MQTTnet.Server
if (subscribePacket == null) throw new ArgumentNullException(nameof(subscribePacket));
if (connectPacket == null) throw new ArgumentNullException(nameof(connectPacket));
var result = new MqttClientSubscribeResult
{
ResponsePacket = new MqttSubAckPacket
{
PacketIdentifier = subscribePacket.PacketIdentifier
},
CloseConnection = false
};
var result = new MqttClientSubscribeResult();
foreach (var originalTopicFilter in subscribePacket.TopicFilters)
{
@@ -46,13 +38,13 @@ namespace MQTTnet.Server
if (finalTopicFilter == null || string.IsNullOrEmpty(finalTopicFilter.Topic) || !interceptorContext.AcceptSubscription)
{
result.ResponsePacket.ReturnCodes.Add(MqttSubscribeReturnCode.Failure);
result.ResponsePacket.ReasonCodes.Add(MqttSubscribeReasonCode.UnspecifiedError);
result.ReturnCodes.Add(MqttSubscribeReturnCode.Failure);
result.ReasonCodes.Add(MqttSubscribeReasonCode.UnspecifiedError);
}
else
{
result.ResponsePacket.ReturnCodes.Add(ConvertToSubscribeReturnCode(finalTopicFilter.QualityOfServiceLevel));
result.ResponsePacket.ReasonCodes.Add(ConvertToSubscribeReasonCode(finalTopicFilter.QualityOfServiceLevel));
result.ReturnCodes.Add(ConvertToSubscribeReturnCode(finalTopicFilter.QualityOfServiceLevel));
result.ReasonCodes.Add(ConvertToSubscribeReasonCode(finalTopicFilter.QualityOfServiceLevel));
}
if (interceptorContext.CloseConnection)
@@ -98,21 +90,18 @@ namespace MQTTnet.Server
}
}
public async Task<MqttUnsubAckPacket> UnsubscribeAsync(MqttUnsubscribePacket unsubscribePacket)
public async Task<List<MqttUnsubscribeReasonCode>> UnsubscribeAsync(MqttUnsubscribePacket unsubscribePacket)
{
if (unsubscribePacket == null) throw new ArgumentNullException(nameof(unsubscribePacket));
var unsubAckPacket = new MqttUnsubAckPacket
{
PacketIdentifier = unsubscribePacket.PacketIdentifier
};
var reasonCodes = new List<MqttUnsubscribeReasonCode>();
foreach (var topicFilter in unsubscribePacket.TopicFilters)
{
var interceptorContext = await InterceptUnsubscribeAsync(topicFilter).ConfigureAwait(false);
if (!interceptorContext.AcceptUnsubscription)
{
unsubAckPacket.ReasonCodes.Add(MqttUnsubscribeReasonCode.ImplementationSpecificError);
reasonCodes.Add(MqttUnsubscribeReasonCode.ImplementationSpecificError);
continue;
}
@@ -120,11 +109,11 @@ namespace MQTTnet.Server
{
if (_subscriptions.Remove(topicFilter))
{
unsubAckPacket.ReasonCodes.Add(MqttUnsubscribeReasonCode.Success);
reasonCodes.Add(MqttUnsubscribeReasonCode.Success);
}
else
{
unsubAckPacket.ReasonCodes.Add(MqttUnsubscribeReasonCode.NoSubscriptionExisted);
reasonCodes.Add(MqttUnsubscribeReasonCode.NoSubscriptionExisted);
}
}
}
@@ -134,7 +123,7 @@ namespace MQTTnet.Server
await _eventDispatcher.SafeNotifyClientUnsubscribedTopicAsync(_clientSession.ClientId, topicFilter).ConfigureAwait(false);
}
return unsubAckPacket;
return reasonCodes;
}
public async Task UnsubscribeAsync(IEnumerable<string> topicFilters)
@@ -184,7 +173,7 @@ namespace MQTTnet.Server
return CreateSubscriptionResult(qosLevel, qosLevels);
}
private static MqttSubscribeReturnCode ConvertToSubscribeReturnCode(MqttQualityOfServiceLevel qualityOfServiceLevel)
static MqttSubscribeReturnCode ConvertToSubscribeReturnCode(MqttQualityOfServiceLevel qualityOfServiceLevel)
{
switch (qualityOfServiceLevel)
{
@@ -195,7 +184,7 @@ namespace MQTTnet.Server
}
}
private static MqttSubscribeReasonCode ConvertToSubscribeReasonCode(MqttQualityOfServiceLevel qualityOfServiceLevel)
static MqttSubscribeReasonCode ConvertToSubscribeReasonCode(MqttQualityOfServiceLevel qualityOfServiceLevel)
{
switch (qualityOfServiceLevel)
{
@@ -206,7 +195,7 @@ namespace MQTTnet.Server
}
}
private async Task<MqttSubscriptionInterceptorContext> InterceptSubscribeAsync(MqttTopicFilter topicFilter)
async Task<MqttSubscriptionInterceptorContext> InterceptSubscribeAsync(MqttTopicFilter topicFilter)
{
var context = new MqttSubscriptionInterceptorContext(_clientSession.ClientId, topicFilter, _clientSession.Items);
if (_serverOptions.SubscriptionInterceptor != null)
@@ -217,7 +206,7 @@ namespace MQTTnet.Server
return context;
}
private async Task<MqttUnsubscriptionInterceptorContext> InterceptUnsubscribeAsync(string topicFilter)
async Task<MqttUnsubscriptionInterceptorContext> InterceptUnsubscribeAsync(string topicFilter)
{
var context = new MqttUnsubscriptionInterceptorContext(_clientSession.ClientId, topicFilter, _clientSession.Items);
if (_serverOptions.UnsubscriptionInterceptor != null)
@@ -228,7 +217,7 @@ namespace MQTTnet.Server
return context;
}
private static CheckSubscriptionsResult CreateSubscriptionResult(MqttQualityOfServiceLevel qosLevel, HashSet<MqttQualityOfServiceLevel> subscribedQoSLevels)
static CheckSubscriptionsResult CreateSubscriptionResult(MqttQualityOfServiceLevel qosLevel, HashSet<MqttQualityOfServiceLevel> subscribedQoSLevels)
{
MqttQualityOfServiceLevel effectiveQoS;
if (subscribedQoSLevels.Contains(qosLevel))
@@ -0,0 +1,115 @@
using MQTTnet.Diagnostics;
using MQTTnet.Internal;
using System;
using System.Threading;
using System.Threading.Tasks;
using MQTTnet.Implementations;
namespace MQTTnet.Server
{
public sealed class MqttServerKeepAliveMonitor
{
readonly IMqttServerOptions _options;
readonly MqttClientSessionsManager _sessionsManager;
readonly IMqttNetScopedLogger _logger;
public MqttServerKeepAliveMonitor(IMqttServerOptions 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));
_logger = logger.CreateScopedLogger(nameof(MqttServerKeepAliveMonitor));
}
public void Start(CancellationToken cancellationToken)
{
// The keep alive monitor spawns a real new thread (LongRunning) because it does not
// support async/await. Async etc. is avoided here because the thread will usually check
// the connections every few milliseconds and thus the context changes (due to async) are
// only consuming resources. Also there is just 1 thread for the entire server which is fine at all!
Task.Factory.StartNew(_ => DoWork(cancellationToken), cancellationToken, TaskCreationOptions.LongRunning).Forget(_logger);
}
void DoWork(CancellationToken cancellationToken)
{
try
{
_logger.Info("Starting keep alive monitor.");
while (!cancellationToken.IsCancellationRequested)
{
TryMaintainConnections();
PlatformAbstractionLayer.Sleep(_options.KeepAliveMonitorInterval);
}
}
catch (OperationCanceledException)
{
}
catch (Exception exception)
{
_logger.Error(exception, "Unhandled exception while checking keep alive timeouts.");
}
finally
{
_logger.Verbose("Stopped checking keep alive timeout.");
}
}
void TryMaintainConnections()
{
var now = DateTime.UtcNow;
foreach (var connection in _sessionsManager.GetConnections())
{
TryMaintainConnection(connection, now);
}
}
void TryMaintainConnection(MqttClientConnection connection, DateTime now)
{
try
{
//if (connection.IsStopped)
//{
// // The connection is already dead so there is no need to check it.
// return;
//}
if (connection.ConnectPacket.KeepAlivePeriod == 0)
{
// The keep alive feature is not used by the current connection.
return;
}
if (connection.IsReadingPacket)
{
// The connection is currently reading a (large) packet. So it is obviously
// doing something and thus "connected".
return;
}
// Values described here: [MQTT-3.1.2-24].
// If the client sends 5 sec. the server will allow up to 7.5 seconds.
// If the client sends 1 sec. the server will allow up to 1.5 seconds.
var maxDurationWithoutPacket = connection.ConnectPacket.KeepAlivePeriod * 1.5D;
var secondsWithoutPackage = (now - connection.LastPacketReceivedTimestamp).TotalSeconds;
if (secondsWithoutPackage < maxDurationWithoutPacket)
{
// A packet was received before the timeout is affected.
return;
}
_logger.Warning(null, "Client '{0}': Did not receive any packet or keep alive signal.", connection.ClientId);
// Execute the disconnection in background so that the keep alive monitor can continue
// with checking other connections.
Task.Run(() => connection.StopAsync());
}
catch (Exception exception)
{
_logger.Error(exception, "Client {0}: Unhandled exception while checking keep alive timeouts.", connection.ClientId);
}
}
}
}
@@ -18,6 +18,8 @@ namespace MQTTnet.Server
public TimeSpan DefaultCommunicationTimeout { get; set; } = TimeSpan.FromSeconds(15);
public TimeSpan KeepAliveMonitorInterval { get; set; } = TimeSpan.FromMilliseconds(500);
public IMqttServerConnectionValidator ConnectionValidator { get; set; }
public IMqttServerApplicationMessageInterceptor ApplicationMessageInterceptor { get; set; }