< Summary

Information
Class: Services.ApiServices.OzonApi.PackageLabelPollingHelper
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/OzonApi/PackageLabelPollingHelper.cs
Line coverage
70%
Covered lines: 47
Uncovered lines: 20
Coverable lines: 67
Total lines: 165
Line coverage: 70.1%
Branch coverage
48%
Covered branches: 37
Total branches: 76
Branch coverage: 48.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
PollSingleTaskForBytesAsync()0%110100%
PollSingleTaskForFileUrlAsync()25%161269.23%
PollTasksParallelForFileUrlsAsync()87.5%161690.9%
<PollTasksParallelForFileUrlsAsync()100%11100%
EvaluateResponse(...)52.63%643873.68%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/OzonApi/PackageLabelPollingHelper.cs

#LineLine coverage
 1using Services.ApiServices.OzonApi.Filters;
 2
 3namespace Services.ApiServices.OzonApi;
 4
 5public sealed class PackageLabelPollResult
 6{
 7    public bool IsSuccess { get; init; }
 8    public string? FileUrl { get; init; }
 9    public string? Error { get; init; }
 10
 11    public static PackageLabelPollResult Completed(string fileUrl) =>
 12        new() { IsSuccess = true, FileUrl = fileUrl };
 13
 14    public static PackageLabelPollResult Failed(string error) =>
 15        new() { IsSuccess = false, Error = error };
 16}
 17
 18/// <summary>
 19/// Общая логика опроса заданий на этикетки Ozon (package-label/get).
 20/// </summary>
 21public static class PackageLabelPollingHelper
 22{
 23    public const int DefaultMaxAttempts = 20;
 24
 125    private static readonly int[] BackoffDelaysMs = [300, 500, 1000, 2000];
 26
 27    public static async Task<byte[]> PollSingleTaskForBytesAsync(
 28        Func<long, Task<PackageLabelGetResponse>> getFile,
 29        Func<string, Task<byte[]>> downloadFile,
 30        long taskId,
 31        IReadOnlyCollection<string> postingNumbers,
 32        int maxAttempts = DefaultMaxAttempts,
 33        CancellationToken cancellationToken = default)
 34    {
 035        var poll = await PollSingleTaskForFileUrlAsync(getFile, taskId, postingNumbers, maxAttempts, cancellationToken)
 036            .ConfigureAwait(false);
 037        if (!poll.IsSuccess || string.IsNullOrEmpty(poll.FileUrl))
 038            throw new Exception(poll.Error ?? "Не удалось получить этикетку");
 39
 040        var bytes = await downloadFile(poll.FileUrl).ConfigureAwait(false);
 041        if (bytes is not { Length: > 0 })
 042            throw new Exception("Этикетка не была напечатана (пустой файл)");
 43
 044        return bytes;
 045    }
 46
 47    public static async Task<PackageLabelPollResult> PollSingleTaskForFileUrlAsync(
 48        Func<long, Task<PackageLabelGetResponse>> getFile,
 49        long taskId,
 50        IReadOnlyCollection<string> postingNumbers,
 51        int maxAttempts = DefaultMaxAttempts,
 52        CancellationToken cancellationToken = default)
 53    {
 154        var postingSet = new HashSet<string>(postingNumbers, StringComparer.Ordinal);
 155        PackageLabelGetResponse? lastResponse = null;
 56
 257        for (var attempt = 0; attempt < maxAttempts; attempt++)
 58        {
 159            if (attempt > 0)
 60            {
 061                var delayMs = BackoffDelaysMs[Math.Min(attempt - 1, BackoffDelaysMs.Length - 1)];
 062                await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false);
 63            }
 64
 165            lastResponse = await getFile(taskId).ConfigureAwait(false);
 166            var outcome = EvaluateResponse(lastResponse, postingSet);
 167            if (outcome != null)
 168                return outcome;
 69        }
 70
 071        var status = lastResponse?.Status?.Code ?? "unknown";
 072        return PackageLabelPollResult.Failed($"Не удалось получить этикетку: время ожидания истекло. Статус: {status}");
 173    }
 74
 75    /// <summary>
 76    /// Параллельно опрашивает все незавершённые taskId на каждой итерации (общий backoff).
 77    /// </summary>
 78    public static async Task<Dictionary<long, PackageLabelPollResult>> PollTasksParallelForFileUrlsAsync(
 79        Func<long, Task<PackageLabelGetResponse>> getFile,
 80        IReadOnlyDictionary<long, string[]> taskIdToPostings,
 81        int maxAttempts = DefaultMaxAttempts,
 82        CancellationToken cancellationToken = default)
 83    {
 184        var results = new Dictionary<long, PackageLabelPollResult>();
 185        if (taskIdToPostings.Count == 0)
 086            return results;
 87
 188        var pending = taskIdToPostings.Keys.ToHashSet();
 89
 690        for (var attempt = 0; attempt < maxAttempts && pending.Count > 0; attempt++)
 91        {
 292            if (attempt > 0)
 93            {
 194                var delayMs = BackoffDelaysMs[Math.Min(attempt - 1, BackoffDelaysMs.Length - 1)];
 195                await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false);
 96            }
 97
 298            var pollTasks = pending.Select(async taskId =>
 299            {
 6100                var response = await getFile(taskId).ConfigureAwait(false);
 6101                return (taskId, response);
 8102            });
 103
 2104            var pollResponses = await Task.WhenAll(pollTasks).ConfigureAwait(false);
 105
 16106            foreach (var (taskId, response) in pollResponses)
 107            {
 6108                var postings = taskIdToPostings[taskId];
 6109                var postingSet = new HashSet<string>(postings, StringComparer.Ordinal);
 6110                var outcome = EvaluateResponse(response, postingSet);
 6111                if (outcome == null)
 112                    continue;
 113
 3114                results[taskId] = outcome;
 3115                pending.Remove(taskId);
 116            }
 117        }
 118
 2119        foreach (var taskId in pending)
 120        {
 0121            results[taskId] = PackageLabelPollResult.Failed("Не удалось получить этикетку: время ожидания истекло");
 122        }
 123
 1124        return results;
 1125    }
 126
 127    private static PackageLabelPollResult? EvaluateResponse(
 128        PackageLabelGetResponse? response,
 129        HashSet<string> postingSet)
 130    {
 7131        if (response == null)
 0132            return null;
 133
 7134        var status = response.Status;
 135
 7136        if (status?.UnprintedPostings is { Count: > 0 })
 137        {
 1138            var unprinted = status.UnprintedPostings
 2139                .FirstOrDefault(p => !string.IsNullOrEmpty(p.PostingNumber) &&
 2140                                     postingSet.Contains(p.PostingNumber));
 1141            if (unprinted != null)
 142            {
 1143                return PackageLabelPollResult.Failed(
 1144                    $"Ошибка формирования этикетки для {unprinted.PostingNumber}: {unprinted.Message ?? "Неизвестная оши
 145            }
 146        }
 147
 6148        if (status?.Code == "completed" && !string.IsNullOrEmpty(response.FileUrl))
 149        {
 3150            if (status.PrintedPostingsCount > 0)
 3151                return PackageLabelPollResult.Completed(response.FileUrl);
 152
 0153            return PackageLabelPollResult.Failed(
 0154                $"Ошибка формирования этикетки: {response.Error?.Message ?? "Этикетка не была напечатана"}");
 155        }
 156
 3157        if (status?.Code == "error" || !string.IsNullOrEmpty(response.Error?.Message))
 158        {
 0159            return PackageLabelPollResult.Failed(
 0160                $"Ошибка формирования этикетки: {response.Error?.Message ?? "Неизвестная ошибка"}");
 161        }
 162
 3163        return null;
 164    }
 165}