- 보상 지급 실패 시 즉시 재시도(3회 backoff) + DB 기록 + 백그라운드 워커 재시도 - WaitForTx 폴링으로 블록체인 TX 확정 대기, SendTxAndWait 편의 메서드 - chain 트랜잭션 코드 중복 제거 (userTx/operatorTx 헬퍼, 50% 감소) - AppError 기반 에러 응답 포맷 통일 (8개 코드, 전 핸들러 마이그레이션) - TX 에러 분류 + 한국어 사용자 메시지 매핑 (11가지 패턴) - player 서비스 테스트 20개 + chain WaitForTx 테스트 10개 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
113 lines
3.1 KiB
Go
113 lines
3.1 KiB
Go
package bossraid
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/tolelom/tolchain/core"
|
|
)
|
|
|
|
// RewardWorker periodically retries failed reward grants.
|
|
type RewardWorker struct {
|
|
repo *Repository
|
|
rewardGrant func(username string, tokenAmount uint64, assets []core.MintAssetPayload) error
|
|
expGrant func(username string, exp int) error
|
|
interval time.Duration
|
|
stopCh chan struct{}
|
|
}
|
|
|
|
// NewRewardWorker creates a new RewardWorker. Default interval is 1 minute.
|
|
func NewRewardWorker(
|
|
repo *Repository,
|
|
rewardGrant func(username string, tokenAmount uint64, assets []core.MintAssetPayload) error,
|
|
expGrant func(username string, exp int) error,
|
|
) *RewardWorker {
|
|
return &RewardWorker{
|
|
repo: repo,
|
|
rewardGrant: rewardGrant,
|
|
expGrant: expGrant,
|
|
interval: 1 * time.Minute,
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Start begins the background polling loop in a goroutine.
|
|
func (w *RewardWorker) Start() {
|
|
go func() {
|
|
ticker := time.NewTicker(w.interval)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-w.stopCh:
|
|
log.Println("보상 재시도 워커 종료")
|
|
return
|
|
case <-ticker.C:
|
|
w.processFailures()
|
|
}
|
|
}
|
|
}()
|
|
log.Println("보상 재시도 워커 시작")
|
|
}
|
|
|
|
// Stop gracefully stops the worker.
|
|
func (w *RewardWorker) Stop() {
|
|
close(w.stopCh)
|
|
}
|
|
|
|
func (w *RewardWorker) processFailures() {
|
|
failures, err := w.repo.GetPendingRewardFailures(10)
|
|
if err != nil {
|
|
log.Printf("보상 재시도 조회 실패: %v", err)
|
|
return
|
|
}
|
|
for _, rf := range failures {
|
|
w.retryOne(rf)
|
|
}
|
|
}
|
|
|
|
func (w *RewardWorker) retryOne(rf RewardFailure) {
|
|
var retryErr error
|
|
|
|
// 블록체인 보상 재시도 (토큰 또는 에셋이 있는 경우)
|
|
if (rf.TokenAmount > 0 || rf.Assets != "[]") && w.rewardGrant != nil {
|
|
var assets []core.MintAssetPayload
|
|
if rf.Assets != "" && rf.Assets != "[]" {
|
|
if err := json.Unmarshal([]byte(rf.Assets), &assets); err != nil {
|
|
log.Printf("보상 재시도 에셋 파싱 실패: ID=%d: %v", rf.ID, err)
|
|
// 파싱 불가능한 경우 resolved 처리
|
|
if err := w.repo.ResolveRewardFailure(rf.ID); err != nil {
|
|
log.Printf("보상 실패 resolve 실패: ID=%d: %v", rf.ID, err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
retryErr = w.rewardGrant(rf.Username, rf.TokenAmount, assets)
|
|
}
|
|
|
|
// 경험치 재시도 (블록체인 보상이 없거나 성공한 경우)
|
|
if retryErr == nil && rf.Experience > 0 && w.expGrant != nil {
|
|
retryErr = w.expGrant(rf.Username, rf.Experience)
|
|
}
|
|
|
|
if retryErr == nil {
|
|
if err := w.repo.ResolveRewardFailure(rf.ID); err != nil {
|
|
log.Printf("보상 재시도 성공 기록 실패: ID=%d: %v", rf.ID, err)
|
|
} else {
|
|
log.Printf("보상 재시도 성공: ID=%d, %s", rf.ID, rf.Username)
|
|
}
|
|
return
|
|
}
|
|
|
|
// 재시도 실패 — retry_count 증가
|
|
if err := w.repo.IncrementRetryCount(rf.ID, retryErr.Error()); err != nil {
|
|
log.Printf("보상 재시도 카운트 증가 실패: ID=%d: %v", rf.ID, err)
|
|
}
|
|
newCount := rf.RetryCount + 1
|
|
if newCount >= 10 {
|
|
log.Printf("보상 재시도 포기 (최대 횟수 초과): ID=%d, %s", rf.ID, rf.Username)
|
|
} else {
|
|
log.Printf("보상 재시도 실패 (%d/10): ID=%d, %s: %v", newCount, rf.ID, rf.Username, retryErr)
|
|
}
|
|
}
|