Fix: entry token 만료 기반으로 stale waiting room 판단
All checks were successful
Server CI/CD / lint-and-build (push) Successful in 35s
Server CI/CD / deploy (push) Successful in 56s

타임아웃 대신 Redis entry token 존재 여부로 판단:
- pending token이 Redis에 남아있음 → 정상 로딩 중, 방 보존
- pending token이 없음 (만료/소비) → abandoned 확정, 즉시 정리

정상 로딩 중인 파티원의 방을 보호하면서도
강제 종료 유저의 즉각적 복구 가능.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 11:16:28 +09:00
parent 7ece5f3c44
commit 4c367e84ad
2 changed files with 20 additions and 11 deletions

View File

@@ -64,14 +64,13 @@ func (r *Repository) CountActiveByUsername(username string) (int64, error) {
return count, err
}
// FindStaleWaitingRoomsByUsername returns waiting rooms containing the given username
// that were created before the threshold.
func (r *Repository) FindStaleWaitingRoomsByUsername(username string, threshold time.Time) ([]BossRoom, error) {
// FindWaitingRoomsByUsername returns all waiting rooms containing the given username.
func (r *Repository) FindWaitingRoomsByUsername(username string) ([]BossRoom, error) {
escaped := strings.NewReplacer("%", "\\%", "_", "\\_").Replace(username)
search := `"` + escaped + `"`
var rooms []BossRoom
err := r.db.Where("status = ? AND players LIKE ? AND created_at < ?",
StatusWaiting, "%"+search+"%", threshold).
err := r.db.Where("status = ? AND players LIKE ?",
StatusWaiting, "%"+search+"%").
Find(&rooms).Error
return rooms, err
}

View File

@@ -414,19 +414,29 @@ func (s *Service) RequestEntryWithTokens(usernames []string, bossID int) (*BossR
}
// cleanupStaleWaitingForUsers checks if any of the given users are stuck in
// a stale waiting room (older than waitingRoomTimeout) and cleans them up.
// This provides instant resolution for players who force-quit during loading.
// a waiting room whose entry token has already expired or been consumed.
// If the pending token is gone from Redis, the room is abandoned and safe to remove.
// If the token still exists, the room may have active loading players — leave it alone.
func (s *Service) cleanupStaleWaitingForUsers(usernames []string) {
threshold := time.Now().Add(-waitingRoomTimeout)
ctx := context.Background()
for _, username := range usernames {
rooms, err := s.repo.FindStaleWaitingRoomsByUsername(username, threshold)
rooms, err := s.repo.FindWaitingRoomsByUsername(username)
if err != nil || len(rooms) == 0 {
continue
}
// pending entry token이 Redis에 남아있으면 정상 로딩 중일 수 있음 → 보존
pendingKey := pendingEntryPrefix + username
exists, _ := s.rdb.Exists(ctx, pendingKey).Result()
if exists > 0 {
continue
}
// 토큰 만료/소비됨 → 방 abandoned 확정, 정리
for _, room := range rooms {
log.Printf("stale 대기방 정리: session=%s, player=%s", room.SessionName, username)
log.Printf("abandoned 대기방 정리 (토큰 만료): session=%s, player=%s", room.SessionName, username)
if err := s.repo.DeleteRoomBySessionName(room.SessionName); err != nil {
log.Printf("stale 대기방 삭제 실패: %v", err)
log.Printf("대기방 삭제 실패: %v", err)
}
_ = s.repo.ResetRoomSlot(room.SessionName)
}