Task 1-2: JMS Retry khi Send thất bại
Phase: 1 Priority: Medium Module:
SendHealthCheck,SendGamePlayDepends 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ừ SendHealthCheck và SendGamePlay 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@EnableRetrytrên@Configurationclass vàspring-retrydependency. 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 handleInterruptedExceptionbằng cách restore interrupt flag. Tránh dùngThread.sleep()trên connection-reading thread — retry nên xảy ra trên JMS sender thread riêng.
- [ ] Kiểm tra
build.gradlehoặcpom.xmlxem cóspring-retrychư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ặcsendGameplayEvent()) để 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
JmsTemplatethrowJmsException2 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
JmsTemplatethrowJmsException3 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.sleephoặcClock) - [ ]
jms.retry.max-attempts=5trong 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.javasrc/main/java/.../SendGamePlay.javasrc/main/java/.../jms_factory/(các files liên quan nếu có)src/main/resources/config.propertiesbuild.gradlehoặcpom.xml(nếu cần thêmspring-retry)