Skip to content

Task 1-2: JMS Retry khi Send thất bại

Phase: 1 Priority: Medium Module: SendHealthCheck, SendGamePlay Depends on: Không có Reference: docs/BountyHunter-ControlServer/details/feature-machine-management/SPEC.md

Background

PLAN.md đề cập retry logic khi gửi JMS thất bại. Hiện tại nếu ActiveMQ broker tạm thời không khả dụng (restart, network blip), các events từ SendHealthCheckSendGamePlay sẽ bị mất hoàn toàn — không có retry, không có dead letter queue handling ở application level. Kết quả: backend không biết machine status (health check lost) hoặc game events bị drop, gây inconsistency giữa ControlServer và backend state.

Tasks

Note: Spring Retry (@Retryable) yêu cầu @EnableRetry trên @Configuration class và spring-retry dependency. Nếu không muốn thêm dependency, manual retry với exponential backoff là cách đơn giản và kiểm soát được hơn. Thread.sleep() trong retry loop phải handle InterruptedException bằng cách restore interrupt flag. Tránh dùng Thread.sleep() trên connection-reading thread — retry nên xảy ra trên JMS sender thread riêng.

  • [ ] Kiểm tra build.gradle hoặc pom.xml xem có spring-retry chưa. Nếu có, dùng @Retryable. Nếu không, implement manual retry:
// Option A: spring-retry (nếu dependency đã có)
@Retryable(
    value = {JmsException.class},
    maxAttempts = 3,
    backoff = @Backoff(delay = 1000, multiplier = 2)  // 1s, 2s
)
public void sendHealthCheck(String macIp, int status) {
    jmsTemplate.convertAndSend(healthCheckQueue, buildMessage(macIp, status));
}

@Recover
public void recoverHealthCheck(JmsException e, String macIp, int status) {
    log.error("[{}] Failed to send health check after 3 attempts: {}", macIp, e.getMessage());
}
// Option B: Manual retry với exponential backoff (không cần extra dependency)
private void sendWithRetry(String queue, Object message, String contextDescription) {
    int maxAttempts = maxRetryAttempts; // từ config
    long delayMs = retryInitialDelayMs; // từ config

    for (int attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
            jmsTemplate.convertAndSend(queue, message);
            return; // success
        } catch (JmsException e) {
            if (attempt == maxAttempts) {
                log.error("Failed to send {} after {} attempts: {}", contextDescription, maxAttempts, e.getMessage());
                throw e;
            }
            log.warn("Retry {}/{} for {}: {}", attempt, maxAttempts, contextDescription, e.getMessage());
            try {
                Thread.sleep(delayMs);
                delayMs *= 2; // exponential backoff: 1s, 2s, 4s
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();  // restore interrupt flag
                throw new RuntimeException("Retry interrupted", ie);
            }
        }
    }
}
  • [ ] Thêm config vào config.properties:
# JMS retry configuration
jms.retry.max-attempts=3
jms.retry.initial-delay-ms=1000
  • [ ] Update SendHealthCheck.send() để dùng retry mechanism
  • [ ] Update SendGamePlay.send() (hoặc sendGameplayEvent()) để dùng retry mechanism
  • [ ] Audit các JMS send calls trong jms_factory/ package để xác nhận tất cả đều được covered
  • [ ] Log WARN khi retry, log ERROR khi tất cả retries thất bại (không throw unchecked exception lên caller nếu caller không handle được)

Verification / Acceptance Criteria

  • [ ] Mock JmsTemplate throw JmsException 2 lần, succeed lần thứ 3 → WARN log 2 lần, message được gửi thành công, không có ERROR log
  • [ ] Mock JmsTemplate throw JmsException 3 lần → ERROR log "after 3 attempts", no crash (server tiếp tục chạy)
  • [ ] Delay giữa retries tăng theo exponential backoff: 1s → 2s (verify với mock Thread.sleep hoặc Clock)
  • [ ] jms.retry.max-attempts=5 trong config → retry 5 lần, không phải hardcoded 3
  • [ ] Unit test SendHealthCheckTest: verify retry count và delay với mock JmsTemplate

Files to Modify

  • src/main/java/.../SendHealthCheck.java
  • src/main/java/.../SendGamePlay.java
  • src/main/java/.../jms_factory/ (các files liên quan nếu có)
  • src/main/resources/config.properties
  • build.gradle hoặc pom.xml (nếu cần thêm spring-retry)