Task 2-2: queryGameResult Retry khi Game End Timeout
Phase: 2 Priority: Medium Module:
ConnectedClientDepends on: Không có Reference: docs/BountyHunter-ControlServer/details/feature-machine-management/SPEC.md
Background
Trong claw machine protocol, game kết thúc khi machine gửi game-end packet về ControlServer. Nếu packet này bị mất (network blip, machine crash ngay sau game end), gameIsStarted sẽ mãi là true và backend room sẽ stuck trong GAMING state vĩnh viễn — player không thể play lại, và operator phải manually reset state. queryGameResult() tồn tại trong code để hỏi lại máy về kết quả, nhưng hiện tại không có cơ chế tự động gọi nó khi timeout xảy ra.
Tasks
Note: Watchdog timer phải dùng
ScheduledExecutorService(không dùngTimer— deprecated, không handle exceptions well).ScheduledFuturereference phải được lưu lại để có thể cancel khi game kết thúc bình thường — nếu không cancel, watchdog sẽ fire ngay cả khi game đã xong.gameIsStartedflag phải làvolatileđể socket-reading thread và scheduler thread đọc được giá trị mới nhất.forceEndGame()phải idempotent — gọi nhiều lần không gây duplicate events.
- [ ] Thêm watchdog fields trong
ConnectedClient:
private volatile boolean gameIsStarted = false;
private ScheduledFuture<?> gameWatchdog;
// Inject hoặc dùng shared scheduler:
private final ScheduledExecutorService watchdogScheduler =
Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "game-watchdog-" + macIp);
t.setDaemon(true);
return t;
});
- [ ] Trong
handleGameStarted(): set flag và schedule watchdog:
private void handleGameStarted(GameStarted gameStarted) {
gameIsStarted = true;
int timeoutSeconds = gameStarted.getTimeoutInSeconds() + GAME_END_BUFFER_SECONDS; // +30s buffer
log.info("[{}] Game started, watchdog scheduled in {}s", macIp, timeoutSeconds);
gameWatchdog = watchdogScheduler.schedule(() -> {
checkAndRecoverGame(0); // attempt 0 = first check
}, timeoutSeconds, TimeUnit.SECONDS);
// ... existing countdown logic (Thread.sleep — see task-1-4 for fix)
}
- [ ] Implement
checkAndRecoverGame()với max 3 retries:
private void checkAndRecoverGame(int attempt) {
if (!gameIsStarted) return; // game already ended normally, nothing to do
if (attempt >= MAX_GAME_QUERY_RETRIES) { // default: 3
log.error("[{}] Game result not received after {} queries — forcing end game", macIp, MAX_GAME_QUERY_RETRIES);
forceEndGame();
return;
}
log.warn("[{}] Game result timeout (attempt {}/{}) — querying machine", macIp, attempt + 1, MAX_GAME_QUERY_RETRIES);
queryGameResult(); // send query packet to machine
// Schedule next check in QUERY_RETRY_INTERVAL_SECONDS
gameWatchdog = watchdogScheduler.schedule(() -> {
checkAndRecoverGame(attempt + 1);
}, QUERY_RETRY_INTERVAL_SECONDS, TimeUnit.SECONDS);
}
- [ ] Implement
forceEndGame():
private void forceEndGame() {
if (!gameIsStarted) return; // idempotent guard
gameIsStarted = false;
log.error("[{}] Force ending game — machine did not respond to game result queries", macIp);
try {
backendRMCClient.endGame(macIp, 0); // 0 = no prize (force lose)
} catch (Exception e) {
log.error("[{}] Failed to force end game on backend: {}", macIp, e.getMessage());
}
}
- [ ] Trong
handleGameEnded(): cancel watchdog và clear flag:
private void handleGameEnded(GameEnded gameEnded) {
gameIsStarted = false;
if (gameWatchdog != null && !gameWatchdog.isDone()) {
gameWatchdog.cancel(false); // false: don't interrupt if running
log.debug("[{}] Game ended normally — watchdog cancelled", macIp);
}
// ... existing logic
}
- [ ] Thêm config:
# Game watchdog configuration
game.end.buffer-seconds=30
game.end.query-retries=3
game.end.query-retry-interval-seconds=10
- [ ] Trong
close()method: shutdown watchdog scheduler:
watchdogScheduler.shutdownNow();
Verification / Acceptance Criteria
- [ ] Bình thường: game-end packet →
handleGameEnded()cancel watchdog → không cóqueryGameResult()call - [ ] Game result mất: watchdog fires →
queryGameResult()được gọi →handleQueryGameResultReport()→gameIsStarted = false, game resolved - [ ] Máy không trả lời 3 lần: sau 3 queries →
backendRMCClient.endGame(macIp, 0)được gọi đúng 1 lần - [ ]
forceEndGame()idempotent: gọi 2 lần →endGame()chỉ được gọi 1 lần - [ ]
gameIsStartedvolatile: đọc đúng giá trị từ watchdog thread sau khi được set bởi socket-reading thread
Files to Modify
src/main/java/.../ConnectedClient.javasrc/main/resources/config.properties