< Summary

Information
Class: TeleFlow.Telegram.Internal.TelegramRequestExecutor
Assembly: TeleFlow.Telegram.Client
File(s): /_/src/TeleFlow.Telegram.Client/Internal/TelegramRequestExecutor.cs
Line coverage
95%
Covered lines: 246
Uncovered lines: 12
Coverable lines: 258
Total lines: 582
Line coverage: 95.3%
Branch coverage
87%
Covered branches: 65
Total branches: 74
Branch coverage: 87.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/_/src/TeleFlow.Telegram.Client/Internal/TelegramRequestExecutor.cs

#LineLine coverage
 1using System.Text.Json;
 2using Microsoft.Extensions.Logging;
 3
 4namespace TeleFlow.Telegram.Internal;
 5
 6internal sealed partial class TelegramRequestExecutor : ITelegramRequestExecutor
 7{
 8    private const int RequestStartedEventId = 1;
 9    private const int RequestCompletedEventId = 2;
 10    private const int RequestThrottledEventId = 3;
 11    private const int RequestFailedEventId = 4;
 12
 13    private readonly JsonSerializerOptions _serializerOptions;
 14    private readonly TelegramRequestSender _sender;
 15    private readonly TelegramTransportEnvelopeParser _envelopeParser;
 16    private readonly TelegramRetryAfterPolicy _retryAfterPolicy;
 17    private readonly TimeProvider _timeProvider;
 18    private readonly ILogger<TelegramRequestExecutor> _logger;
 19
 20    public TelegramRequestExecutor(
 21        ITelegramTransport transport,
 22        TelegramClientOptions options,
 23        TelegramJsonOptions jsonOptions,
 24        TimeProvider timeProvider,
 25        ILoggerFactory loggerFactory)
 26    {
 30227        ArgumentNullException.ThrowIfNull(transport);
 30228        ArgumentNullException.ThrowIfNull(options);
 30229        ArgumentNullException.ThrowIfNull(jsonOptions);
 30230        ArgumentNullException.ThrowIfNull(timeProvider);
 30231        ArgumentNullException.ThrowIfNull(loggerFactory);
 32
 30233        _serializerOptions = jsonOptions.SerializerOptions;
 30234        _sender = new TelegramRequestSender(
 30235            transport,
 30236            options,
 30237            new TelegramRequestContentBuilder(_serializerOptions));
 30238        _envelopeParser = new TelegramTransportEnvelopeParser(_serializerOptions);
 30239        _retryAfterPolicy = options.RetryAfter;
 30240        _timeProvider = timeProvider;
 30241        _logger = loggerFactory.CreateLogger<TelegramRequestExecutor>();
 30242    }
 43
 44    public async Task<TResponse> ExecuteAsync<TResponse>(
 45        ITelegramRequest<TResponse> request,
 46        CancellationToken cancellationToken = default)
 47        where TResponse : ITelegramResponse
 48    {
 8549        var executableRequest = GetExecutableRequest(request);
 8550        var context = TelegramRequestExecutionContext.Create(executableRequest.MethodName);
 51
 9252        for (var attempt = 1; ; attempt++)
 53        {
 9254            var diagnostics = TelegramRequestAttemptDiagnostics.Create(this, context, attempt);
 9255            var response = await SendAttemptAsync(
 9256                executableRequest,
 9257                diagnostics,
 9258                cancellationToken).ConfigureAwait(false);
 59
 8760            if (!_envelopeParser.TryParse(response.Body, out var envelope, out var parseException))
 61            {
 562                if (await TryDelayRetryAfterAsync(
 563                        diagnostics,
 564                        attempt,
 565                        response,
 566                        envelope: null,
 567                        cancellationToken).ConfigureAwait(false))
 68                {
 69                    continue;
 70                }
 71
 472                throw CreateUnparsedResponseException(
 473                    context,
 474                    diagnostics,
 475                    response,
 476                    parseException);
 77            }
 78
 8279            using var parsedEnvelope = envelope;
 80
 8281            if (parsedEnvelope.Ok)
 82            {
 5483                return DeserializeSuccessResponse(
 5484                    executableRequest,
 5485                    context,
 5486                    diagnostics,
 5487                    response,
 5488                    parsedEnvelope);
 89            }
 90
 2891            if (await TryDelayRetryAfterAsync(
 2892                    diagnostics,
 2893                    attempt,
 2894                    response,
 2895                    parsedEnvelope,
 2896                    cancellationToken).ConfigureAwait(false))
 97            {
 698                continue;
 99            }
 100
 21101            throw CreateApiFailureException(
 21102                context,
 21103                diagnostics,
 21104                response,
 21105                parsedEnvelope);
 106        }
 53107    }
 108
 109    private static ITelegramExecutableRequest<TResponse> GetExecutableRequest<TResponse>(
 110        ITelegramRequest<TResponse> request)
 111        where TResponse : ITelegramResponse
 112    {
 85113        if (request is ITelegramExecutableRequest<TResponse> executableRequest)
 114        {
 85115            return executableRequest;
 116        }
 117
 0118        throw new InvalidOperationException(
 0119            $"Request type '{request.GetType().FullName}' is not executable by the Telegram runtime.");
 120    }
 121
 122    private async Task<TelegramTransportResponse> SendAttemptAsync<TResponse>(
 123        ITelegramExecutableRequest<TResponse> executableRequest,
 124        TelegramRequestAttemptDiagnostics diagnostics,
 125        CancellationToken cancellationToken)
 126        where TResponse : ITelegramResponse
 127    {
 128        try
 129        {
 92130            var transportRequest = _sender.CreateRequest(executableRequest);
 91131            diagnostics.LogStarted(transportRequest.Content);
 132
 91133            var response = await _sender.SendAsync(transportRequest, cancellationToken).ConfigureAwait(false);
 87134            diagnostics.LogCompleted(response.StatusCode);
 135
 87136            return response;
 137        }
 1138        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 139        {
 1140            throw;
 141        }
 4142        catch (Exception exception)
 143        {
 4144            diagnostics.LogAttemptFailure(GetHttpStatusCode(exception), exception);
 4145            throw;
 146        }
 87147    }
 148
 149    private async ValueTask<bool> TryDelayRetryAfterAsync(
 150        TelegramRequestAttemptDiagnostics diagnostics,
 151        int attempt,
 152        TelegramTransportResponse response,
 153        TelegramTransportEnvelope? envelope,
 154        CancellationToken cancellationToken)
 155    {
 33156        if (!IsThrottlingResponse(response.StatusCode, envelope))
 157        {
 19158            return false;
 159        }
 160
 14161        var retryAfterDelay = TelegramRetryAfterDelayResolver.ResolveDelay(response, envelope, _timeProvider);
 14162        if (retryAfterDelay is null ||
 14163            !CanRetryAfter(attempt, retryAfterDelay.Value))
 164        {
 6165            return false;
 166        }
 167
 8168        diagnostics.LogThrottled(retryAfterDelay.Value);
 169
 8170        await TelegramRetryAfterDelayResolver.DelayAsync(
 8171            retryAfterDelay.Value,
 8172            _timeProvider,
 8173            cancellationToken).ConfigureAwait(false);
 174
 7175        return true;
 32176    }
 177
 178    private TResponse DeserializeSuccessResponse<TResponse>(
 179        ITelegramExecutableRequest<TResponse> executableRequest,
 180        TelegramRequestExecutionContext context,
 181        TelegramRequestAttemptDiagnostics diagnostics,
 182        TelegramTransportResponse response,
 183        TelegramTransportEnvelope envelope)
 184        where TResponse : ITelegramResponse
 185    {
 54186        if (!envelope.HasResult)
 187        {
 0188            var exception = new TelegramDecodeException(
 0189                $"Telegram response for method '{context.MethodName}' did not contain a result payload.",
 0190                methodName: context.MethodName,
 0191                httpStatusCode: response.StatusCode);
 192
 0193            diagnostics.LogFinalFailure(response.StatusCode, exception);
 0194            throw exception;
 195        }
 196
 197        try
 198        {
 54199            return executableRequest.DeserializeResponse(_serializerOptions, envelope.Result);
 200        }
 0201        catch (TelegramRequestException)
 202        {
 0203            throw;
 204        }
 1205        catch (Exception exception) when (exception is JsonException or NotSupportedException)
 206        {
 1207            var decodeException = new TelegramDecodeException(
 1208                $"Failed to deserialize Telegram response for method '{context.MethodName}'.",
 1209                exception,
 1210                context.MethodName,
 1211                httpStatusCode: response.StatusCode);
 212
 1213            diagnostics.LogFinalFailure(response.StatusCode, decodeException);
 1214            throw decodeException;
 215        }
 53216    }
 217
 218    private Exception CreateUnparsedResponseException(
 219        TelegramRequestExecutionContext context,
 220        TelegramRequestAttemptDiagnostics diagnostics,
 221        TelegramTransportResponse response,
 222        JsonException? parseException)
 223    {
 4224        var retryAfterHeaderDelay = TelegramRetryAfterDelayResolver.ResolveDelay(response, envelope: null, _timeProvider
 225
 4226        if (response.StatusCode == 429)
 227        {
 1228            var exception = CreateRetryAfterException(
 1229                context.MethodName,
 1230                response.StatusCode,
 1231                envelope: null,
 1232                retryAfterHeaderDelay,
 1233                GetRetryAfterFailureDescription(retryAfterHeaderDelay));
 234
 1235            diagnostics.LogFinalFailure(response.StatusCode, exception);
 1236            return exception;
 237        }
 238
 3239        var decodeException = new TelegramDecodeException(
 3240            $"Failed to parse Telegram API response envelope for method '{context.MethodName}'.",
 3241            parseException,
 3242            context.MethodName,
 3243            httpStatusCode: response.StatusCode,
 3244            retryAfterHeaderDelay?.Seconds);
 245
 3246        diagnostics.LogFinalFailure(response.StatusCode, decodeException);
 3247        return decodeException;
 248    }
 249
 250    private Exception CreateApiFailureException(
 251        TelegramRequestExecutionContext context,
 252        TelegramRequestAttemptDiagnostics diagnostics,
 253        TelegramTransportResponse response,
 254        TelegramTransportEnvelope envelope)
 255    {
 21256        if (TelegramApiExceptionFactory.IsThrottling(response.StatusCode, envelope))
 257        {
 5258            var retryAfterDelay = TelegramRetryAfterDelayResolver.ResolveDelay(response, envelope, _timeProvider);
 5259            var exception = CreateRetryAfterException(
 5260                context.MethodName,
 5261                response.StatusCode,
 5262                envelope,
 5263                retryAfterDelay,
 5264                GetRetryAfterFailureDescription(retryAfterDelay));
 265
 5266            diagnostics.LogFinalFailure(response.StatusCode, exception);
 5267            return exception;
 268        }
 269
 16270        var apiException = TelegramApiExceptionFactory.CreateApiException(
 16271            context.MethodName,
 16272            response.StatusCode,
 16273            envelope);
 274
 16275        diagnostics.LogFinalFailure(response.StatusCode, apiException);
 16276        return apiException;
 277    }
 278
 279    private static bool IsThrottlingResponse(
 280        int statusCode,
 281        TelegramTransportEnvelope? envelope)
 282    {
 33283        return envelope is null
 33284            ? statusCode == 429
 33285            : TelegramApiExceptionFactory.IsThrottling(statusCode, envelope);
 286    }
 287
 288    private static int? GetHttpStatusCode(Exception exception)
 289    {
 4290        return exception is TelegramRequestException requestException
 4291            ? requestException.HttpStatusCode
 4292            : null;
 293    }
 294
 295    private readonly record struct TelegramRequestExecutionContext(
 296        string MethodName,
 297        bool IsPollingRequest)
 298    {
 299        public static TelegramRequestExecutionContext Create(string methodName)
 300        {
 85301            return new TelegramRequestExecutionContext(
 85302                methodName,
 85303                IsPollingMethod(methodName));
 304        }
 305    }
 306
 307    private readonly struct TelegramRequestAttemptDiagnostics
 308    {
 309        private readonly TelegramRequestExecutor _executor;
 310        private readonly TelegramRequestExecutionContext _context;
 311        private readonly int _attempt;
 312        private readonly bool _debugEnabled;
 313        private readonly bool _errorEnabled;
 314        private readonly bool _timingEnabled;
 315        private readonly bool _enabled;
 316        private readonly long _started;
 317
 318        private TelegramRequestAttemptDiagnostics(
 319            TelegramRequestExecutor executor,
 320            TelegramRequestExecutionContext context,
 321            int attempt,
 322            bool debugEnabled,
 323            bool errorEnabled,
 324            bool timingEnabled)
 325        {
 92326            _executor = executor;
 92327            _context = context;
 92328            _attempt = attempt;
 92329            _debugEnabled = debugEnabled;
 92330            _errorEnabled = errorEnabled;
 92331            _timingEnabled = timingEnabled;
 92332            _enabled = debugEnabled || errorEnabled || timingEnabled;
 92333            _started = _enabled ? executor._timeProvider.GetTimestamp() : 0;
 92334        }
 335
 336        public static TelegramRequestAttemptDiagnostics Create(
 337            TelegramRequestExecutor executor,
 338            TelegramRequestExecutionContext context,
 339            int attempt)
 340        {
 92341            var debugEnabled = !context.IsPollingRequest && executor._logger.IsEnabled(LogLevel.Debug);
 92342            var errorEnabled = !context.IsPollingRequest && executor._logger.IsEnabled(LogLevel.Error);
 92343            var timingEnabled = !context.IsPollingRequest && TelegramHandlerRequestTimingScope.HasCurrent;
 344
 92345            return new TelegramRequestAttemptDiagnostics(
 92346                executor,
 92347                context,
 92348                attempt,
 92349                debugEnabled,
 92350                errorEnabled,
 92351                timingEnabled);
 352        }
 353
 354        public void LogStarted(TelegramTransportContent content)
 355        {
 91356            if (!_debugEnabled)
 357            {
 81358                return;
 359            }
 360
 10361            LogRequestStarted(
 10362                _executor._logger,
 10363                _context.MethodName,
 10364                _attempt,
 10365                GetContentKind(content));
 10366        }
 367
 368        public void LogCompleted(int httpStatusCode)
 369        {
 87370            if (!_enabled)
 371            {
 77372                return;
 373            }
 374
 10375            var ended = _executor._timeProvider.GetTimestamp();
 10376            RecordTiming(ended);
 377
 10378            if (!_debugEnabled)
 379            {
 1380                return;
 381            }
 382
 9383            LogRequestCompleted(
 9384                _executor._logger,
 9385                _context.MethodName,
 9386                _attempt,
 9387                httpStatusCode,
 9388                _executor.GetElapsedMilliseconds(_started, ended));
 9389        }
 390
 391        public void LogAttemptFailure(
 392            int? httpStatusCode,
 393            Exception exception)
 394        {
 4395            if (!_enabled)
 396            {
 3397                return;
 398            }
 399
 1400            var ended = _executor._timeProvider.GetTimestamp();
 1401            RecordTiming(ended);
 402
 1403            if (_errorEnabled)
 404            {
 1405                _executor.LogRequestFailure(
 1406                    _context.MethodName,
 1407                    _attempt,
 1408                    httpStatusCode,
 1409                    _started,
 1410                    ended,
 1411                    exception);
 412            }
 1413        }
 414
 415        public void LogFinalFailure(
 416            int httpStatusCode,
 417            Exception exception)
 418        {
 26419            if (!_errorEnabled)
 420            {
 24421                return;
 422            }
 423
 2424            _executor.LogRequestFailure(
 2425                _context.MethodName,
 2426                _attempt,
 2427                httpStatusCode,
 2428                _started,
 2429                _executor._timeProvider.GetTimestamp(),
 2430                exception);
 2431        }
 432
 433        public void LogThrottled(TelegramRetryAfterDelay retryAfter)
 434        {
 8435            if (_context.IsPollingRequest ||
 8436                !_executor._logger.IsEnabled(LogLevel.Warning))
 437            {
 7438                return;
 439            }
 440
 1441            _executor.LogRequestThrottled(
 1442                _context.MethodName,
 1443                _attempt,
 1444                retryAfter);
 1445        }
 446
 447        private void RecordTiming(long ended)
 448        {
 11449            if (_timingEnabled)
 450            {
 4451                TelegramHandlerRequestTimingScope.Record(_started, ended);
 452            }
 11453        }
 454    }
 455
 456    private static bool IsPollingMethod(string methodName)
 457    {
 85458        return string.Equals(methodName, "getUpdates", StringComparison.Ordinal);
 459    }
 460
 461    private bool CanRetryAfter(int attempt, TelegramRetryAfterDelay retryAfter)
 462    {
 12463        return _retryAfterPolicy.Enabled &&
 12464            attempt <= _retryAfterPolicy.MaxRetries &&
 12465            retryAfter.Value <= _retryAfterPolicy.MaxDelay;
 466    }
 467
 468    private static TelegramRetryAfterException CreateRetryAfterException(
 469        string methodName,
 470        int httpStatusCode,
 471        TelegramTransportEnvelope? envelope,
 472        TelegramRetryAfterDelay? retryAfter,
 473        string fallbackDescription)
 474    {
 6475        var description = envelope?.Description ?? fallbackDescription;
 6476        var message = $"Telegram request '{methodName}' failed: {description}";
 477
 6478        return new TelegramRetryAfterException(
 6479            message,
 6480            methodName,
 6481            httpStatusCode: httpStatusCode,
 6482            telegramErrorCode: envelope?.ErrorCode,
 6483            description: description,
 6484            retryAfterSeconds: retryAfter?.Seconds);
 485    }
 486
 487    private static string GetRetryAfterFailureDescription(TelegramRetryAfterDelay? retryAfter)
 488    {
 6489        return retryAfter is null
 6490            ? "Telegram throttling response did not provide retry timing metadata."
 6491            : "Telegram request was throttled and automatic retry-after handling was not applied by the configured polic
 492    }
 493
 494    private static string GetContentKind(TelegramTransportContent content)
 495    {
 10496        return content switch
 10497        {
 10498            TelegramJsonTransportContent => "json",
 0499            TelegramMultipartTransportContent => "multipart",
 0500            _ => content.GetType().Name
 10501        };
 502    }
 503
 504    private void LogRequestThrottled(
 505        string methodName,
 506        int attempt,
 507        TelegramRetryAfterDelay retryAfter)
 508    {
 1509        LogRequestThrottledCore(
 1510            _logger,
 1511            methodName,
 1512            attempt,
 1513            retryAfter.Value);
 1514    }
 515
 516    private void LogRequestFailure(
 517        string methodName,
 518        int attempt,
 519        int? httpStatusCode,
 520        long attemptStarted,
 521        long attemptEnded,
 522        Exception exception)
 523    {
 3524        LogRequestFailed(
 3525            _logger,
 3526            exception,
 3527            methodName,
 3528            attempt,
 3529            httpStatusCode,
 3530            GetElapsedMilliseconds(attemptStarted, attemptEnded),
 3531            exception.GetType().FullName ?? exception.GetType().Name);
 3532    }
 533
 534    private double GetElapsedMilliseconds(long startingTimestamp, long endingTimestamp)
 535    {
 12536        return _timeProvider.GetElapsedTime(startingTimestamp, endingTimestamp).TotalMilliseconds;
 537    }
 538
 539    [LoggerMessage(
 540        EventId = RequestStartedEventId,
 541        Level = LogLevel.Debug,
 542        Message = "Telegram request started. method={MethodName}, attempt={Attempt}, content={ContentKind}.")]
 543    private static partial void LogRequestStarted(
 544        ILogger logger,
 545        string methodName,
 546        int attempt,
 547        string contentKind);
 548
 549    [LoggerMessage(
 550        EventId = RequestCompletedEventId,
 551        Level = LogLevel.Debug,
 552        Message = "Telegram request completed. method={MethodName}, attempt={Attempt}, status={HttpStatusCode}, request_
 553    private static partial void LogRequestCompleted(
 554        ILogger logger,
 555        string methodName,
 556        int attempt,
 557        int httpStatusCode,
 558        double requestElapsedMilliseconds);
 559
 560    [LoggerMessage(
 561        EventId = RequestThrottledEventId,
 562        Level = LogLevel.Warning,
 563        Message = "Telegram request throttled. method={MethodName}, attempt={Attempt}, retry_after={RetryAfter}.")]
 564    private static partial void LogRequestThrottledCore(
 565        ILogger logger,
 566        string methodName,
 567        int attempt,
 568        TimeSpan retryAfter);
 569
 570    [LoggerMessage(
 571        EventId = RequestFailedEventId,
 572        Level = LogLevel.Error,
 573        Message = "Telegram request failed. method={MethodName}, attempt={Attempt}, status={HttpStatusCode}, request_ms=
 574    private static partial void LogRequestFailed(
 575        ILogger logger,
 576        Exception exception,
 577        string methodName,
 578        int attempt,
 579        int? httpStatusCode,
 580        double requestElapsedMilliseconds,
 581        string exceptionType);
 582}

Methods/Properties

.ctor(TeleFlow.Telegram.ITelegramTransport,TeleFlow.Telegram.TelegramClientOptions,TeleFlow.Telegram.TelegramJsonOptions,System.TimeProvider,Microsoft.Extensions.Logging.ILoggerFactory)
ExecuteAsync()
GetExecutableRequest(TeleFlow.Telegram.ITelegramRequest`1<TResponse>)
SendAttemptAsync()
TryDelayRetryAfterAsync()
DeserializeSuccessResponse(TeleFlow.Telegram.Internal.ITelegramExecutableRequest`1<TResponse>,TeleFlow.Telegram.Internal.TelegramRequestExecutor/TelegramRequestExecutionContext,TeleFlow.Telegram.Internal.TelegramRequestExecutor/TelegramRequestAttemptDiagnostics,TeleFlow.Telegram.TelegramTransportResponse,TeleFlow.Telegram.Internal.TelegramTransportEnvelope)
CreateUnparsedResponseException(TeleFlow.Telegram.Internal.TelegramRequestExecutor/TelegramRequestExecutionContext,TeleFlow.Telegram.Internal.TelegramRequestExecutor/TelegramRequestAttemptDiagnostics,TeleFlow.Telegram.TelegramTransportResponse,System.Text.Json.JsonException)
CreateApiFailureException(TeleFlow.Telegram.Internal.TelegramRequestExecutor/TelegramRequestExecutionContext,TeleFlow.Telegram.Internal.TelegramRequestExecutor/TelegramRequestAttemptDiagnostics,TeleFlow.Telegram.TelegramTransportResponse,TeleFlow.Telegram.Internal.TelegramTransportEnvelope)
IsThrottlingResponse(System.Int32,TeleFlow.Telegram.Internal.TelegramTransportEnvelope)
GetHttpStatusCode(System.Exception)
Create(System.String)
.ctor(TeleFlow.Telegram.Internal.TelegramRequestExecutor,TeleFlow.Telegram.Internal.TelegramRequestExecutor/TelegramRequestExecutionContext,System.Int32,System.Boolean,System.Boolean,System.Boolean)
Create(TeleFlow.Telegram.Internal.TelegramRequestExecutor,TeleFlow.Telegram.Internal.TelegramRequestExecutor/TelegramRequestExecutionContext,System.Int32)
LogStarted(TeleFlow.Telegram.TelegramTransportContent)
LogCompleted(System.Int32)
LogAttemptFailure(System.Nullable`1<System.Int32>,System.Exception)
LogFinalFailure(System.Int32,System.Exception)
LogThrottled(TeleFlow.Telegram.Internal.TelegramRetryAfterDelay)
RecordTiming(System.Int64)
IsPollingMethod(System.String)
CanRetryAfter(System.Int32,TeleFlow.Telegram.Internal.TelegramRetryAfterDelay)
CreateRetryAfterException(System.String,System.Int32,TeleFlow.Telegram.Internal.TelegramTransportEnvelope,System.Nullable`1<TeleFlow.Telegram.Internal.TelegramRetryAfterDelay>,System.String)
GetRetryAfterFailureDescription(System.Nullable`1<TeleFlow.Telegram.Internal.TelegramRetryAfterDelay>)
GetContentKind(TeleFlow.Telegram.TelegramTransportContent)
LogRequestThrottled(System.String,System.Int32,TeleFlow.Telegram.Internal.TelegramRetryAfterDelay)
LogRequestFailure(System.String,System.Int32,System.Nullable`1<System.Int32>,System.Int64,System.Int64,System.Exception)
GetElapsedMilliseconds(System.Int64,System.Int64)