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) (txID string, err error) expGrant func(username string, exp int) error txCheck func(txID string) (confirmed bool, err error) // 이중 지급 방지: tx 상태 확인 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) (txID string, err error), expGrant func(username string, exp int) error, txCheck func(txID string) (confirmed bool, err error), ) *RewardWorker { return &RewardWorker{ repo: repo, rewardGrant: rewardGrant, expGrant: expGrant, txCheck: txCheck, 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 { // 이중 지급 방지: 마지막 tx가 이미 성공했는지 확인 if rf.LastTxID != "" && w.txCheck != nil { confirmed, checkErr := w.txCheck(rf.LastTxID) if checkErr != nil { log.Printf("보상 재시도 tx 상태 확인 실패: ID=%d, txID=%s: %v", rf.ID, rf.LastTxID, checkErr) // 상태 확인 실패 시 안전하게 재시도 건너뜀 (다음 주기에 다시 확인) return } if confirmed { log.Printf("보상 재시도 건너뜀 (이전 tx 이미 성공): ID=%d, txID=%s, %s", rf.ID, rf.LastTxID, rf.Username) // 블록체인 보상은 이미 지급됨 → 경험치만 확인 goto expRetry } } 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 } } txID, grantErr := w.rewardGrant(rf.Username, rf.TokenAmount, assets) retryErr = grantErr // 시도한 txID 저장 (다음 재시도 시 이중 지급 방지용) if txID != "" { if err := w.repo.UpdateLastTxID(rf.ID, txID); err != nil { log.Printf("보상 재시도 txID 저장 실패: ID=%d: %v", rf.ID, err) } } } expRetry: // 경험치 재시도 (블록체인 보상이 없거나 성공한 경우) 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("CRITICAL: 보상 재시도 포기 (최대 횟수 초과) — 수동 복구 필요: ID=%d, session=%s, user=%s, token=%d, exp=%d", rf.ID, rf.SessionName, rf.Username, rf.TokenAmount, rf.Experience) } else { log.Printf("보상 재시도 실패 (%d/10): ID=%d, %s: %v", newCount, rf.ID, rf.Username, retryErr) } }