Task 1-1: Idempotency Key cho Prize Allocation Messages
Phase: 1 - Idempotency Priority: High Module:
batch,application-coreDepends on: Không có Reference: docs/BountyHunter-Backend/details/feature-prize-allocation/SPEC.md
Background
Khi JMS redelivers message (broker restart, consumer crash), listener xử lý cùng message 2 lần → allocation/rollback bị double. PRIZE queue có concurrency=1-1 nhưng không tránh được redelivery.
Tasks
1. Thêm correlationId vào PrizeAllocationData
DI Note:
PrizeAllocationDatalà plain data class (POJO / record / Lombok@Builder). Không cần Spring injection —UUID.randomUUID()là static call. Đảm bảo@Builder.Defaultđược dùng nếu class dùng Lombok@Builder(không dùng@Builder.Defaultthì field sẽ lànullkhi build không set).
File: application-core/service/prize_allocation/PrizeAllocationData.java
public class PrizeAllocationData {
// ... existing fields ...
/** Idempotency key: unique per allocation attempt. Auto-generated if not set. */
@Builder.Default
private String correlationId = UUID.randomUUID().toString();
}
2. Producer set correlationId
DI Note:
SendPrizeAllocationMessageServiceinjectJmsTemplateđể gửi message. Không cần inject thêm để setcorrelationId— chỉ thêm field khi buildPrizeAllocationData.
File: application-core/service/prize_allocation/SendPrizeAllocationMessageService.java
PrizeAllocationData data = PrizeAllocationData.builder()
.allocationType(allocationType)
.actionType(actionType)
.createRoomData(createRoomData)
.correlationId(UUID.randomUUID().toString()) // unique per send
.build();
3. Listener check idempotency
DI Note: Tất cả 9 listeners cần inject
RedisTemplate<String, String>(hoặcStringRedisTemplate) qua constructor. Nếu listeners đã injectRedisTemplatecho purpose khác, tái sử dụng cùng bean.setIfAbsent= RedisSETNX— atomic, thread-safe.
File: batch/jms/prize_allocation/JMSPrizeAllocationListener.java (áp dụng cho tất cả 9 listeners)
private static final String IDEMPOTENCY_KEY_PREFIX = "prize-alloc:processed:";
private static final long IDEMPOTENCY_TTL_SECONDS = 3600; // 1 giờ đủ cho JMS redelivery window
@JmsListener(...)
public void onMessage(String messageJson) {
PrizeAllocationData data = convertMessage(messageJson, PrizeAllocationData.class);
String idemKey = IDEMPOTENCY_KEY_PREFIX + data.getCorrelationId();
Boolean isNew = redisTemplate.opsForValue()
.setIfAbsent(idemKey, "1", IDEMPOTENCY_TTL_SECONDS, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(isNew)) {
LOGGER.warn("[IDEMPOTENCY] Duplicate message correlationId={}, skipping", data.getCorrelationId());
return; // ACK message (không throw), nhưng không process
}
// ... existing logic ...
}
Verification / Acceptance Criteria
- [ ]
PrizeAllocationData.correlationIdcó giá trị không null (auto-generated) sau khi build - [ ]
SendPrizeAllocationMessageServicesetcorrelationIdmỗi khi send - [ ] Gửi cùng
correlationId2 lần tới bất kỳ queue nào → chỉ xử lý lần đầu - [ ] Lần 2 log
[IDEMPOTENCY] Duplicate message correlationId=... - [ ] Message bị skip vẫn được ACK (không requeue vô hạn)
- [ ] Redis key expire sau 1 giờ (TTL đúng)
Notes
- TTL 1 giờ đủ cho JMS redelivery window mặc định
- Test case: send cùng correlationId 2 lần → chỉ xử lý 1 lần
Files to Modify
application-core/service/prize_allocation/PrizeAllocationData.javaapplication-core/service/prize_allocation/SendPrizeAllocationMessageService.javabatch/jms/prize_allocation/JMSPrizeAllocationListener.javabatch/jms/prize_allocation/JMSPrizeForStreamerAllocationListener.javabatch/jms/prize_allocation/JMSUnlimitedPrizeAllocationListener.javabatch/jms/prize_allocation/JMSMissionAllocationListener.javabatch/jms/prize_allocation/JMSGachaAllocationListener.javabatch/jms/prize_allocation/JMSPresentBoxAllocationListener.javabatch/jms/prize_allocation/JMSMKPAllocationListener.javabatch/jms/prize_allocation/JMSPresaleAllocationListener.javabatch/jms/prize_allocation/JMSNoneAllocationListener.java