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 |