| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114 |
- using Application.Abstractions.Cache;
- using Application.Abstractions.Hub;
- using StackExchange.Redis;
- namespace Infrastructure.Hubs;
- /// <summary>
- /// SignalR 멤버 접속 추적 (Redis 기반).
- /// `member:connections:{memberID}` set에 connectionId 저장 → refcount 패턴.
- /// 첫 connection 진입 시 `member:online` set에 추가, 마지막 disconnect 시 제거.
- /// </summary>
- internal sealed class PresenceTracker(IConnectionMultiplexer redis) : IPresenceTracker
- {
- public async Task<bool> AddConnectionAsync(int memberID, string connectionId)
- {
- var db = redis.GetDatabase();
- var key = CacheKeys.MemberConnections(memberID);
- var added = await db.SetAddAsync(key, connectionId);
- if (!added)
- {
- // 동일 connectionId 중복 등록 — 변화 없음
- return false;
- }
- // 항상 MemberOnlineSet 반영. SADD 의 set semantics 로 idempotent.
- // 이전 버전은 totalConnections==1 가드를 두었으나, Web.Api 비정상 종료로 남은
- // 고아 connectionId 가 set 에 있을 때 신규 connection 이 와도 online set 에 반영되지 않는 버그가 있었음.
- var wasNewlyOnline = await db.SetAddAsync(CacheKeys.MemberOnlineSet, memberID);
- return wasNewlyOnline; // true = 0→1 전환 (online set 에 새로 추가됨)
- }
- public async Task ClearAllAsync()
- {
- // Web.Api 재시작 시 stale presence 데이터 정리. 모든 client 는 reconnect 시 OnConnected 로 재등록됨.
- var db = redis.GetDatabase();
- var endpoints = redis.GetEndPoints();
- foreach (var endpoint in endpoints)
- {
- var server = redis.GetServer(endpoint);
- if (!server.IsConnected || server.IsReplica)
- {
- continue;
- }
- // member:connections:* 패턴 일괄 삭제
- await foreach (var key in server.KeysAsync(database: db.Database, pattern: "member:connections:*"))
- {
- await db.KeyDeleteAsync(key);
- }
- }
- // member:online set 전체 삭제
- await db.KeyDeleteAsync(CacheKeys.MemberOnlineSet);
- }
- public async Task<bool> RemoveConnectionAsync(int memberID, string connectionId)
- {
- var db = redis.GetDatabase();
- var key = CacheKeys.MemberConnections(memberID);
- var removed = await db.SetRemoveAsync(key, connectionId);
- if (!removed)
- {
- return false;
- }
- // 마지막 connection이면 online set에서 제거 + 빈 set 삭제
- var remaining = await db.SetLengthAsync(key);
- if (remaining == 0)
- {
- await db.KeyDeleteAsync(key);
- await db.SetRemoveAsync(CacheKeys.MemberOnlineSet, memberID);
- return true; // 1→0 전환
- }
- return false;
- }
- public async Task<bool> IsOnlineAsync(int memberID)
- {
- var db = redis.GetDatabase();
- return await db.SetContainsAsync(CacheKeys.MemberOnlineSet, memberID);
- }
- public async Task<HashSet<int>> GetOnlineMembersAsync(IEnumerable<int> memberIDs)
- {
- var ids = memberIDs.Distinct().ToArray();
- if (ids.Length == 0)
- {
- return [];
- }
- var db = redis.GetDatabase();
- var values = ids.Select(id => (RedisValue)id).ToArray();
- var results = await db.SetContainsAsync(CacheKeys.MemberOnlineSet, values);
- var online = new HashSet<int>();
- for (int i = 0; i < ids.Length; i++)
- {
- if (results[i])
- {
- online.Add(ids[i]);
- }
- }
- return online;
- }
- }
|