Skip to content

Task 2-2: queryGameResult Retry khi Game End Timeout

Phase: 2 Priority: Medium Module: ConnectedClient Depends 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ùng Timer — deprecated, không handle exceptions well). ScheduledFuture reference 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. gameIsStarted flag 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
  • [ ] gameIsStarted volatile: đọc đúng giá trị từ watchdog thread sau khi được set bởi socket-reading thread

Files to Modify

  • src/main/java/.../ConnectedClient.java
  • src/main/resources/config.properties