Обнаружение мертвых соединений с помощью SignalR

#c# #asp.net #.net #signalr

#c# #asp.net #.net #signalr

Вопрос:

Версия SignalR: SignalR 2.4.1

Версия .Net Framework: 4.8 (я не использую .Net Core)

Транспорт SignalR: websockets

Я разрабатываю фоновую службу для SignalR (PresenceMonitor), где мне нужно определить, является ли соединение с определенным идентификатором клиента активным или нет. Я использую следующий код для монитора присутствия, чтобы начать с того, чего я хочу достичь:

 using System;
using System.Data.Entity.SqlServer;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.AspNet.SignalR.Transports;

namespace UserPresence
{
    /// <summary>
    /// This class keeps track of connections that the <see cref="UserTrackingHub"/>
    /// has seen. It uses a time based system to verify if connections are *actually* still online.
    /// Using this class combined with the connection events SignalR raises will ensure
    /// that your database will always be in sync with what SignalR is seeing.
    /// </summary>
    public class PresenceMonitor
    {
        private readonly ITransportHeartbeat _heartbeat;
        private Timer _timer;

        // How often we plan to check if the connections in our store are valid
        private readonly TimeSpan _presenceCheckInterval = TimeSpan.FromSeconds(10);

        // How many periods need pass without an update to consider a connection invalid
        private const int periodsBeforeConsideringZombie = 3;

        // The number of seconds that have to pass to consider a connection invalid.
        private readonly int _zombieThreshold;

        public PresenceMonitor(ITransportHeartbeat heartbeat)
        {
            _heartbeat = heartbeat;
            _zombieThreshold = (int)_presenceCheckInterval.TotalSeconds * periodsBeforeConsideringZombie;
        }

        public void StartMonitoring()
        {
            if (_timer == null)
            {
                _timer = new Timer(_ =>
                {
                    try
                    {
                        Check();
                    }
                    catch (Exception ex)
                    {
                        // Don't throw on background threads, it'll kill the entire process
                        Trace.TraceError(ex.Message);
                    }
                }, 
                null, 
                TimeSpan.Zero, 
                _presenceCheckInterval);
            }
        }

        private void Check()
        {
            using (var db = new UserContext())
            {
                // Get all connections on this node and update the activity
                foreach (var trackedConnection in _heartbeat.GetConnections())
                {
                    if (!trackedConnection.IsAlive)
                    {
                        continue;
                    }

                    Connection connection = db.Connections.Find(trackedConnection.ConnectionId);

                    // Update the client's last activity
                    if (connection != null)
                    {
                        connection.LastActivity = DateTimeOffset.UtcNow;
                    }
                    else
                    {
                        // We have a connection that isn't tracked in our DB!
                        // This should *NEVER* happen
                        // Debugger.Launch();
                    }
                }

                // Now check all db connections to see if there's any zombies

                // Remove all connections that haven't been updated based on our threshold
                var zombies = db.Connections.Where(c =>
                    SqlFunctions.DateDiff("ss", c.LastActivity, DateTimeOffset.UtcNow) >= _zombieThreshold);

                // We're doing ToList() since there's no MARS support on azure
                foreach (var connection in zombies.ToList())
                {
                    db.Connections.Remove(connection);
                }

                db.SaveChanges();
            }
        }
    }
} 
 

Проблема, с которой я сталкиваюсь, находится здесь:

  // Get all connections on this node and update the activity
                foreach (var trackedConnection in _heartbeat.GetConnections())
                {
 

Сканирование всех соединений при большом количестве соединений сильно влияет на производительность моего приложения и приводит к большим скачкам процессора.

В моей базе данных у меня уже есть сопоставление идентификаторов соединений для каждого пользователя. Исходя из этого, у меня уже есть поле в моем кэше для каждого пользователя, независимо от того, имеет ли этот пользователь какое-либо соединение в БД или нет. Эти сопоставления уже находятся в кэше. Я бы просканировал каждое из этих сопоставлений и проверил, является ли соединение (идентификатор соединения) для этого конкретного пользователя живым или нет. Я попытался найти интерфейс ITransportHeartbeat для того же, но, к сожалению, этот интерфейс дает нам только эти четыре метода:

 //
// Summary:
//     Manages tracking the state of connections.
public interface ITransportHeartbeat
{
    //
    // Summary:
    //     Adds a new connection to the list of tracked connections.
    //
    // Parameters:
    //   connection:
    //     The connection to be added.
    //
    // Returns:
    //     The connection it replaced, if any.
    ITrackingConnection AddOrUpdateConnection(ITrackingConnection connection);
    //
    // Summary:
    //     Gets a list of connections being tracked.
    //
    // Returns:
    //     A list of connections.
    IList<ITrackingConnection> GetConnections();
    //
    // Summary:
    //     Marks an existing connection as active.
    //
    // Parameters:
    //   connection:
    //     The connection to mark.
    void MarkConnection(ITrackingConnection connection);
    //
    // Summary:
    //     Removes a connection from the list of tracked connections.
    //
    // Parameters:
    //   connection:
    //     The connection to remove.
    void RemoveConnection(ITrackingConnection connection);
} 
 

Нет метода, с помощью которого я мог бы получить состояние соединения по идентификатору соединения. Есть ли какой-либо способ, с помощью которого я могу получить конкретную информацию о соединении, не сканируя все соединения. Я знаю о традиционном способе получения того, что могло бы использовать это: _heartbeat.getConnections().Выберите(b => b.connectionId). Но этот код также будет сканировать все соединения.

Я также знаю о событии OnDisconnected, которое мы могли бы использовать на самом хабе, но OnDisconnected даже не гарантирует, что оно всегда срабатывает (браузер может закрыться, интернет отключится, перезапуск сайта).

Есть ли какой-либо код, который я мог бы подключить к самому моему хабу, чтобы определить пинг, выполняемый API Heartbeat? Я мог бы сохранить последние пинги для каждого соединения (своего рода денормализовать способ определения последнего пинга) и может определить, является ли это соединение мертвым или нет?

SignalR для .Net Core имеет что-то вроде этого:

 var heartbeat = Context.Features.Get<IConnectionHeartbeatFeature>();
heartbeat.OnHeartBeat(MyAction,  
 

но я ищу аналогичную функцию, подобную в SignalR для .NET Framework.

Комментарии:

1. У вас есть несколько вопросов, что затрудняет дать краткий ответ. Как бы вы ожидали выбрать конкретное соединение, не просматривая их все, чтобы определить, какое из них соответствует критериям выбора? И нет, в более старом SignalR нет способа прослушивать сердцебиения, по крайней мере, насколько я знаю.

2. Моя основная цель — проверить, является ли данный connectionid активным или нет. Я не хочу сканировать все соединения.

3. Насколько я знаю — любое соединение, которое фреймворк обнаружит как мертвое, будет автоматически удалено из концентратора. Это, конечно, может занять время, и если вы хотите, чтобы это было быстрее, вам, возможно, придется внедрить свою собственную систему, которая, конечно, должна будет просматривать каждое соединение по отдельности (асинхронное или нет) — или проверить, можно ли изменить какие-либо настройки, чтобы уменьшить таймер сердцебиения.

4. Я хочу использовать этот API, чтобы удалить сопоставления из моей базы данных для мертвых клиентов.

5. Тогда OnDisconnected это ваш единственный выбор, и он всегда должен срабатывать независимо от причины отключения клиента.