| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- using Application.Abstractions.Data;
- using Application.Abstractions.Paper;
- using Application.Helpers;
- using StackExchange.Redis;
- namespace Infrastructure.Paper;
- /// <summary>
- /// Redis Sorted Set 기반 모의투자 리더보드 캐시 (d4 M3 리더보드 백엔드 구현) — RedisChatLeaderboard 패턴 복제.
- /// (sort, period) 조합별 키. TTL 25h — 만료 시 다음 호출이 DB 지표 산출 후 재시드.
- /// return/winrate 는 ZREVRANGE(내림차순 = 높을수록 상위), mdd 는 ZRANGE(오름차순 = 낮을수록 상위).
- /// </summary>
- internal sealed class PaperLeaderboardCache(IConnectionMultiplexer redis) : IPaperLeaderboardCache
- {
- private static readonly TimeSpan CacheTtl = TimeSpan.FromHours(25);
- private static string Key(PaperLeaderboardSort sort, PaperLeaderboardPeriod period)
- {
- var s = sort switch
- {
- PaperLeaderboardSort.Return => "return",
- PaperLeaderboardSort.WinRate => "winrate",
- PaperLeaderboardSort.Mdd => "mdd",
- _ => "return"
- };
- var p = period switch
- {
- PaperLeaderboardPeriod.Week => "week",
- PaperLeaderboardPeriod.Month => "month",
- PaperLeaderboardPeriod.All => "all",
- _ => "all"
- };
- return $"paper-lb:{s}:{p}";
- }
- public async Task<IReadOnlyList<PaperLeaderboardRow>> GetPageAsync(
- PaperLeaderboardSort sort,
- PaperLeaderboardPeriod period,
- int offset,
- int count,
- IAppDbContext db,
- int minFillsForRank,
- CancellationToken ct = default)
- {
- var r = redis.GetDatabase();
- var key = Key(sort, period);
- await EnsureSeededAsync(r, key, sort, period, db, minFillsForRank, ct);
- var ascending = PaperLeaderboardMetrics.IsAscending(sort);
- var order = ascending ? Order.Ascending : Order.Descending;
- var stop = offset + count - 1;
- var entries = await r.SortedSetRangeByRankWithScoresAsync(key, offset, stop, order);
- var rows = new List<PaperLeaderboardRow>(entries.Length);
- for (var i = 0; i < entries.Length; i++)
- {
- if (int.TryParse((string?)entries[i].Element, out var memberID))
- {
- rows.Add(new PaperLeaderboardRow(memberID, (int)entries[i].Score, offset + i));
- }
- }
- return rows;
- }
- public async Task<PaperLeaderboardMyRank?> GetMyRankAsync(
- PaperLeaderboardSort sort,
- PaperLeaderboardPeriod period,
- int memberID,
- IAppDbContext db,
- int minFillsForRank,
- CancellationToken ct = default)
- {
- var r = redis.GetDatabase();
- var key = Key(sort, period);
- await EnsureSeededAsync(r, key, sort, period, db, minFillsForRank, ct);
- var member = (RedisValue)memberID.ToString();
- var score = await r.SortedSetScoreAsync(key, member);
- if (!score.HasValue)
- {
- return null;
- }
- var ascending = PaperLeaderboardMetrics.IsAscending(sort);
- var order = ascending ? Order.Ascending : Order.Descending;
- var rank = await r.SortedSetRankAsync(key, member, order);
- var total = await r.SortedSetLengthAsync(key);
- if (!rank.HasValue)
- {
- return null;
- }
- return new PaperLeaderboardMyRank(rank.Value, (int)score.Value, total);
- }
- /// <summary>캐시 미스 → DB 지표 산출 후 ZADD 재시드. TTL 동안 후속 호출은 Redis only.</summary>
- private static async Task EnsureSeededAsync(
- IDatabase r,
- string key,
- PaperLeaderboardSort sort,
- PaperLeaderboardPeriod period,
- IAppDbContext db,
- int minFillsForRank,
- CancellationToken ct)
- {
- if (await r.KeyExistsAsync(key))
- {
- return;
- }
- var metrics = await PaperLeaderboardMetrics.ComputeAllAsync(db, period, minFillsForRank, ct);
- if (metrics.Count == 0)
- {
- return;
- }
- var entries = metrics
- .Select(c => new SortedSetEntry(c.MemberID.ToString(), PaperLeaderboardMetrics.Score(c, sort)))
- .ToArray();
- await r.SortedSetAddAsync(key, entries);
- await r.KeyExpireAsync(key, CacheTtl);
- }
- }
|