Fix: 로딩 중 강제 종료 시 stale waiting room 자동 정리
로딩 화면에서 강제 종료하면 BossRoom이 waiting 상태로 남아 재입장이 영구 차단되는 문제 수정. - waiting 상태 2분 초과 BossRoom 자동 정리 (15초 주기) - RequestEntry 시 해당 유저의 stale waiting room 선제 정리 - 연결된 RoomSlot도 idle로 리셋 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,18 @@ 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) {
|
||||
escaped := strings.NewReplacer("%", "\\%", "_", "\\_").Replace(username)
|
||||
search := `"` + escaped + `"`
|
||||
var rooms []BossRoom
|
||||
err := r.db.Where("status = ? AND players LIKE ? AND created_at < ?",
|
||||
StatusWaiting, "%"+search+"%", threshold).
|
||||
Find(&rooms).Error
|
||||
return rooms, err
|
||||
}
|
||||
|
||||
// --- DedicatedServer & RoomSlot ---
|
||||
|
||||
// UpsertDedicatedServer creates or updates a server group by name.
|
||||
@@ -213,6 +225,40 @@ func (r *Repository) DeleteRoomBySessionName(sessionName string) error {
|
||||
return r.db.Unscoped().Where("session_name = ?", sessionName).Delete(&BossRoom{}).Error
|
||||
}
|
||||
|
||||
// CleanupStaleWaitingRooms deletes BossRoom records stuck in "waiting" status
|
||||
// past the given threshold and resets their associated RoomSlots to idle.
|
||||
// This handles cases where players disconnect during loading before the Fusion session starts.
|
||||
func (r *Repository) CleanupStaleWaitingRooms(threshold time.Time) (int64, error) {
|
||||
// 1. waiting 상태에서 threshold보다 오래된 방 조회
|
||||
var staleRooms []BossRoom
|
||||
if err := r.db.Where("status = ? AND created_at < ?", StatusWaiting, threshold).
|
||||
Find(&staleRooms).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(staleRooms) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// 2. 연결된 슬롯을 idle로 리셋
|
||||
staleSessionNames := make([]string, len(staleRooms))
|
||||
for i, room := range staleRooms {
|
||||
staleSessionNames[i] = room.SessionName
|
||||
}
|
||||
r.db.Model(&RoomSlot{}).
|
||||
Where("session_name IN ? AND status = ?", staleSessionNames, SlotWaiting).
|
||||
Updates(map[string]interface{}{
|
||||
"status": SlotIdle,
|
||||
"boss_room_id": nil,
|
||||
})
|
||||
|
||||
// 3. BossRoom 레코드 하드 삭제
|
||||
result := r.db.Unscoped().
|
||||
Where("status = ? AND created_at < ?", StatusWaiting, threshold).
|
||||
Delete(&BossRoom{})
|
||||
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
|
||||
// ResetStaleSlots clears instanceID for slots with stale heartbeats
|
||||
// and resets any active raids on those slots.
|
||||
func (r *Repository) ResetStaleSlots(threshold time.Time) (int64, error) {
|
||||
|
||||
@@ -22,6 +22,9 @@ const (
|
||||
entryTokenPrefix = "bossraid:entry:"
|
||||
// pendingEntryPrefix is the Redis key prefix for username → {sessionName, entryToken}.
|
||||
pendingEntryPrefix = "bossraid:pending:"
|
||||
// waitingRoomTimeout is the maximum time a room can stay in "waiting" status
|
||||
// before being considered stale and cleaned up. Covers loading + Fusion connection + retries.
|
||||
waitingRoomTimeout = 2 * time.Minute
|
||||
)
|
||||
|
||||
// entryTokenData is stored in Redis for each entry token.
|
||||
@@ -56,8 +59,10 @@ func (s *Service) SetExpGranter(fn func(username string, exp int) error) {
|
||||
// Allocates an idle room slot from a registered dedicated server.
|
||||
// Returns the room with assigned session name.
|
||||
func (s *Service) RequestEntry(usernames []string, bossID int) (*BossRoom, error) {
|
||||
// 좀비 슬롯 정리 — idle 슬롯 검색 전에 stale 인스턴스를 리셋
|
||||
// 좀비 슬롯 정리 — idle 슬롯 검색 전에 stale 인스턴스와 대기방을 리셋
|
||||
s.CheckStaleSlots()
|
||||
// 입장 요청 플레이어들의 stale waiting room 선제 정리
|
||||
s.cleanupStaleWaitingForUsers(usernames)
|
||||
|
||||
if len(usernames) == 0 {
|
||||
return nil, fmt.Errorf("플레이어 목록이 비어있습니다")
|
||||
@@ -408,6 +413,26 @@ func (s *Service) RequestEntryWithTokens(usernames []string, bossID int) (*BossR
|
||||
return room, tokens, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (s *Service) cleanupStaleWaitingForUsers(usernames []string) {
|
||||
threshold := time.Now().Add(-waitingRoomTimeout)
|
||||
for _, username := range usernames {
|
||||
rooms, err := s.repo.FindStaleWaitingRoomsByUsername(username, threshold)
|
||||
if err != nil || len(rooms) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, room := range rooms {
|
||||
log.Printf("stale 대기방 정리: session=%s, player=%s", room.SessionName, username)
|
||||
if err := s.repo.DeleteRoomBySessionName(room.SessionName); err != nil {
|
||||
log.Printf("stale 대기방 삭제 실패: %v", err)
|
||||
}
|
||||
_ = s.repo.ResetRoomSlot(room.SessionName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Dedicated Server Management ---
|
||||
|
||||
const staleTimeout = 30 * time.Second
|
||||
@@ -458,7 +483,8 @@ func (s *Service) Heartbeat(instanceID string) error {
|
||||
return s.repo.UpdateHeartbeat(instanceID)
|
||||
}
|
||||
|
||||
// CheckStaleSlots resets slots whose instances have gone silent.
|
||||
// CheckStaleSlots resets slots whose instances have gone silent
|
||||
// and cleans up waiting rooms that have exceeded the timeout.
|
||||
func (s *Service) CheckStaleSlots() {
|
||||
threshold := time.Now().Add(-staleTimeout)
|
||||
count, err := s.repo.ResetStaleSlots(threshold)
|
||||
@@ -469,6 +495,17 @@ func (s *Service) CheckStaleSlots() {
|
||||
if count > 0 {
|
||||
log.Printf("스태일 슬롯 %d개 리셋", count)
|
||||
}
|
||||
|
||||
// waiting 상태로 너무 오래 머문 방 정리 (로딩 중 강제 종료 등)
|
||||
waitingThreshold := time.Now().Add(-waitingRoomTimeout)
|
||||
cleaned, err := s.repo.CleanupStaleWaitingRooms(waitingThreshold)
|
||||
if err != nil {
|
||||
log.Printf("스태일 대기방 정리 실패: %v", err)
|
||||
return
|
||||
}
|
||||
if cleaned > 0 {
|
||||
log.Printf("스태일 대기방 %d개 정리", cleaned)
|
||||
}
|
||||
}
|
||||
|
||||
// ResetRoom resets a room slot back to idle and cleans up any lingering BossRoom records.
|
||||
|
||||
Reference in New Issue
Block a user