瀏覽代碼

fix chặn triệt để k cho retry

student 3 周之前
父節點
當前提交
b82abff78a

+ 30 - 5
Kitty_Fall/Kitty_Fall/Kitty_Fall.Woker/Services/GameMonthlyWinnerWorker.cs

@@ -29,8 +29,7 @@ namespace Kitty_Fall.Woker.Services
 
         protected override async Task ExecuteAsync(CancellationToken stoppingToken)
         {
-            var intervalSeconds = GetInt("MonthlyWinner:IntervalSeconds", 3600);
-            log.Info($"GameMonthlyWinnerWorker started. intervalSeconds={intervalSeconds}");
+            log.Info("GameMonthlyWinnerWorker started.");
 
             while (!stoppingToken.IsCancellationRequested)
             {
@@ -38,7 +37,15 @@ namespace Kitty_Fall.Woker.Services
                 {
                     if (GetBool("MonthlyWinner:AutoGenerate", true))
                     {
-                        await ProcessMonthlyWinnerAsync(stoppingToken);
+                        var now = DateTime.Now;
+                        if (TryGetScheduledTargetMonth(now, out var targetMonth, out var scheduledTime))
+                        {
+                            await ProcessMonthlyWinnerAsync(targetMonth, stoppingToken);
+                        }
+                        else
+                        {
+                            log.Debug($"Monthly winner waiting for cutoff. now={now:yyyy-MM-dd HH:mm:ss}, scheduledTime={scheduledTime:yyyy-MM-dd HH:mm:ss}");
+                        }
                     }
                 }
                 catch (Exception ex)
@@ -46,16 +53,34 @@ namespace Kitty_Fall.Woker.Services
                     log.Error("GameMonthlyWinnerWorker loop error", ex);
                 }
 
+                // Doc lai moi vong de appsettings co the reload dong khi test.
+                var intervalSeconds = GetInt("MonthlyWinner:IntervalSeconds", 3600);
                 await Task.Delay(TimeSpan.FromSeconds(intervalSeconds), stoppingToken);
             }
         }
 
