public async ValueTask <Stream> ReceiveAsync(CancellationToken cancellationToken)
        {
            ThrowIfDisposed();

            await _receiveSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);

            try
            {
                _receiveStream.Position = 0;
                _receiveStream.SetLength(0);
                do
                {
                    // See _limboCts for more info on cancellation.
                    var receiveTask = _ws.ReceiveAsync(_receiveBuffer, _limboCts.Token).AsTask();
                    using (var infiniteCts = Cts.Linked(cancellationToken))
                    {
                        var infiniteTask = Task.Delay(Timeout.Infinite, infiniteCts.Token);
                        var task         = await Task.WhenAny(infiniteTask, receiveTask).ConfigureAwait(false);

                        infiniteCts.Cancel();
                    }

                    if (cancellationToken.IsCancellationRequested)
                    {
                        throw new OperationCanceledException(cancellationToken);
                    }

                    var result = await receiveTask.ConfigureAwait(false);

                    if (result.MessageType == WebSocketMessageType.Close)
                    {
                        var closeStatus  = _ws.CloseStatus;
                        var closeMessage = _ws.CloseMessage;
                        try
                        {
                            await _ws.CloseOutputAsync(closeStatus.GetValueOrDefault(), closeMessage, default).ConfigureAwait(false);
                        }
                        catch { }

                        throw new WebSocketClosedException(closeStatus, closeMessage);
                    }

                    _receiveStream.Write(_receiveBuffer.AsSpan(0, result.Count));
                    if (!result.EndOfMessage)
                    {
                        continue;
                    }

                    if (result.MessageType != WebSocketMessageType.Binary)
                    {
                        _receiveStream.Position = 0;
                        return(_receiveStream);
                    }

                    _receiveStream.TryGetBuffer(out var streamBuffer);

                    // We check the data for the ZLib flush which marks the end of the actual message.
                    if (streamBuffer.Count < 4 || BinaryPrimitives.ReadUInt32BigEndian(streamBuffer[^ 4..]) != 0x0000FFFF)
        /// <inheritdoc/>
        public virtual Task StartAsync(CancellationToken cancellationToken)
        {
            var method = GetType().GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic, null, new[] { typeof(CancellationToken) }, null);

            if (method.DeclaringType == method.GetBaseDefinition().DeclaringType)
            {
                return(Task.CompletedTask);
            }

            _cts = Cts.Linked(cancellationToken);
            var stoppingToken = _cts.Token;

            _executeTask = Task.Run(async() =>
            {
                try
                {
                    await ExecuteAsync(stoppingToken).ConfigureAwait(false);
                }
                catch (OperationCanceledException ex) when(ex.CancellationToken == stoppingToken)
                {
                    // Ignore cancellation exceptions caused by the stopping token.
                }
                catch (Exception ex)
                {
                    Logger.LogError(ex, "An exception occurred while executing service {0}.", GetType().Name);
                    throw;
                }
            }, cancellationToken);

            if (_executeTask.IsCompleted)
            {
                return(_executeTask);
            }

            return(Task.CompletedTask);
        }
Exemple #3
0
        public override async Task RunAsync(CancellationToken stoppingToken)
        {
            StoppingToken = stoppingToken;
            // TODO: fallback on bot gateway data?
            var uri = new Uri("wss://gateway.discord.gg/");

            _scopes = new Dictionary <ShardId, IServiceScope>();
            var shardIds = new List <ShardId>();

            if (_configuredShardCount != null)
            {
                // If the shard count is specified we generate the appropriate shard IDs.
                var shardCount = _configuredShardCount.Value;
                for (var i = 0; i < shardCount; i++)
                {
                    var id = new ShardId(i, shardCount);
                    shardIds.Add(id);
                }
            }
            else if (_configuredShardIds != null)
            {
                // If the shard IDs are specified we validate them and manage those instead.
                // TODO: validation
                foreach (var id in _configuredShardIds)
                {
                    shardIds.Add(id);
                }
            }
            else
            {
                var botGatewayData = await this.FetchBotGatewayDataAsync(cancellationToken : stoppingToken);

                Logger.LogDebug("Using Discord's recommended shard count of {0}.", botGatewayData.RecommendedShardCount);
                for (var i = 0; i < botGatewayData.RecommendedShardCount; i++)
                {
                    var id = new ShardId(i, botGatewayData.RecommendedShardCount);
                    shardIds.Add(id);
                }
            }

            Logger.LogInformation("This sharder will manage {0} shards with IDs: {1}", shardIds.Count, shardIds.Select(x => x.Id));
            var shards               = GatewayClient.Shards as ISynchronizedDictionary <ShardId, IGatewayApiClient>;
            var dispatcher           = GatewayClient.Dispatcher as DefaultGatewayDispatcher;
            var originalReadyHandler = dispatcher["READY"] as ReadyHandler;

            originalReadyHandler.InitialReadys.Clear();

            // We create a service scope and a shard for every
            // shard ID this sharder is supposed to manage.
            foreach (var id in shardIds)
            {
                var scope = _services.CreateScope();
                _scopes.Add(id, scope);
                shards.Add(id, _shardFactory.Create(id, scope.ServiceProvider));

                originalReadyHandler.InitialReadys.Add(id, new Tcs());
            }

            _initialReadyTcs.Complete();

            // These two locals will be reused via the closure below.
            Tcs     readyTcs = null;
            ShardId shardId  = default;

            dispatcher["READY"] = Handler.Intercept(originalReadyHandler, (shard, _) =>
            {
                if (shard.Id == shardId)
                {
                    // If the shard that identified is the shard ID we're booting up
                    // we complete the TCS to signal the code below to boot the next shard.
                    readyTcs.Complete();
                }
            });

            var linkedCts = Cts.Linked(stoppingToken);
            var runTasks  = new List <Task>(shards.Count);

            foreach (var shard in shards.Values)
            {
                // Set the TCS and shard ID for the shard we'll boot up
                // which are used by the intercepting ready handler above.
                readyTcs = new Tcs();
                shardId  = shard.Id;

                shard.DispatchReceived += dispatcher.HandleDispatchAsync;
                var runTask = shard.RunAsync(uri, linkedCts.Token);
                var task    = Task.WhenAny(runTask, readyTcs.Task);
                try
                {
                    if (await task.ConfigureAwait(false) == runTask)
                    {
                        // If the task that finished is the run task
                        // it means an exception occurred when identifying.
                        // We await it to get the exception thrown.
                        await runTask.ConfigureAwait(false);
                    }

                    // If we reached here it means the shard successfully identified,
                    // so we add the task to the list and continue.
                    runTasks.Add(runTask);
                }
                catch (Exception ex)
                {
                    Logger.LogCritical(ex, "An exception occurred when starting {0}.", shardId);

                    // Cancel the CTS so that if some shards are running already
                    // it's a coordinated close as they will probably break sooner or later.
                    linkedCts.Cancel();

                    // We bubble the exception up to the client runner.
                    throw;
                }
            }

            shardId = default;
            try
            {
                // We wait for any of the tasks to finish (throw)
                // and handle it appropriately.
                var runTask = await Task.WhenAny(runTasks).ConfigureAwait(false);

                await runTask.ConfigureAwait(false);
            }
            catch
            {
                // TODO: if sharding required redo the process with more shards blah blah
                linkedCts.Cancel();
                throw;
            }
        }
Exemple #4
0
        public async ValueTask <Stream> ReceiveAsync(CancellationToken cancellationToken)
        {
            ThrowIfDisposed();

            await _receiveSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);

            try
            {
                // Ensures that the receive stream is fully read and the underlying DeflateStream acknowledges the ZLib suffix.
                if (_supportsZLib && _wasLastPayloadZLib && _receiveStream.Position != _receiveStream.Length)
                {
                    // We just need the inflater to read further so that it picks up the suffix and knows it's done.
                    _receiveZLibStream.Read(Array.Empty <byte>());
                }

                _receiveStream.Position = 0;
                _receiveStream.SetLength(0);
                do
                {
                    // See _limboCts for more info on cancellation.
                    var receiveTask = _ws.ReceiveAsync(_receiveBuffer, _limboCts.Token).AsTask();
                    using (var infiniteCts = Cts.Linked(cancellationToken))
                    {
                        var infiniteTask = Task.Delay(Timeout.Infinite, infiniteCts.Token);
                        var task         = await Task.WhenAny(infiniteTask, receiveTask).ConfigureAwait(false);

                        infiniteCts.Cancel();
                    }

                    if (cancellationToken.IsCancellationRequested)
                    {
                        throw new OperationCanceledException(cancellationToken);
                    }

                    var result = await receiveTask.ConfigureAwait(false);

                    if (result.MessageType == WebSocketMessageType.Close)
                    {
                        var closeStatus  = _ws.CloseStatus;
                        var closeMessage = _ws.CloseMessage;
                        try
                        {
                            await _ws.CloseOutputAsync(closeStatus.GetValueOrDefault(), closeMessage, default).ConfigureAwait(false);
                        }
                        catch { }

                        throw new WebSocketClosedException(closeStatus, closeMessage);
                    }

                    _receiveStream.Write(_receiveBuffer.AsSpan(0, result.Count));
                    if (!result.EndOfMessage)
                    {
                        continue;
                    }

                    if (result.MessageType != WebSocketMessageType.Binary)
                    {
                        _wasLastPayloadZLib     = false;
                        _receiveStream.Position = 0;
                        return(_receiveStream);
                    }

                    _receiveStream.TryGetBuffer(out var streamBuffer);

                    // We check the data for the ZLib flush which marks the end of the actual message.
                    if (streamBuffer.Count < 4 || BinaryPrimitives.ReadUInt32BigEndian(streamBuffer[^ 4..]) != 0x0000FFFF)