Skip to content

Task 1-1: Idempotency Key cho Prize Allocation Messages

Phase: 1 - Idempotency Priority: High Module: batch, application-core Depends 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: PrizeAllocationData là 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.Default thì field sẽ là null khi 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: SendPrizeAllocationMessageService inject JmsTemplate để gửi message. Không cần inject thêm để set correlationId — chỉ thêm field khi build PrizeAllocationData.

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ặc StringRedisTemplate) qua constructor. Nếu listeners đã inject RedisTemplate cho purpose khác, tái sử dụng cùng bean. setIfAbsent = Redis SETNX — 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.correlationId có giá trị không null (auto-generated) sau khi build
  • [ ] SendPrizeAllocationMessageService set correlationId mỗi khi send
  • [ ] Gửi cùng correlationId 2 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.java
  • application-core/service/prize_allocation/SendPrizeAllocationMessageService.java
  • batch/jms/prize_allocation/JMSPrizeAllocationListener.java
  • batch/jms/prize_allocation/JMSPrizeForStreamerAllocationListener.java
  • batch/jms/prize_allocation/JMSUnlimitedPrizeAllocationListener.java
  • batch/jms/prize_allocation/JMSMissionAllocationListener.java
  • batch/jms/prize_allocation/JMSGachaAllocationListener.java
  • batch/jms/prize_allocation/JMSPresentBoxAllocationListener.java
  • batch/jms/prize_allocation/JMSMKPAllocationListener.java
  • batch/jms/prize_allocation/JMSPresaleAllocationListener.java
  • batch/jms/prize_allocation/JMSNoneAllocationListener.java