< Summary

Information
Class: TeleFlow.Telegram.TelegramLongPollingClient
Assembly: TeleFlow.Telegram.LongPolling
File(s): /_/src/TeleFlow.Telegram.LongPolling/TelegramLongPollingClient.cs
Line coverage
89%
Covered lines: 142
Uncovered lines: 17
Coverable lines: 159
Total lines: 341
Line coverage: 89.3%
Branch coverage
94%
Covered branches: 53
Total branches: 56
Branch coverage: 94.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
RunAsync()91.66%181265.95%
GetUpdatesAsync()100%1818100%
GetUpdatesBatchAsync()100%11100%
LogStart(...)100%22100%
GetElapsedMilliseconds(...)100%210%
DelayAsync(...)50%22100%
CopyAllowedUpdates(...)100%22100%
IsPollingTransient(...)87.5%88100%
GetPollingRetryDelay(...)100%44100%

File(s)

/_/src/TeleFlow.Telegram.LongPolling/TelegramLongPollingClient.cs

#LineLine coverage
 1using System.Runtime.CompilerServices;
 2using Microsoft.Extensions.Logging;
 3using TeleFlow.Telegram.Internal;
 4using TeleFlow.Telegram.Schema.Methods;
 5using TeleFlow.Telegram.Schema.Types;
 6
 7namespace TeleFlow.Telegram;
 8
 9public sealed partial class TelegramLongPollingClient : ITelegramLongPollingClient
 10{
 11    private readonly ITelegramClient _telegramClient;
 12    private readonly TimeProvider _timeProvider;
 13    private readonly ILogger<TelegramLongPollingClient> _logger;
 14
 15    public TelegramLongPollingClient(
 16        ITelegramClient telegramClient,
 17        TimeProvider timeProvider,
 18        ILoggerFactory loggerFactory)
 19    {
 2520        ArgumentNullException.ThrowIfNull(telegramClient);
 2521        ArgumentNullException.ThrowIfNull(timeProvider);
 2522        ArgumentNullException.ThrowIfNull(loggerFactory);
 23
 2524        _telegramClient = telegramClient;
 2525        _timeProvider = timeProvider;
 2526        _logger = loggerFactory.CreateLogger<TelegramLongPollingClient>();
 2527    }
 28
 29    public async Task RunAsync(
 30        Func<Update, CancellationToken, Task> updateHandler,
 31        TelegramRawLongPollingOptions? options = null,
 32        CancellationToken cancellationToken = default)
 33    {
 634        ArgumentNullException.ThrowIfNull(updateHandler);
 35
 636        options ??= new TelegramRawLongPollingOptions();
 637        TelegramRawLongPollingOptionsValidator.Validate(options);
 38
 439        long? offset = null;
 440        var backoff = new TelegramRawLongPollingBackoff(options.Backoff);
 441        var allowedUpdates = CopyAllowedUpdates(options.AllowedUpdates);
 442        var connected = false;
 443        var recoveringFromPollingFailure = false;
 44
 445        LogStart(options, allowedUpdates);
 46
 947        while (!cancellationToken.IsCancellationRequested)
 48        {
 649            var updates = await GetUpdatesBatchAsync(
 650                offset,
 651                options,
 652                allowedUpdates,
 653                backoff,
 654                () =>
 655                {
 656                    if (!connected)
 657                    {
 458                        LogConnected(_logger);
 459                        connected = true;
 660                    }
 661
 662                    if (recoveringFromPollingFailure)
 663                    {
 264                        LogGetUpdatesRecovered(_logger);
 265                        recoveringFromPollingFailure = false;
 666                    }
 667                },
 368                () => recoveringFromPollingFailure = true,
 669                cancellationToken).ConfigureAwait(false);
 70
 2071            for (var index = 0; index < updates.Count; index++)
 72            {
 573                var update = updates[index];
 74
 575                if (_logger.IsEnabled(LogLevel.Debug))
 76                {
 077                    var updateType = TelegramRawLongPollingLogFormatter.GetUpdateType(update);
 078                    var processingStarted = _timeProvider.GetTimestamp();
 79
 080                    LogUpdateReceived(
 081                        _logger,
 082                        update.UpdateId,
 083                        updateType,
 084                        index + 1,
 085                        updates.Count);
 86
 087                    await updateHandler(update, cancellationToken).ConfigureAwait(false);
 088                    offset = update.UpdateId + 1;
 89
 090                    LogUpdateAcknowledgedByHandler(
 091                        _logger,
 092                        update.UpdateId,
 093                        updateType,
 094                        GetElapsedMilliseconds(processingStarted));
 095                    continue;
 96                }
 97
 598                await updateHandler(update, cancellationToken).ConfigureAwait(false);
 499                offset = update.UpdateId + 1;
 4100            }
 5101        }
 3102    }
 103
 104    public async IAsyncEnumerable<TelegramPolledUpdate> GetUpdatesAsync(
 105        TelegramRawLongPollingOptions? options = null,
 106        [EnumeratorCancellation] CancellationToken cancellationToken = default)
 107    {
 18108        options ??= new TelegramRawLongPollingOptions();
 18109        TelegramRawLongPollingOptionsValidator.Validate(options);
 110
 18111        long? offset = null;
 18112        var backoff = new TelegramRawLongPollingBackoff(options.Backoff);
 18113        var allowedUpdates = CopyAllowedUpdates(options.AllowedUpdates);
 18114        var connected = false;
 18115        var recoveringFromPollingFailure = false;
 116
 18117        LogStart(options, allowedUpdates);
 118
 37119        while (!cancellationToken.IsCancellationRequested)
 120        {
 23121            var updates = await GetUpdatesBatchAsync(
 23122                offset,
 23123                options,
 23124                allowedUpdates,
 23125                backoff,
 23126                () =>
 23127                {
 23128                    if (!connected)
 23129                    {
 18130                        LogConnected(_logger);
 18131                        connected = true;
 23132                    }
 23133
 23134                    if (recoveringFromPollingFailure)
 23135                    {
 4136                        LogGetUpdatesRecovered(_logger);
 4137                        recoveringFromPollingFailure = false;
 23138                    }
 23139                },
 5140                () => recoveringFromPollingFailure = true,
 23141                cancellationToken).ConfigureAwait(false);
 142
 82143            for (var index = 0; index < updates.Count; index++)
 144            {
 22145                var update = updates[index];
 22146                var polledUpdate = new TelegramPolledUpdate(update, index + 1, updates.Count);
 147
 22148                var debugEnabled = _logger.IsEnabled(LogLevel.Debug);
 22149                var updateType = debugEnabled
 22150                    ? TelegramRawLongPollingLogFormatter.GetUpdateType(update)
 22151                    : string.Empty;
 152
 22153                if (debugEnabled)
 154                {
 2155                    LogStreamUpdateReceived(
 2156                        _logger,
 2157                        update.UpdateId,
 2158                        updateType);
 159                }
 160
 22161                yield return polledUpdate;
 162
 19163                if (!polledUpdate.IsAcknowledged)
 164                {
 1165                    throw new InvalidOperationException(
 1166                        "Telegram polled updates must be acknowledged with AcknowledgeAsync before requesting the next u
 167                }
 168
 18169                offset = update.UpdateId + 1;
 170
 18171                if (debugEnabled)
 172                {
 1173                    LogStreamUpdateAcknowledged(
 1174                        _logger,
 1175                        update.UpdateId,
 1176                        updateType);
 177                }
 18178            }
 19179        }
 17180    }
 181
 182    private async Task<IReadOnlyList<Update>> GetUpdatesBatchAsync(
 183        long? offset,
 184        TelegramRawLongPollingOptions options,
 185        IReadOnlyList<string>? allowedUpdates,
 186        TelegramRawLongPollingBackoff backoff,
 187        Action onSuccess,
 188        Action onTransientFailure,
 189        CancellationToken cancellationToken)
 190    {
 191        while (true)
 192        {
 193            try
 194            {
 37195                var updates = await _telegramClient.SendAsync(
 37196                    new GetUpdates
 37197                    {
 37198                        Offset = offset,
 37199                        Limit = options.Limit,
 37200                        Timeout = options.TimeoutSeconds,
 37201                        AllowedUpdates = allowedUpdates
 37202                    },
 37203                    cancellationToken).ConfigureAwait(false);
 204
 29205                onSuccess();
 29206                backoff.Reset();
 29207                return updates;
 208            }
 8209            catch (Exception exception) when (IsPollingTransient(exception) && !cancellationToken.IsCancellationRequeste
 210            {
 8211                var delay = GetPollingRetryDelay(exception, backoff);
 8212                onTransientFailure();
 213
 8214                LogGetUpdatesFailed(
 8215                    _logger,
 8216                    exception,
 8217                    delay);
 218
 8219                await DelayAsync(delay, cancellationToken).ConfigureAwait(false);
 8220            }
 221        }
 29222    }
 223
 224    private void LogStart(
 225        TelegramRawLongPollingOptions options,
 226        IReadOnlyList<string>? allowedUpdates)
 227    {
 22228        if (!_logger.IsEnabled(LogLevel.Information))
 229        {
 20230            return;
 231        }
 232
 2233        LogStarting(
 2234            _logger,
 2235            TelegramRawLongPollingLogFormatter.FormatAllowedUpdates(allowedUpdates),
 2236            options.TimeoutSeconds,
 2237            options.Limit);
 2238    }
 239
 240    private double GetElapsedMilliseconds(long startingTimestamp)
 241    {
 0242        return _timeProvider.GetElapsedTime(startingTimestamp).TotalMilliseconds;
 243    }
 244
 245    private ValueTask DelayAsync(TimeSpan delay, CancellationToken cancellationToken)
 246    {
 8247        return delay <= TimeSpan.Zero
 8248            ? ValueTask.CompletedTask
 8249            : new ValueTask(Task.Delay(delay, _timeProvider, cancellationToken));
 250    }
 251
 252    private static string[]? CopyAllowedUpdates(IReadOnlyList<string>? allowedUpdates)
 253    {
 22254        return allowedUpdates is null ? null : allowedUpdates.ToArray();
 255    }
 256
 257    private static bool IsPollingTransient(Exception exception)
 258    {
 8259        return exception is TelegramNetworkException or
 8260            TelegramServerException or
 8261            TelegramDecodeException or
 8262            TelegramRetryAfterException;
 263    }
 264
 265    private static TimeSpan GetPollingRetryDelay(Exception exception, TelegramRawLongPollingBackoff backoff)
 266    {
 8267        return exception is TelegramRetryAfterException { RetryAfter: { } retryAfter }
 8268            ? retryAfter
 8269            : backoff.NextDelay();
 270    }
 271
 272    [LoggerMessage(
 273        EventId = 1,
 274        Level = LogLevel.Information,
 275        Message = "Starting raw Telegram long polling. allowed_updates={AllowedUpdates}, timeout={TimeoutSeconds}s, limi
 276    private static partial void LogStarting(
 277        ILogger logger,
 278        string allowedUpdates,
 279        int timeoutSeconds,
 280        int limit);
 281
 282    [LoggerMessage(
 283        EventId = 2,
 284        Level = LogLevel.Information,
 285        Message = "Raw Telegram long polling connected.")]
 286    private static partial void LogConnected(ILogger logger);
 287
 288    [LoggerMessage(
 289        EventId = 3,
 290        Level = LogLevel.Information,
 291        Message = "Raw Telegram long polling getUpdates recovered after transient failures.")]
 292    private static partial void LogGetUpdatesRecovered(ILogger logger);
 293
 294    [LoggerMessage(
 295        EventId = 4,
 296        Level = LogLevel.Debug,
 297        Message = "Raw Telegram update received. update_id={UpdateId}, type={UpdateType}, batch_index={BatchIndex}/{Batc
 298    private static partial void LogUpdateReceived(
 299        ILogger logger,
 300        long updateId,
 301        string updateType,
 302        int batchIndex,
 303        int batchCount);
 304
 305    [LoggerMessage(
 306        EventId = 5,
 307        Level = LogLevel.Debug,
 308        Message = "Raw Telegram update acknowledged by handler. update_id={UpdateId}, type={UpdateType}, total_ms={Total
 309    private static partial void LogUpdateAcknowledgedByHandler(
 310        ILogger logger,
 311        long updateId,
 312        string updateType,
 313        double totalElapsedMilliseconds);
 314
 315    [LoggerMessage(
 316        EventId = 6,
 317        Level = LogLevel.Debug,
 318        Message = "Raw Telegram update received. update_id={UpdateId}, type={UpdateType}.")]
 319    private static partial void LogStreamUpdateReceived(
 320        ILogger logger,
 321        long updateId,
 322        string updateType);
 323
 324    [LoggerMessage(
 325        EventId = 7,
 326        Level = LogLevel.Debug,
 327        Message = "Raw Telegram update acknowledged. update_id={UpdateId}, type={UpdateType}.")]
 328    private static partial void LogStreamUpdateAcknowledged(
 329        ILogger logger,
 330        long updateId,
 331        string updateType);
 332
 333    [LoggerMessage(
 334        EventId = 8,
 335        Level = LogLevel.Warning,
 336        Message = "Raw Telegram long polling getUpdates failed. Retrying in {Delay}.")]
 337    private static partial void LogGetUpdatesFailed(
 338        ILogger logger,
 339        Exception exception,
 340        TimeSpan delay);
 341}