-        private async Task ProcessMonthlyWinnerAsync(CancellationToken cancellationToken)
+        private bool TryGetScheduledTargetMonth(
+            DateTime now,
+            out DateTime targetMonth,
+            out DateTime scheduledTime)
+        {
+            var runDay = Math.Clamp(GetInt("MonthlyWinner:RunDay", 1), 1, 28);
+            var cutoffHour = Math.Clamp(GetInt("MonthlyWinner:CutoffHour", 2), 0, 23);
+            var cutoffMinute = Math.Clamp(GetInt("MonthlyWinner:CutoffMinute", 0), 0, 59);
+
+            scheduledTime = new DateTime(now.Year, now.Month, runDay, cutoffHour, cutoffMinute, 0);
+            targetMonth = new DateTime(now.Year, now.Month, 1)
+                .AddMonths(GetInt("MonthlyWinner:TargetMonthOffset", -1));
+
+            // Sau cutoff van xu ly: day la catch-up neu service bi tat/restart dung lich.
+            return now >= scheduledTime;
+        }
+
+        private async Task ProcessMonthlyWinnerAsync(DateTime targetMonth, CancellationToken cancellationToken)
         {
             using var scope = scopeFactory.CreateScope();
             var dbContext = scope.ServiceProvider.GetRequiredService<ModelContext>();
 
-            var targetMonth = DateTime.Now.Date.AddMonths(GetInt("MonthlyWinner:TargetMonthOffset", -1));
             var monthKey = targetMonth.Year * 100 + targetMonth.Month;
 
             var draw = await EnsureDrawAsync(dbContext, monthKey, cancellationToken);

+ 35 - 26
Kitty_Fall/Kitty_Fall/Kitty_Fall.Woker/Services/GameRewardPayoutWorker.cs

@@ -54,45 +54,50 @@ namespace Kitty_Fall.Woker.Services
             var dbContext = scope.ServiceProvider.GetRequiredService<ModelContext>();
 
             var batchSize = GetInt("RewardWorker:BatchSize", 20);
-            var maxRetry = GetInt("RewardWorker:MaxRetry", 3);
-            var retryAfterMinutes = GetInt("RewardWorker:RetryAfterMinutes", 1);
-            var retryBefore = DateTime.Now.AddMinutes(-retryAfterMinutes);
-
-            // Lay cac log thuong can tra qua WS. SCORE la diem noi bo nen API da xu ly.
-            var rewardLogs = await dbContext.GameRewardLogs
-                .Where(x => (x.Status == StatusNew || x.Status == StatusFailed)
-                    && x.RetryCount < maxRetry
-                    && (x.RewardType == "DATA" || x.RewardType == "LOYALTY")
-                    && (x.UpdatedTime == null || x.UpdatedTime <= retryBefore))
+
+            // Chi lay NEW de moi reward chi duoc tu dong goi WS mot lan.
+            // FAILED va PROCESSING (ke ca sau restart) phai duoc admin doi soat thu cong,
+            // tuyet doi khong tu retry vi ket qua phia WS co the da thanh cong.
+            var rewardLogIds = await dbContext.GameRewardLogs
+                .AsNoTracking()
+                .Where(x => x.Status == StatusNew
+                    && (x.RewardType == "DATA" || x.RewardType == "LOYALTY"))
                 .OrderBy(x => x.CreatedTime)
                 .ThenBy(x => x.RewardLogId)
+                .Select(x => x.RewardLogId)
                 .Take(batchSize)
                 .ToListAsync(cancellationToken);
 
-            foreach (var rewardLog in rewardLogs)
+            foreach (var rewardLogId in rewardLogIds)
             {
                 if (cancellationToken.IsCancellationRequested) return;
-                await ProcessRewardLogAsync(dbContext, rewardLog, maxRetry, cancellationToken);
+
+                var now = DateTime.Now;
+                // Claim bang UPDATE co dieu kien de hai instance worker khong the cung tra.
+                var claimed = await dbContext.GameRewardLogs
+                    .Where(x => x.RewardLogId == rewardLogId && x.Status == StatusNew)
+                    .ExecuteUpdateAsync(setters => setters
+                        .SetProperty(x => x.Status, StatusProcessing)
+                        .SetProperty(x => x.RequestTime, now)
+                        .SetProperty(x => x.UpdatedTime, now), cancellationToken);
+                if (claimed != 1) continue;
+
+                var rewardLog = await dbContext.GameRewardLogs
+                    .FirstAsync(x => x.RewardLogId == rewardLogId, cancellationToken);
+                await ProcessRewardLogAsync(dbContext, rewardLog, cancellationToken);
             }
         }
 
         private async Task ProcessRewardLogAsync(
             ModelContext dbContext,
             GameRewardLog rewardLog,
-            int maxRetry,
             CancellationToken cancellationToken)
         {
             var now = DateTime.Now;
 
-            // Khoa mem log de tranh 2 vong worker cung tra mot reward.
-            rewardLog.Status = StatusProcessing;
-            rewardLog.RequestTime = now;
-            rewardLog.UpdatedTime = now;
-            await dbContext.SaveChangesAsync(cancellationToken);
-
             try
             {
-                var limitResult = await ApplyLimitBeforePayoutAsync(dbContext, rewardLog, maxRetry, now, cancellationToken);
+                var limitResult = await ApplyLimitBeforePayoutAsync(dbContext, rewardLog, now, cancellationToken);
                 if (!limitResult.CanPayout)
                 {
                     await dbContext.SaveChangesAsync(cancellationToken);
@@ -129,7 +134,10 @@ namespace Kitty_Fall.Woker.Services
                 else
                 {
                     rewardLog.RetryCount++;
-                    rewardLog.Status = rewardLog.RetryCount >= maxRetry ? StatusFailed : StatusNew;
+                    rewardLog.Status = StatusFailed;
+                    rewardLog.ResponseMessage = Truncate(
+                        $"Manual review required; automatic retry is disabled. {rewardLog.ResponseMessage}",
+                        500);
                 }
 
                 await dbContext.SaveChangesAsync(cancellationToken);
@@ -139,10 +147,12 @@ namespace Kitty_Fall.Woker.Services
                 log.Error($"Process reward log error. rewardLogId={rewardLog.RewardLogId}", ex);
 
                 rewardLog.RetryCount++;
-                rewardLog.Status = rewardLog.RetryCount >= maxRetry ? StatusFailed : StatusNew;
+                rewardLog.Status = StatusFailed;
                 rewardLog.ResponseTime = DateTime.Now;
                 rewardLog.ResponseCode = "EXCEPTION";
-                rewardLog.ResponseMessage = Truncate(ex.Message, 500);
+                rewardLog.ResponseMessage = Truncate(
+                    $"Manual review required; automatic retry is disabled. {ex.Message}",
+                    500);
                 rewardLog.UpdatedTime = DateTime.Now;
                 await dbContext.SaveChangesAsync(cancellationToken);
             }
@@ -151,7 +161,6 @@ namespace Kitty_Fall.Woker.Services
         private async Task<LimitCheckResult> ApplyLimitBeforePayoutAsync(
             ModelContext dbContext,
             GameRewardLog rewardLog,
-            int maxRetry,
             DateTime now,
             CancellationToken cancellationToken)
         {
@@ -206,7 +215,7 @@ namespace Kitty_Fall.Woker.Services
                         StringComparison.OrdinalIgnoreCase))
                     {
                         rewardLog.Status = StatusFailed;
-                        rewardLog.RetryCount = (short)maxRetry;
+                        rewardLog.RetryCount++;
                         rewardLog.ResponseTime = now;
                         rewardLog.ResponseCode = "INVALID_CROSS_TYPE_FALLBACK";
                         rewardLog.ResponseMessage = $"Cross-type fallback is not allowed: {rewardLog.RewardType} -> {limitConfig.FallbackRewardType}";
@@ -223,7 +232,7 @@ namespace Kitty_Fall.Woker.Services
                 // Vuot han muc va khong co fallback thi danh dau failed, khong retry lap lai.
                 rewardLog.LimitConfigId = limitConfig.LimitConfigId;
                 rewardLog.Status = StatusFailed;
-                rewardLog.RetryCount = (short)maxRetry;
+                rewardLog.RetryCount++;
                 rewardLog.ResponseTime = now;
                 rewardLog.ResponseCode = "LIMIT_EXCEEDED";
                 rewardLog.ResponseMessage = $"Reward limit exceeded: {limitConfig.LimitCode}";

+ 6 - 2
Kitty_Fall/Kitty_Fall/Kitty_Fall.Woker/appsettings.Development.json

@@ -7,10 +7,9 @@
   "wsUser": "ws_KittyFall",
   "wsPass": "KittyFall@123A",
   "RewardWorker": {
+    // Worker chi goi moi reward NEW mot lan; loi/timeout/PROCESSING de admin doi soat, khong auto retry.
     "IntervalSeconds": 10,
     "BatchSize": 20,
-    "MaxRetry": 3,
-    "RetryAfterMinutes": 1,
     "ServiceId": "KittyFall",
     "DataPackageName": "GAME_DATA",
     "LoyaltyPackageName": "GAME_LOYALTY",
@@ -25,6 +24,11 @@
     "AutoAnnounceWinners": false,
     // So giay giua moi lan worker quet xu ly giai thang.
     "IntervalSeconds": 3600,
+    // Doi thanh ngay hien tai khi can test; production cau hinh ngay 1.
+    "RunDay": 1,
+    // Gio/phut cutoff theo gio local cua server.
+    "CutoffHour": 2,
+    "CutoffMinute": 0,
     // Thang can xu ly so voi thang hien tai, -1 la thang truoc.
     "TargetMonthOffset": -1,
     // Diem toi thieu de user duoc sinh ticket tham gia giai thang.

+ 6 - 2
Kitty_Fall/Kitty_Fall/Kitty_Fall.Woker/appsettings.json

@@ -7,10 +7,9 @@
   "wsUser": "ws_KittyFall",
   "wsPass": "KittyFall@123A",
   "RewardWorker": {
+    // Worker chi goi moi reward NEW mot lan; loi/timeout/PROCESSING de admin doi soat, khong auto retry.
     "IntervalSeconds": 10,
     "BatchSize": 20,
-    "MaxRetry": 3,
-    "RetryAfterMinutes": 1,
     "ServiceId": "KittyFall",
     "DataPackageName": "GAME_DATA",
     "LoyaltyPackageName": "GAME_LOYALTY",
@@ -25,6 +24,11 @@
     "AutoAnnounceWinners": false,
     // So giay giua moi lan worker quet xu ly giai thang.
     "IntervalSeconds": 3600,
+    // Ngay worker duoc phep tong ket. Production de 1; khi test co the doi 1..28.
+    "RunDay": 1,
+    // Gio/phut cutoff theo gio local cua server. Sau moc nay restart van catch-up.
+    "CutoffHour": 2,
+    "CutoffMinute": 0,
     // Thang can xu ly so voi thang hien tai, -1 la thang truoc.
     "TargetMonthOffset": -1,
     // Diem toi thieu de user duoc sinh ticket tham gia giai thang.