Skip to content

Thiết kế: Prize Allocation Feature

1. Tổng quan kiến trúc

flowchart TB
    subgraph Producers["Producers (websocket + web modules)"]
        CRS["CreateRoomService"]
        USS["UserMatchingService (via websocket)"]
        OPS["OpponentPoolService (via websocket)"]
        OtherSvc["MissionService / GachaService / ..."]
    end

    subgraph Routing["SendPrizeAllocationMessageService"]
        SEND["buildMessage(data)\n+ route to queue by allocationType"]
    end

    subgraph Queues["ActiveMQ Artemis - Prize Allocation Queues"]
        Q_P["queue-prize-allocation-PRIZE\nconcurrency: 1-1"]
        Q_PS["queue-prize-allocation-PRIZE_FOR_STREAMER\nconcurrency: 1-1"]
        Q_UN["queue-prize-allocation-UNLIMITED\nconcurrency: 20-100"]
        Q_M["queue-prize-allocation-MISSION"]
        Q_G["queue-prize-allocation-GACHA"]
        Q_PB["queue-prize-allocation-PRESENT_BOX"]
        Q_MKP["queue-prize-allocation-MKP"]
        Q_PRE["queue-prize-allocation-PRESALE"]
        Q_NONE["queue-prize-allocation-NONE"]
    end

    subgraph Consumers["batch module - JMS Listeners"]
        L_P["JMSPrizeAllocationListener"]
        L_PS["JMSPrizeForStreamerAllocationListener"]
        L_UN["JMSUnlimitedPrizeAllocationListener"]
        L_M["JMSMissionAllocationListener"]
        L_G["JMSGachaAllocationListener"]
        L_PB["JMSPresentBoxAllocationListener"]
        L_MKP["JMSMKPAllocationListener"]
        L_PRE["JMSPresaleAllocationListener"]
    end

    subgraph Handlers["CreateRoomJMSService + Domain Services"]
        CRJ["CreateRoomJMSService\n(room creation)"]
        UMS["UserMatchingService\n(matching logic)"]
        OPS2["OpponentPoolService\n(pool logic)"]
        ROLL["PrizeRollbackService\n(rollback)"]
        MINT["NFT*Service\n(mint allocation)"]
    end

    Producers --> SEND
    SEND --> Q_P & Q_PS & Q_UN & Q_M & Q_G & Q_PB & Q_MKP & Q_PRE & Q_NONE

    Q_P --> L_P
    Q_PS --> L_PS
    Q_UN --> L_UN
    Q_M --> L_M
    Q_G --> L_G
    Q_PB --> L_PB
    Q_MKP --> L_MKP
    Q_PRE --> L_PRE

    L_P & L_PS & L_UN --> CRJ & UMS & OPS2 & ROLL & MINT

2. Message flow - CREATE_PVE_SINGLE_PLAY

sequenceDiagram
    participant WS as CreateRoomService
    participant SEND as SendPrizeAllocationMessageService
    participant PAH as PrizeAllocationHelperService
    participant Q as queue-prize-allocation-PRIZE
    participant L as JMSPrizeAllocationListener
    participant CRJ as CreateRoomJMSService
    participant R as Redis
    participant BCAST as BroadcastPubSubService

    WS->>PAH: determineAllocationType(prizeId)
    PAH->>PAH: check reservedType, prizeType
    PAH-->>WS: AllocationType.PRIZE

    WS->>SEND: sendMessage(PrizeAllocationData{allocType=PRIZE, action=CREATE_PVE_SINGLE_PLAY})
    SEND->>SEND: serialize to JSON
    SEND->>Q: JMSProducer.send(queue, messageJson)

    Q->>L: @JmsListener onMessage(messageJson)
    L->>L: deserialize + validate allocationType == PRIZE
    L->>L: switch(actionType) → createPveSinglePlayRoom
    L->>CRJ: createPveSinglePlayRoom(createRoomData)
    CRJ->>R: create RRoomModel
    CRJ->>BCAST: broadcast ROOM_CREATED

3. Message flow - DECREASE (rollback)

