PaperLeaderboardCache.cs 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Paper;
  3. using Application.Helpers;
  4. using StackExchange.Redis;
  5. namespace Infrastructure.Paper;
  6. /// <summary>
  7. /// Redis Sorted Set 기반 모의투자 리더보드 캐시 (d4 M3 리더보드 백엔드 구현) — RedisChatLeaderboard 패턴 복제.
  8. /// (sort, period) 조합별 키. TTL 25h — 만료 시 다음 호출이 DB 지표 산출 후 재시드.
  9. /// return/winrate 는 ZREVRANGE(내림차순 = 높을수록 상위), mdd 는 ZRANGE(오름차순 = 낮을수록 상위).
  10. /// </summary>
  11. internal sealed class PaperLeaderboardCache(IConnectionMultiplexer redis) : IPaperLeaderboardCache
  12. {
  13. private static readonly TimeSpan CacheTtl = TimeSpan.FromHours(25);
  14. private static string Key(PaperLeaderboardSort sort, PaperLeaderboardPeriod period)
  15. {
  16. var s = sort switch
  17. {
  18. PaperLeaderboardSort.Return => "return",
  19. PaperLeaderboardSort.WinRate => "winrate",
  20. PaperLeaderboardSort.Mdd => "mdd",
  21. _ => "return"
  22. };
  23. var p = period switch
  24. {
  25. PaperLeaderboardPeriod.Week => "week",
  26. PaperLeaderboardPeriod.Month => "month",
  27. PaperLeaderboardPeriod.All => "all",
  28. _ => "all"
  29. };
  30. return $"paper-lb:{s}:{p}";
  31. }
  32. public async Task<IReadOnlyList<PaperLeaderboardRow>> GetPageAsync(
  33. PaperLeaderboardSort sort,
  34. PaperLeaderboardPeriod period,
  35. int offset,
  36. int count,
  37. IAppDbContext db,
  38. int minFillsForRank,
  39. CancellationToken ct = default)
  40. {
  41. var r = redis.GetDatabase();
  42. var key = Key(sort, period);
  43. await EnsureSeededAsync(r, key, sort, period, db, minFillsForRank, ct);
  44. var ascending = PaperLeaderboardMetrics.IsAscending(sort);
  45. var order = ascending ? Order.Ascending : Order.Descending;
  46. var stop = offset + count - 1;
  47. var entries = await r.SortedSetRangeByRankWithScoresAsync(key, offset, stop, order);
  48. var rows = new List<PaperLeaderboardRow>(entries.Length);
  49. for (var i = 0; i < entries.Length; i++)
  50. {
  51. if (int.TryParse((string?)entries[i].Element, out var memberID))
  52. {
  53. rows.Add(new PaperLeaderboardRow(memberID, (int)entries[i].Score, offset + i));
  54. }
  55. }
  56. return rows;
  57. }
  58. public async Task<PaperLeaderboardMyRank?> GetMyRankAsync(
  59. PaperLeaderboardSort sort,
  60. PaperLeaderboardPeriod period,
  61. int memberID,
  62. IAppDbContext db,
  63. int minFillsForRank,
  64. CancellationToken ct = default)
  65. {
  66. var r = redis.GetDatabase();
  67. var key = Key(sort, period);
  68. await EnsureSeededAsync(r, key, sort, period, db, minFillsForRank, ct);
  69. var member = (RedisValue)memberID.ToString();
  70. var score = await r.SortedSetScoreAsync(key, member);
  71. if (!score.HasValue)
  72. {
  73. return null;
  74. }
  75. var ascending = PaperLeaderboardMetrics.IsAscending(sort);
  76. var order = ascending ? Order.Ascending : Order.Descending;
  77. var rank = await r.SortedSetRankAsync(key, member, order);
  78. var total = await r.SortedSetLengthAsync(key);
  79. if (!rank.HasValue)
  80. {
  81. return null;
  82. }
  83. return new PaperLeaderboardMyRank(rank.Value, (int)score.Value, total);
  84. }
  85. /// <summary>전체 (sort, period) 키 삭제 — 다음 조회가 최신 스냅샷으로 재시드(스냅샷 배치 직후 warm-refresh).</summary>
  86. public async Task InvalidateAllAsync(CancellationToken ct = default)
  87. {
  88. var r = redis.GetDatabase();
  89. foreach (var sort in Enum.GetValues<PaperLeaderboardSort>())
  90. {
  91. foreach (var period in Enum.GetValues<PaperLeaderboardPeriod>())
  92. {
  93. await r.KeyDeleteAsync(Key(sort, period));
  94. }
  95. }
  96. }
  97. /// <summary>캐시 미스 → DB 지표 산출 후 ZADD 재시드. TTL 동안 후속 호출은 Redis only.</summary>
  98. private static async Task EnsureSeededAsync(
  99. IDatabase r,
  100. string key,
  101. PaperLeaderboardSort sort,
  102. PaperLeaderboardPeriod period,
  103. IAppDbContext db,
  104. int minFillsForRank,
  105. CancellationToken ct)
  106. {
  107. if (await r.KeyExistsAsync(key))
  108. {
  109. return;
  110. }
  111. var metrics = await PaperLeaderboardMetrics.ComputeAllAsync(db, period, minFillsForRank, ct);
  112. if (metrics.Count == 0)
  113. {
  114. return;
  115. }
  116. var entries = metrics
  117. .Select(c => new SortedSetEntry(c.MemberID.ToString(), PaperLeaderboardMetrics.Score(c, sort)))
  118. .ToArray();
  119. await r.SortedSetAddAsync(key, entries);
  120. await r.KeyExpireAsync(key, CacheTtl);
  121. }
  122. }