11package com .back .domain .matching .queue .store .redis ;
22
33import java .time .LocalDateTime ;
4+ import java .time .ZoneId ;
45import java .util .ArrayList ;
56import java .util .LinkedHashMap ;
67import java .util .List ;
@@ -105,12 +106,13 @@ public class RedisMatchStateStore implements MatchStateStore {
105106 -- MATCHING:MATCH_MARK_ACCEPT_PENDING
106107 local participantCount = tonumber(ARGV[3])
107108 redis.call('SET', KEYS[1], ARGV[1])
109+ redis.call('ZADD', KEYS[2], ARGV[4], ARGV[2])
108110
109111 for i = 1, participantCount do
110- redis.call('SET', KEYS[i + 1 ], ARGV[2])
112+ redis.call('SET', KEYS[i + 2 ], ARGV[2])
111113 end
112114
113- for i = participantCount + 2 , #KEYS do
115+ for i = participantCount + 3 , #KEYS do
114116 redis.call('DEL', KEYS[i])
115117 end
116118
@@ -268,6 +270,7 @@ public MatchSession markAcceptPending(QueueKey queueKey, List<WaitingUser> match
268270
269271 List <String > keys = new ArrayList <>();
270272 keys .add (MatchingRedisKeys .match (matchId ));
273+ keys .add (MatchingRedisKeys .matchDeadline ());
271274 participantIds .forEach (participantId -> keys .add (MatchingRedisKeys .userMatch (participantId )));
272275 participantIds .forEach (participantId -> keys .add (MatchingRedisKeys .userQueue (participantId )));
273276
@@ -276,7 +279,8 @@ public MatchSession markAcceptPending(QueueKey queueKey, List<WaitingUser> match
276279 keys ,
277280 sessionJson ,
278281 String .valueOf (matchId ),
279- String .valueOf (participantIds .size ()));
282+ String .valueOf (participantIds .size ()),
283+ String .valueOf (deadlineScore (deadline )));
280284
281285 if (storedJson == null || storedJson .isBlank ()) {
282286 throw new IllegalStateException ("Redis ready-check 세션 저장 결과를 확인할 수 없습니다." );
@@ -351,6 +355,7 @@ public RoomCreationAttempt tryBeginRoomCreation(Long matchId) {
351355 String updatedJson = serializer .writeMatchSession (roomCreatingSession );
352356
353357 if (compareAndSet (matchKey , currentJson , updatedJson )) {
358+ removeDeadlineIndex (matchId );
354359 return new RoomCreationAttempt (roomCreatingSession , true );
355360 }
356361 }
@@ -378,7 +383,7 @@ public MatchSession markRoomReady(Long matchId, Long roomId) {
378383 public MatchSession expire (Long matchId ) {
379384 return updateMatchSession (matchId , currentSession -> {
380385 if (currentSession .status () != MatchSessionStatus .ACCEPT_PENDING ) {
381- return currentSession ;
386+ throw new IllegalStateException ( "이미 만료 처리 대상이 아닌 매치 세션입니다." ) ;
382387 }
383388
384389 return currentSession .expired ();
@@ -461,31 +466,46 @@ public MatchSession findMatchSessionByUserId(Long userId) {
461466
462467 @ Override
463468 public List <Long > findExpiredAcceptPendingMatchIds (LocalDateTime now ) {
464- Set <String > keys = redisTemplate .keys (MatchingRedisKeys .matchPattern ());
469+ Set <String > candidates = redisTemplate
470+ .opsForZSet ()
471+ .rangeByScore (MatchingRedisKeys .matchDeadline (), Double .NEGATIVE_INFINITY , deadlineScore (now ));
465472
466- if (keys == null || keys .isEmpty ()) {
473+ if (candidates == null || candidates .isEmpty ()) {
467474 return List .of ();
468475 }
469476
470477 List <Long > expiredMatchIds = new ArrayList <>();
471478
472- // deadline index 도입 전까지는 match 세션 key 들을 임시로 pattern scan 하며 만료 대상을 찾는다 .
473- for (String key : keys ) {
474- Long matchId = extractMatchIdFromKey ( key );
479+ // Redis ZSET deadline index 는 후보만 좁혀주고, 최종 만료 여부는 세션 문서를 다시 읽어 확정한다 .
480+ for (String candidate : candidates ) {
481+ Long matchId = parseDeadlineMember ( candidate );
475482 if (matchId == null ) {
483+ redisTemplate .opsForZSet ().remove (MatchingRedisKeys .matchDeadline (), candidate );
476484 continue ;
477485 }
478486
487+ String key = MatchingRedisKeys .match (matchId );
479488 String sessionJson = redisTemplate .opsForValue ().get (key );
480489 if (sessionJson == null || sessionJson .isBlank ()) {
490+ removeDeadlineIndex (matchId );
481491 continue ;
482492 }
483493
484494 MatchSession matchSession = serializer .readMatchSession (sessionJson );
485- if (matchSession .status () == MatchSessionStatus .ACCEPT_PENDING
486- && matchSession .deadline () != null
487- && !matchSession .deadline ().isAfter (now )) {
495+ if (matchSession .status () != MatchSessionStatus .ACCEPT_PENDING ) {
496+ removeDeadlineIndex (matchId );
497+ continue ;
498+ }
499+
500+ if (matchSession .deadline () == null ) {
501+ removeDeadlineIndex (matchId );
502+ continue ;
503+ }
504+
505+ if (!matchSession .deadline ().isAfter (now )) {
488506 expiredMatchIds .add (matchSession .matchId ());
507+ } else {
508+ addDeadlineIndex (matchSession );
489509 }
490510 }
491511
@@ -500,6 +520,7 @@ public void clearTerminalMatch(Long matchId) {
500520
501521 if (matchJson == null || matchJson .isBlank ()) {
502522 removeUserMatchLinksByScan (matchId );
523+ removeDeadlineIndex (matchId );
503524 return ;
504525 }
505526
@@ -509,6 +530,7 @@ public void clearTerminalMatch(Long matchId) {
509530 matchSession .participantIds ().forEach (participantId -> keys .add (MatchingRedisKeys .userMatch (participantId )));
510531
511532 redisTemplate .execute (CLEAR_TERMINAL_SCRIPT , keys , String .valueOf (matchId ));
533+ removeDeadlineIndex (matchId );
512534 }
513535
514536 @ Override
@@ -547,6 +569,7 @@ public void clearMatchedRoom(Long userId, Long roomId) {
547569
548570 if (!hasRemainingReference ) {
549571 compareAndDelete (matchKey , matchJson );
572+ removeDeadlineIndex (matchId );
550573 }
551574 }
552575
@@ -564,6 +587,7 @@ private MatchSession updateMatchSession(Long matchId, Function<MatchSession, Mat
564587
565588 String updatedJson = serializer .writeMatchSession (updatedSession );
566589 if (compareAndSet (matchKey , currentJson , updatedJson )) {
590+ syncDeadlineIndexAfterUpdate (currentSession , updatedSession );
567591 return updatedSession ;
568592 }
569593 }
@@ -652,17 +676,45 @@ private Long parseMatchIdValue(String value) {
652676 }
653677 }
654678
655- private Long extractMatchIdFromKey (String key ) {
656- if (key == null || !key .startsWith (MatchingRedisKeys .matchPrefix ())) {
679+ private Long parseDeadlineMember (String value ) {
680+ try {
681+ return Long .parseLong (value );
682+ } catch (NumberFormatException e ) {
657683 return null ;
658684 }
685+ }
659686
660- String suffix = key .substring (MatchingRedisKeys .matchPrefix ().length ());
661- if (suffix .isBlank () || !suffix .chars ().allMatch (Character ::isDigit )) {
662- return null ;
687+ private void syncDeadlineIndexAfterUpdate (MatchSession previousSession , MatchSession updatedSession ) {
688+ if (updatedSession .status () == MatchSessionStatus .ACCEPT_PENDING && updatedSession .deadline () != null ) {
689+ addDeadlineIndex (updatedSession );
690+ return ;
691+ }
692+
693+ // ACCEPT_PENDING 을 벗어난 세션은 더 이상 스케줄러 만료 후보가 아니므로 deadline index 에서 제거한다.
694+ if (previousSession .status () == MatchSessionStatus .ACCEPT_PENDING ) {
695+ removeDeadlineIndex (updatedSession .matchId ());
696+ }
697+ }
698+
699+ private void addDeadlineIndex (MatchSession matchSession ) {
700+ if (matchSession .deadline () == null ) {
701+ return ;
663702 }
664703
665- return Long .parseLong (suffix );
704+ redisTemplate
705+ .opsForZSet ()
706+ .add (
707+ MatchingRedisKeys .matchDeadline (),
708+ String .valueOf (matchSession .matchId ()),
709+ deadlineScore (matchSession .deadline ()));
710+ }
711+
712+ private void removeDeadlineIndex (Long matchId ) {
713+ redisTemplate .opsForZSet ().remove (MatchingRedisKeys .matchDeadline (), String .valueOf (matchId ));
714+ }
715+
716+ private double deadlineScore (LocalDateTime deadline ) {
717+ return deadline .atZone (ZoneId .systemDefault ()).toInstant ().toEpochMilli ();
666718 }
667719
668720 @ SuppressWarnings ("unchecked" )
0 commit comments