Refactor session and connection handling in server. Fix QoS level 2 issues.

This commit is contained in:
Christian Kratky
2019-01-27 11:15:32 +01:00
parent ebf0e91ab8
commit 6a2bded181
72 changed files with 2919 additions and 2461 deletions
+133 -100
View File
@@ -1,14 +1,13 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MQTTnet.Adapter;
using MQTTnet.Diagnostics;
using MQTTnet.Internal;
using MQTTnet.Packets;
using MQTTnet.Protocol;
using MQTTnet.Server.Status;
namespace MQTTnet.Server
{
@@ -16,9 +15,9 @@ namespace MQTTnet.Server
{
private readonly BlockingCollection<MqttEnqueuedApplicationMessage> _messageQueue = new BlockingCollection<MqttEnqueuedApplicationMessage>();
private readonly AsyncLock _sessionsLock = new AsyncLock();
private readonly Dictionary<string, MqttClientSession> _sessions = new Dictionary<string, MqttClientSession>();
private readonly ConcurrentDictionary<string, MqttClientConnection> _connections = new ConcurrentDictionary<string, MqttClientConnection>();
private readonly ConcurrentDictionary<string, MqttClientSession> _sessions = new ConcurrentDictionary<string, MqttClientSession>();
private readonly CancellationToken _cancellationToken;
private readonly MqttServerEventDispatcher _eventDispatcher;
@@ -27,8 +26,8 @@ namespace MQTTnet.Server
private readonly IMqttNetChildLogger _logger;
public MqttClientSessionsManager(
IMqttServerOptions options,
MqttRetainedMessagesManager retainedMessagesManager,
IMqttServerOptions options,
MqttRetainedMessagesManager retainedMessagesManager,
CancellationToken cancellationToken,
MqttServerEventDispatcher eventDispatcher,
IMqttNetChildLogger logger)
@@ -45,98 +44,100 @@ namespace MQTTnet.Server
public void Start()
{
Task.Factory.StartNew(() => TryProcessQueuedApplicationMessagesAsync(_cancellationToken), _cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default);
Task.Run(() => TryProcessQueuedApplicationMessagesAsync(_cancellationToken), _cancellationToken);
}
public async Task StopAsync()
{
List<MqttClientSession> sessions;
using (await _sessionsLock.WaitAsync(CancellationToken.None).ConfigureAwait(false))
{
sessions = _sessions.Values.ToList();
}
//using (await _sessionsLock.WaitAsync(CancellationToken.None).ConfigureAwait(false))
foreach (var session in sessions)
foreach (var connection in _connections.Values)
{
await session.StopAsync(MqttClientDisconnectType.Clean).ConfigureAwait(false);
await connection.StopAsync().ConfigureAwait(false);
}
}
public Task HandleConnectionAsync(IMqttChannelAdapter clientAdapter)
{
return HandleConnectionAsync(clientAdapter, _cancellationToken);
// TODO: Check if Task.Run is required.
//return Task.Run(() => HandleConnectionAsync(clientAdapter, _cancellationToken), _cancellationToken);
}
public async Task<IList<IMqttClientSessionStatus>> GetClientStatusAsync()
public Task<IList<IMqttClientStatus>> GetClientStatusAsync()
{
var result = new List<IMqttClientSessionStatus>();
var result = new List<IMqttClientStatus>();
using (await _sessionsLock.WaitAsync(CancellationToken.None).ConfigureAwait(false))
foreach (var connection in _connections.Values)
{
foreach (var session in _sessions.Values)
{
var status = new MqttClientSessionStatus(this, session);
session.FillStatus(status);
var clientStatus = new MqttClientStatus(connection, this);
connection.FillStatus(clientStatus);
var sessionStatus = new MqttSessionStatus(connection.Session, this);
connection.Session.FillStatus(sessionStatus);
clientStatus.Session = sessionStatus;
result.Add(status);
}
result.Add(clientStatus);
}
return result;
return Task.FromResult((IList<IMqttClientStatus>)result);
}
public void EnqueueApplicationMessage(MqttClientSession senderClientSession, MqttApplicationMessage applicationMessage)
public Task<IList<IMqttSessionStatus>> GetSessionStatusAsync()
{
var result = new List<IMqttSessionStatus>();
foreach (var session in _sessions.Values)
{
var sessionStatus = new MqttSessionStatus(session, this);
session.FillStatus(sessionStatus);
result.Add(sessionStatus);
}
return Task.FromResult((IList<IMqttSessionStatus>)result);
}
public void DispatchApplicationMessage(MqttApplicationMessage applicationMessage, MqttClientConnection sender)
{
if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage));
_messageQueue.Add(new MqttEnqueuedApplicationMessage(senderClientSession, applicationMessage), _cancellationToken);
_messageQueue.Add(new MqttEnqueuedApplicationMessage(applicationMessage, sender), _cancellationToken);
}
public async Task SubscribeAsync(string clientId, IEnumerable<TopicFilter> topicFilters)
public Task SubscribeAsync(string clientId, ICollection<TopicFilter> topicFilters)
{
if (clientId == null) throw new ArgumentNullException(nameof(clientId));
if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters));
using (await _sessionsLock.WaitAsync(CancellationToken.None).ConfigureAwait(false))
if (!_sessions.TryGetValue(clientId, out var session))
{
if (!_sessions.TryGetValue(clientId, out var session))
{
throw new InvalidOperationException($"Client session '{clientId}' is unknown.");
}
await session.SubscribeAsync(topicFilters).ConfigureAwait(false);
throw new InvalidOperationException($"Client session '{clientId}' is unknown.");
}
return session.SubscribeAsync(topicFilters, _retainedMessagesManager);
}
public async Task UnsubscribeAsync(string clientId, IEnumerable<string> topicFilters)
public Task UnsubscribeAsync(string clientId, IEnumerable<string> topicFilters)
{
if (clientId == null) throw new ArgumentNullException(nameof(clientId));
if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters));
using (await _sessionsLock.WaitAsync(CancellationToken.None).ConfigureAwait(false))
if (!_sessions.TryGetValue(clientId, out var session))
{
if (!_sessions.TryGetValue(clientId, out var session))
{
throw new InvalidOperationException($"Client session '{clientId}' is unknown.");
}
await session.UnsubscribeAsync(topicFilters).ConfigureAwait(false);
throw new InvalidOperationException($"Client session '{clientId}' is unknown.");
}
return session.UnsubscribeAsync(topicFilters);
}
public async Task DeleteSessionAsync(string clientId)
{
using (await _sessionsLock.WaitAsync(CancellationToken.None).ConfigureAwait(false))
if (_connections.TryGetValue(clientId, out var connection))
{
if (_sessions.TryGetValue(clientId, out var session))
{
session.Dispose();
}
await connection.StopAsync();
}
_sessions.Remove(clientId);
if (_sessions.TryRemove(clientId, out var session))
{
}
_logger.Verbose("Session for client '{0}' deleted.", clientId);
@@ -179,7 +180,7 @@ namespace MQTTnet.Server
{
if (interceptorContext.CloseConnection)
{
await enqueuedApplicationMessage.Sender.StopAsync(MqttClientDisconnectType.NotClean).ConfigureAwait(false);
await enqueuedApplicationMessage.Sender.StopAsync().ConfigureAwait(false);
}
if (interceptorContext.ApplicationMessage == null || !interceptorContext.AcceptPublish)
@@ -197,15 +198,12 @@ namespace MQTTnet.Server
await _retainedMessagesManager.HandleMessageAsync(sender?.ClientId, applicationMessage).ConfigureAwait(false);
}
using (await _sessionsLock.WaitAsync(CancellationToken.None).ConfigureAwait(false))
foreach (var clientSession in _sessions.Values)
{
foreach (var clientSession in _sessions.Values)
{
await clientSession.EnqueueApplicationMessageAsync(
enqueuedApplicationMessage.Sender,
enqueuedApplicationMessage.ApplicationMessage,
false).ConfigureAwait(false);
}
clientSession.EnqueueApplicationMessage(
enqueuedApplicationMessage.ApplicationMessage,
sender?.ClientId,
false);
}
}
catch (OperationCanceledException)
@@ -219,6 +217,7 @@ namespace MQTTnet.Server
private async Task HandleConnectionAsync(IMqttChannelAdapter channelAdapter, CancellationToken cancellationToken)
{
var disconnectType = MqttClientDisconnectType.NotClean;
var clientId = string.Empty;
try
@@ -232,22 +231,31 @@ namespace MQTTnet.Server
clientId = connectPacket.ClientId;
var connectReturnCode = await ValidateConnectionAsync(connectPacket, channelAdapter).ConfigureAwait(false);
if (connectReturnCode != MqttConnectReturnCode.ConnectionAccepted)
var validatorContext = await ValidateConnectionAsync(connectPacket, channelAdapter).ConfigureAwait(false);
if (validatorContext.ReturnCode != MqttConnectReturnCode.ConnectionAccepted)
{
// TODO: Move to channel adapter data converter.
// Send failure response here without preparing a session. The result for a successful connect
// will be sent from the session itself.
await channelAdapter.SendPacketAsync(
new MqttConnAckPacket
{
ReturnCode = connectReturnCode,
ReturnCode = validatorContext.ReturnCode,
ReasonCode = MqttConnectReasonCode.NotAuthorized
},
_options.DefaultCommunicationTimeout,
cancellationToken).ConfigureAwait(false);
return;
}
var session = await PrepareClientSessionAsync(connectPacket).ConfigureAwait(false);
await session.RunAsync(connectPacket, channelAdapter).ConfigureAwait(false);
var connection = await CreateConnectionAsync(channelAdapter, connectPacket).ConfigureAwait(false);
_eventDispatcher.OnClientConnected(clientId);
disconnectType = await connection.RunAsync().ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -258,26 +266,29 @@ namespace MQTTnet.Server
}
finally
{
_connections.TryRemove(clientId, out _);
////connection?.ReferenceCounter.Decrement();
////if (connection?.ReferenceCounter.HasReferences == true)
////{
//// disconnectType = MqttClientDisconnectType.Takeover;
////}
////else
{
if (!_options.EnablePersistentSessions)
{
await DeleteSessionAsync(clientId).ConfigureAwait(false);
}
}
await TryCleanupChannelAsync(channelAdapter).ConfigureAwait(false);
if (!_options.EnablePersistentSessions)
{
// TODO: Check if the session will be used later.
// Consider reference counter or "Recycle" property
// Or add timer (will be required for MQTTv5 (session life time) "IsActiveProperty".
//öö
//await DeleteSessionAsync(clientId).ConfigureAwait(false);
}
_eventDispatcher.OnClientDisconnected(clientId, disconnectType);
}
}
private async Task<MqttConnectReturnCode> ValidateConnectionAsync(MqttConnectPacket connectPacket, IMqttChannelAdapter clientAdapter)
private async Task<MqttConnectionValidatorContext> ValidateConnectionAsync(MqttConnectPacket connectPacket, IMqttChannelAdapter clientAdapter)
{
if (_options.ConnectionValidator == null)
{
return MqttConnectReturnCode.ConnectionAccepted;
}
var context = new MqttConnectionValidatorContext(
connectPacket.ClientId,
connectPacket.Username,
@@ -285,47 +296,69 @@ namespace MQTTnet.Server
connectPacket.WillMessage,
clientAdapter.Endpoint);
await _options.ConnectionValidator.ValidateConnection(context).ConfigureAwait(false);
return context.ReturnCode;
var connectionValidator = _options.ConnectionValidator;
if (connectionValidator == null)
{
context.ReturnCode = MqttConnectReturnCode.ConnectionAccepted;
return context;
}
await connectionValidator.ValidateConnectionAsync(context).ConfigureAwait(false);
return context;
}
private async Task<MqttClientSession> PrepareClientSessionAsync(MqttConnectPacket connectPacket)
private readonly SemaphoreSlim _createConnectionGate = new SemaphoreSlim(1, 1);
private async Task<MqttClientConnection> CreateConnectionAsync(IMqttChannelAdapter channelAdapter, MqttConnectPacket connectPacket)
{
using (await _sessionsLock.WaitAsync(CancellationToken.None).ConfigureAwait(false))
await _createConnectionGate.WaitAsync(_cancellationToken).ConfigureAwait(false);
try
{
var isSessionPresent = _sessions.TryGetValue(connectPacket.ClientId, out var clientSession);
var isSessionPresent = _sessions.TryGetValue(connectPacket.ClientId, out var session);
var isConnectionPresent = _connections.TryGetValue(connectPacket.ClientId, out var existingConnection);
if (isConnectionPresent)
{
await existingConnection.StopAsync().ConfigureAwait(false);
}
if (isSessionPresent)
{
if (connectPacket.CleanSession)
{
await clientSession.StopAsync(MqttClientDisconnectType.Clean).ConfigureAwait(false);
clientSession.Dispose();
clientSession = null;
_logger.Verbose("Stopped existing session of client '{0}'.", connectPacket.ClientId);
// TODO: Check if required.
//session.Dispose();
session = null;
_logger.Verbose("Deleting existing session of client '{0}'.", connectPacket.ClientId);
}
else
{
await clientSession.StopAsync(MqttClientDisconnectType.Clean).ConfigureAwait(false);
_logger.Verbose("Reusing existing session of client '{0}'.", connectPacket.ClientId);
}
}
if (clientSession == null)
if (session == null)
{
clientSession = new MqttClientSession(connectPacket.ClientId, _options, this, _retainedMessagesManager, _eventDispatcher, _logger);
_sessions[connectPacket.ClientId] = clientSession;
session = new MqttClientSession(connectPacket.ClientId, _eventDispatcher, _options);
_logger.Verbose("Created a new session for client '{0}'.", connectPacket.ClientId);
}
return clientSession;
var connection = new MqttClientConnection(connectPacket, channelAdapter, session, _options, this, _retainedMessagesManager, _logger);
_connections[connection.ClientId] = connection;
_sessions[session.ClientId] = session;
return connection;
}
finally
{
_createConnectionGate.Release();
}
}
private async Task<MqttApplicationMessageInterceptorContext> InterceptApplicationMessageAsync(MqttClientSession sender, MqttApplicationMessage applicationMessage)
private async Task<MqttApplicationMessageInterceptorContext> InterceptApplicationMessageAsync(MqttClientConnection sender, MqttApplicationMessage applicationMessage)
{
var interceptor = _options.ApplicationMessageInterceptor;
if (interceptor == null)