sequenceDiagram
    participant Domain as Domain Service (e.g., GameService)
    participant SEND as SendPrizeAllocationMessageService
    participant Q as queue-prize-allocation-*
    participant L as JMS*AllocationListener
    participant ROLL as PrizeRollbackService

    Domain->>SEND: sendDecreaseMessage(prizeRollbackData, allocationType)
    SEND->>Q: enqueue {action=DECREASE, prizeRollback: {...}}
    Q->>L: onMessage
    L->>L: switch(DECREASE) → prizeRollbackService
    L->>ROLL: processPrizeRollback(data)
    ROLL->>ROLL: restore inventory/allocation
    ROLL->>ROLL: update ledger

4. Message flow - MINT_ALLOCATION

sequenceDiagram
    participant Admin as Admin / System
    participant SEND as SendPrizeAllocationMessageService
    participant Q as queue-prize-allocation-PRIZE
    participant L as JMSPrizeAllocationListener
    participant NFT as NftHunter/Gauntlet/BountyBallService

    Admin->>SEND: sendMintMessage(MintAllocationData{nftType, count, ...})
    SEND->>Q: enqueue {action=MINT_ALLOCATION, mintAllocation: {...}}
    Q->>L: onMessage
    L->>L: switch(MINT_ALLOCATION)
    alt NftType == HUNTER
        L->>NFT: nftHunterService.createHunterAllocation(req)
    else NftType == GAUNTLET
        L->>NFT: nftGauntletService.createGauntletAllocation(req)
    else NftType == BOUNTY_BALL
        L->>NFT: nftBountyBallService.createBountyBallAllocation(req)
    end

5. Listener validation pattern

// Pattern mọi listener đều follow:
@JmsListener(
    destination = "${queue.queue-prize-allocation-PRIZE:queue-prize-allocation-PRIZE}",
    concurrency = "1-1",
    containerFactory = "jmsListenerContainerFactory"
)
public void onMessage(String messageJson) {
    PrizeAllocationData allocationData = convertMessage(messageJson, PrizeAllocationData.class);

    // Guard: verify allocationType matches this listener's type
    if (allocationData.getAllocationType() != PrizeEnum.AllocationType.PRIZE) {
        LOGGER.error("Wrong allocationType {} for PRIZE listener", allocationData.getAllocationType());
        return; // silently drop wrong-typed messages
    }

    try {
        switch (allocationData.getActionType()) {
            case CREATE_PVE_SINGLE_PLAY -> createRoomJMSService.createPveSinglePlayRoom(...)
            case JOIN_AUTO_MATCH_MATCHING -> userMatchingService.joinAutoMatchMatching(...)
            case DECREASE -> prizeRollbackService.processPrizeRollback(...)
            case MINT_ALLOCATION -> handleMintAllocation(allocationData.getMintAllocation())
            // ...
        }
    } catch (CreateRoomFailEx e) {
        broadcastPubSubService.notiPlayer(userId, VALIDATE_GAME, Map.of(REASON_KEY, e.getMessage()));
    } catch (AutoQueueMatchingEx e) {
        broadcastPubSubService.notiPlayer(userId, VALIDATE_GAME, Map.of(REASON_KEY, e.getMessage()));
    }
}

6. Concurrency design

Queue Concurrency Lý do
PRIZE 1-1 Serial per queue để tránh race condition trên stock hữu hạn
PRIZE_FOR_STREAMER 1-1 Tương tự PRIZE, streaming prize cần serialize
UNLIMITED 20-100 Không có stock constraint → parallel xử lý throughput cao
MISSION configurable Theo workload
GACHA configurable Theo workload

7. Error handling & DLQ

Scenario Behavior
Wrong allocationType Log error + drop (không throw)
CreateRoomFailEx Broadcast VALIDATE_GAME + reason
AutoQueueMatchingEx Broadcast VALIDATE_GAME + reason
OpponentPoolActionEx Broadcast VALIDATE_GAME + reason
Unknown exception Let JMS retry → eventually DLQ

8. Điểm cần chú ý

# Vấn đề Chi tiết
1 Idempotency Listener không có explicit idempotency key → risk double-process khi redelivery
2 DLQ strategy Chưa rõ DLQ handling policy (check JMSHandleDlqMessage)
3 Queue config ${queue.*} properties cần đảm bảo nhất quán giữa producer và consumer
4 Multi-match message format prizeIds là list vs prizeId string cần serialize đúng