< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.AnalysisServices.ProductAnalysisCache
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/AnalysisServices/ProductAnalysisCache.cs
Line coverage
100%
Covered lines: 18
Uncovered lines: 0
Coverable lines: 18
Total lines: 54
Line coverage: 100%
Branch coverage
100%
Covered branches: 6
Total branches: 6
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
Canon(...)100%44100%
GetOrAddAsync()100%11100%
Dispose()100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/AnalysisServices/ProductAnalysisCache.cs

#LineLine coverage
 1using Microsoft.Extensions.Caching.Memory;
 2
 3namespace OzonOrdersWeb.Areas.Studio2.AnalysisServices;
 4
 5/// <summary>
 6/// Ограниченный по объёму кэш отчётов «Анализа продуктов» (отдельный экземпляр MemoryCache с SizeLimit).
 7/// Строки заказов периода занимают много места, поэтому ключи с произвольными датами не могут раздуть память процесса:
 8/// при превышении лимита вытесняются старые записи. Одинаковые параллельные запросы делят одну загрузку.
 9/// </summary>
 10public sealed class ProductAnalysisCache : IDisposable
 11{
 12    /// <summary>Лимит кэша в условных единицах; строки периода весят <see cref="RowsWeight"/>, остальное — меньше.</sum
 13    public const long SizeLimit = 200;
 14    public const int RowsWeight = 50;
 15    public const int GroupsWeight = 10;
 16    public const int ResultWeight = 1;
 17
 218    private readonly MemoryCache _cache = new(new MemoryCacheOptions { SizeLimit = SizeLimit });
 19
 20    /// <summary>
 21    /// Границы периода для ключа и запроса: серверные «сейчас» из браузера отличаются секундами, из-за чего кэш не попа
 22    /// время округляется вниз до 5 минут, конец дня (23:59:59…) сохраняется.
 23    /// </summary>
 24    public static DateTime? Canon(DateTime? value)
 25    {
 526        if (!value.HasValue) return null;
 327        var d = value.Value;
 428        if (d.TimeOfDay >= new TimeSpan(23, 59, 59)) return d.Date.AddDays(1).AddTicks(-1);
 229        return new DateTime(d.Ticks - d.Ticks % TimeSpan.FromMinutes(5).Ticks, d.Kind);
 30    }
 31
 32    public async Task<T> GetOrAddAsync<T>(object key, TimeSpan ttl, int weight, Func<Task<T>> factory, CancellationToken
 33    {
 2734        var lazy = _cache.GetOrCreate(key, entry =>
 2735        {
 1936            entry.AbsoluteExpirationRelativeToNow = ttl;
 1937            entry.Size = weight;
 2738            // Общая загрузка не привязана к токену первого вызывающего: его отмена не должна ломать остальных.
 3839            return new Lazy<Task<T>>(() => factory(), LazyThreadSafetyMode.ExecutionAndPublication);
 2740        })!;
 41
 42        try
 43        {
 2744            return await lazy.Value.WaitAsync(ct);
 45        }
 146        catch (Exception) when (!ct.IsCancellationRequested)
 47        {
 148            _cache.Remove(key);   // ошибку не кэшируем
 149            throw;
 50        }
 2651    }
 52
 253    public void Dispose() => _cache.Dispose();
 54}