public RedisHubLifetimeManager(ILogger <RedisHubLifetimeManager <THub> > logger,
                                       IOptions <RedisOptions> options,
                                       IHubProtocolResolver hubProtocolResolver)
        {
            _logger     = logger;
            _options    = options.Value;
            _ackHandler = new AckHandler();
            _channels   = new RedisChannels(typeof(THub).FullName);
            _protocol   = new RedisProtocol(hubProtocolResolver.AllProtocols);

            RedisLog.ConnectingToEndpoints(_logger, options.Value.Configuration.EndPoints, _serverName);
            _ = EnsureRedisServerConnection();
        }
        public RedisHubLifetimeManager(ILogger <RedisHubLifetimeManager <THub> > logger,
                                       IOptions <RedisOptions> options)
        {
            _logger     = logger;
            _options    = options.Value;
            _ackHandler = new AckHandler();

            var writer = new LoggerTextWriter(logger);

            _logger.ConnectingToEndpoints(options.Value.Options.EndPoints);
            _redisServerConnection = _options.Connect(writer);

            _redisServerConnection.ConnectionRestored += (_, e) =>
            {
                // We use the subscription connection type
                // Ignore messages from the interactive connection (avoids duplicates)
                if (e.ConnectionType == ConnectionType.Interactive)
                {
                    return;
                }

                _logger.ConnectionRestored();
            };

            _redisServerConnection.ConnectionFailed += (_, e) =>
            {
                // We use the subscription connection type
                // Ignore messages from the interactive connection (avoids duplicates)
                if (e.ConnectionType == ConnectionType.Interactive)
                {
                    return;
                }

                _logger.ConnectionFailed(e.Exception);
            };

            if (_redisServerConnection.IsConnected)
            {
                _logger.Connected();
            }
            else
            {
                _logger.NotConnected();
            }
            _bus = _redisServerConnection.GetSubscriber();

            SubscribeToHub();
            SubscribeToAllExcept();
            SubscribeToInternalGroup();
            SubscribeToInternalServerName();
        }
        public RedisHubLifetimeManager(ILogger <RedisHubLifetimeManager <THub> > logger,
                                       IOptions <RedisOptions> options)
        {
            _logger     = logger;
            _options    = options.Value;
            _ackHandler = new AckHandler();

            var writer = new LoggerTextWriter(logger);

            _logger.LogInformation("Connecting to redis endpoints: {endpoints}", string.Join(", ", options.Value.Options.EndPoints.Select(e => EndPointCollection.ToString(e))));
            _redisServerConnection = _options.Connect(writer);
            if (_redisServerConnection.IsConnected)
            {
                _logger.LogInformation("Connected to redis");
            }
            else
            {
                // TODO: We could support reconnecting, like old SignalR does.
                throw new InvalidOperationException("Connection to redis failed.");
            }
            _bus = _redisServerConnection.GetSubscriber();

            var previousBroadcastTask = Task.CompletedTask;

            var channelName = _channelNamePrefix;

            _logger.LogInformation("Subscribing to channel: {channel}", channelName);
            _bus.Subscribe(channelName, async(c, data) =>
            {
                await previousBroadcastTask;

                _logger.LogTrace("Received message from redis channel {channel}", channelName);

                var message = DeserializeMessage <HubMessage>(data);

                // TODO: This isn't going to work when we allow JsonSerializer customization or add Protobuf
                var tasks = new List <Task>(_connections.Count);

                foreach (var connection in _connections)
                {
                    tasks.Add(WriteAsync(connection, message));
                }

                previousBroadcastTask = Task.WhenAll(tasks);
            });

            var allExceptTask = Task.CompletedTask;

            channelName = _channelNamePrefix + ".AllExcept";
            _logger.LogInformation("Subscribing to channel: {channel}", channelName);
            _bus.Subscribe(channelName, async(c, data) =>
            {
                await allExceptTask;

                _logger.LogTrace("Received message from redis channel {channel}", channelName);

                var message     = DeserializeMessage <RedisExcludeClientsMessage>(data);
                var excludedIds = message.ExcludedIds;

                // TODO: This isn't going to work when we allow JsonSerializer customization or add Protobuf

                var tasks = new List <Task>(_connections.Count);

                foreach (var connection in _connections)
                {
                    if (!excludedIds.Contains(connection.ConnectionId))
                    {
                        tasks.Add(WriteAsync(connection, message));
                    }
                }

                allExceptTask = Task.WhenAll(tasks);
            });

            channelName = _channelNamePrefix + ".internal.group";
            _bus.Subscribe(channelName, async(c, data) =>
            {
                var groupMessage = DeserializeMessage <GroupMessage>(data);

                if (groupMessage.Action == GroupAction.Remove)
                {
                    if (!await RemoveGroupAsyncCore(groupMessage.ConnectionId, groupMessage.Group))
                    {
                        // user not on this server
                        return;
                    }
                }

                if (groupMessage.Action == GroupAction.Add)
                {
                    if (!await AddGroupAsyncCore(groupMessage.ConnectionId, groupMessage.Group))
                    {
                        // user not on this server
                        return;
                    }
                }

                // Sending ack to server that sent the original add/remove
                await PublishAsync($"{_channelNamePrefix}.internal.{groupMessage.Server}", new GroupMessage
                {
                    Action       = GroupAction.Ack,
                    ConnectionId = groupMessage.ConnectionId,
                    Group        = groupMessage.Group,
                    Id           = groupMessage.Id
                });
            });

            // Create server specific channel in order to send an ack to a single server
            var serverChannel = $"{_channelNamePrefix}.internal.{_serverName}";

            _bus.Subscribe(serverChannel, (c, data) =>
            {
                var groupMessage = DeserializeMessage <GroupMessage>(data);

                if (groupMessage.Action == GroupAction.Ack)
                {
                    _ackHandler.TriggerAck(groupMessage.Id);
                }
            });
        }