feat: Add monitoring alerts skeleton and enhance worker with notifications
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
using System.Globalization;
|
||||
using System.Transactions;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances;
|
||||
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyCommissionPool;
|
||||
using CMSMicroservice.Application.CommissionCQ.Commands.ProcessUserPayouts;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.BackgroundJobs;
|
||||
|
||||
/// <summary>
|
||||
/// Background Worker برای محاسبه و توزیع کمیسیونهای هفتگی شبکه
|
||||
/// زمان اجرا: هر یکشنبه ساعت 23:59
|
||||
/// </summary>
|
||||
public class WeeklyNetworkCommissionWorker : BackgroundService
|
||||
{
|
||||
private readonly ILogger<WeeklyNetworkCommissionWorker> _logger;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IAlertService _alertService;
|
||||
private Timer? _timer;
|
||||
|
||||
public WeeklyNetworkCommissionWorker(
|
||||
ILogger<WeeklyNetworkCommissionWorker> logger,
|
||||
IServiceProvider serviceProvider,
|
||||
IAlertService alertService)
|
||||
{
|
||||
_logger = logger;
|
||||
_serviceProvider = serviceProvider;
|
||||
_alertService = alertService;
|
||||
}
|
||||
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("Weekly Network Commission Worker started at: {Time} (Local Time)", DateTime.Now);
|
||||
|
||||
// محاسبه زمان تا یکشنبه بعدی ساعت 23:59
|
||||
var now = DateTime.Now;
|
||||
var nextSunday = GetNextSunday(now);
|
||||
var nextRunTime = new DateTime(nextSunday.Year, nextSunday.Month, nextSunday.Day, 23, 59, 0);
|
||||
|
||||
var delay = nextRunTime - now;
|
||||
if (delay.TotalMilliseconds < 0)
|
||||
{
|
||||
// اگر زمان گذشته باشد، یکشنبه بعدی
|
||||
nextRunTime = nextRunTime.AddDays(7);
|
||||
delay = nextRunTime - now;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Next execution scheduled for: {NextRun}", nextRunTime);
|
||||
|
||||
// تنظیم timer برای اجرا در زمان مشخص و تکرار هفتگی
|
||||
_timer = new Timer(
|
||||
callback: async _ => await ExecuteWeeklyCalculationAsync(stoppingToken),
|
||||
state: null,
|
||||
dueTime: delay,
|
||||
period: TimeSpan.FromDays(7) // هر 7 روز یکبار
|
||||
);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// محاسبه تاریخ یکشنبه بعدی
|
||||
/// </summary>
|
||||
private static DateTime GetNextSunday(DateTime from)
|
||||
{
|
||||
var daysUntilSunday = ((int)DayOfWeek.Sunday - (int)from.DayOfWeek + 7) % 7;
|
||||
if (daysUntilSunday == 0)
|
||||
{
|
||||
// اگر امروز یکشنبه است و ساعت گذشته، یکشنبه بعدی
|
||||
if (from.TimeOfDay > new TimeSpan(23, 59, 0))
|
||||
{
|
||||
daysUntilSunday = 7;
|
||||
}
|
||||
}
|
||||
return from.Date.AddDays(daysUntilSunday);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// اجرای محاسبات هفتگی کمیسیون
|
||||
/// </summary>
|
||||
private async Task ExecuteWeeklyCalculationAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var executionId = Guid.NewGuid();
|
||||
var startTime = DateTime.Now; // استفاده از Local Time
|
||||
_logger.LogInformation("=== Starting Weekly Commission Calculation [{ExecutionId}] at {Time} (Local Time) ===",
|
||||
executionId, startTime);
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
var context = scope.ServiceProvider.GetRequiredService<IApplicationDbContext>();
|
||||
|
||||
// دریافت شماره هفته قبل (هفتهای که باید محاسبه شود)
|
||||
var previousWeekNumber = GetPreviousWeekNumber();
|
||||
var currentWeekNumber = GetCurrentWeekNumber();
|
||||
|
||||
_logger.LogInformation("Processing week: {WeekNumber}", previousWeekNumber);
|
||||
|
||||
// ===== IDEMPOTENCY CHECK =====
|
||||
// بررسی اینکه آیا این هفته قبلاً محاسبه شده یا نه
|
||||
var existingPool = await context.WeeklyCommissionPools
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.WeekNumber == previousWeekNumber, cancellationToken);
|
||||
|
||||
if (existingPool?.IsCalculated == true)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Week {WeekNumber} already calculated. Skipping execution [{ExecutionId}]",
|
||||
previousWeekNumber, executionId);
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== TRANSACTION SCOPE =====
|
||||
// تمام مراحل باید داخل یک تراکنش باشند برای Atomicity
|
||||
using var transaction = new TransactionScope(
|
||||
TransactionScopeOption.Required,
|
||||
new TransactionOptions
|
||||
{
|
||||
IsolationLevel = IsolationLevel.ReadCommitted,
|
||||
Timeout = TimeSpan.FromMinutes(30) // برای شبکههای بزرگ
|
||||
},
|
||||
TransactionScopeAsyncFlowOption.Enabled);
|
||||
|
||||
int balancesCalculated = 0;
|
||||
long poolValue = 0;
|
||||
int payoutsProcessed = 0;
|
||||
|
||||
try
|
||||
{
|
||||
// مرحله 1: محاسبه تعادلهای شبکه
|
||||
_logger.LogInformation("Step 1/4: Calculating network balances for week {WeekNumber}", previousWeekNumber);
|
||||
balancesCalculated = await mediator.Send(new CalculateWeeklyBalancesCommand
|
||||
{
|
||||
WeekNumber = previousWeekNumber,
|
||||
ForceRecalculate = false
|
||||
}, cancellationToken);
|
||||
_logger.LogInformation("Network balances calculated: {Count} users processed", balancesCalculated);
|
||||
|
||||
// مرحله 2: محاسبه استخر کمیسیون و ارزش هر امتیاز
|
||||
_logger.LogInformation("Step 2/4: Calculating commission pool for week {WeekNumber}", previousWeekNumber);
|
||||
poolValue = await mediator.Send(new CalculateWeeklyCommissionPoolCommand
|
||||
{
|
||||
WeekNumber = previousWeekNumber,
|
||||
ForceRecalculate = false
|
||||
}, cancellationToken);
|
||||
_logger.LogInformation("Commission pool calculated. Value per balance: {Value:N0} Rials", poolValue);
|
||||
|
||||
// مرحله 3: توزیع کمیسیونها به کاربران
|
||||
_logger.LogInformation("Step 3/4: Processing user payouts for week {WeekNumber}", previousWeekNumber);
|
||||
payoutsProcessed = await mediator.Send(new ProcessUserPayoutsCommand
|
||||
{
|
||||
WeekNumber = previousWeekNumber,
|
||||
ForceReprocess = false
|
||||
}, cancellationToken);
|
||||
_logger.LogInformation("User payouts processed: {Count} payouts created", payoutsProcessed);
|
||||
|
||||
// ===== مرحله 4 (گام 5 در مستندات): ریست/Expire کردن تعادلهای هفته قبل =====
|
||||
_logger.LogInformation("Step 4/4: Expiring weekly balances for week {WeekNumber}", previousWeekNumber);
|
||||
var balancesToExpire = await context.NetworkWeeklyBalances
|
||||
.Where(x => x.WeekNumber == previousWeekNumber && !x.IsExpired)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var balance in balancesToExpire)
|
||||
{
|
||||
balance.IsExpired = true;
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
_logger.LogInformation("Expired {Count} balance records", balancesToExpire.Count);
|
||||
|
||||
// Commit Transaction
|
||||
transaction.Complete();
|
||||
|
||||
var duration = DateTime.Now - startTime; // محاسبه مدت زمان با Local Time
|
||||
_logger.LogInformation(
|
||||
"=== Weekly Commission Calculation Completed Successfully [{ExecutionId}] ===" +
|
||||
"\n Week: {WeekNumber}" +
|
||||
"\n Users Processed: {UserCount}" +
|
||||
"\n Value Per Balance: {ValuePerBalance:N0} Rials" +
|
||||
"\n Payouts Created: {PayoutCount}" +
|
||||
"\n Balances Expired: {ExpiredCount}" +
|
||||
"\n Duration: {Duration:mm\\:ss}",
|
||||
executionId,
|
||||
previousWeekNumber,
|
||||
balancesCalculated,
|
||||
poolValue,
|
||||
payoutsProcessed,
|
||||
balancesToExpire.Count,
|
||||
duration
|
||||
);
|
||||
|
||||
// Send success notification to admin
|
||||
using var successScope = _serviceProvider.CreateScope();
|
||||
var alertService = successScope.ServiceProvider.GetRequiredService<IAlertService>();
|
||||
|
||||
await alertService.SendSuccessNotificationAsync(
|
||||
"Weekly Commission Completed",
|
||||
$"Week {previousWeekNumber}: {payoutsProcessed} payouts, {balancesToExpire.Count} balances expired");
|
||||
|
||||
// TODO: Send notifications to users who received commission
|
||||
// await NotifyUsersAboutPayouts(payoutsProcessed, previousWeekNumber);
|
||||
}
|
||||
catch (Exception innerEx)
|
||||
{
|
||||
_logger.LogError(innerEx,
|
||||
"Transaction failed during step execution. Rolling back. [{ExecutionId}]",
|
||||
executionId);
|
||||
// Transaction will auto-rollback when scope is disposed without Complete()
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCritical(ex,
|
||||
"!!! CRITICAL ERROR in Weekly Commission Calculation [{ExecutionId}] !!!" +
|
||||
"\n Week: {WeekNumber}" +
|
||||
"\n Message: {Message}" +
|
||||
"\n StackTrace: {StackTrace}" +
|
||||
"\n Please investigate immediately!",
|
||||
executionId,
|
||||
GetPreviousWeekNumber(),
|
||||
ex.Message,
|
||||
ex.StackTrace);
|
||||
|
||||
// ===== ERROR HANDLING & ALERTING =====
|
||||
// در محیط production باید Alert/Notification ارسال شود
|
||||
|
||||
using var errorScope = _serviceProvider.CreateScope();
|
||||
var alertService = errorScope.ServiceProvider.GetRequiredService<IAlertService>();
|
||||
|
||||
await alertService.SendCriticalAlertAsync(
|
||||
"Weekly Commission Worker Failed",
|
||||
$"Worker execution {executionId} failed for week {GetPreviousWeekNumber()}",
|
||||
ex,
|
||||
cancellationToken);
|
||||
|
||||
// TODO: Retry logic با exponential backoff
|
||||
// await RetryWithExponentialBackoff(() => ExecuteWeeklyCalculationAsync(cancellationToken));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت شماره هفته جاری (فرمت ISO 8601: YYYY-Www)
|
||||
/// </summary>
|
||||
private static string GetCurrentWeekNumber()
|
||||
{
|
||||
var today = DateTime.Today;
|
||||
var calendar = CultureInfo.CurrentCulture.Calendar;
|
||||
var weekNumber = calendar.GetWeekOfYear(
|
||||
today,
|
||||
CalendarWeekRule.FirstFourDayWeek,
|
||||
DayOfWeek.Monday
|
||||
);
|
||||
return $"{today.Year}-W{weekNumber:D2}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت شماره هفته قبل
|
||||
/// </summary>
|
||||
private static string GetPreviousWeekNumber()
|
||||
{
|
||||
var lastWeek = DateTime.Today.AddDays(-7);
|
||||
var calendar = CultureInfo.CurrentCulture.Calendar;
|
||||
var weekNumber = calendar.GetWeekOfYear(
|
||||
lastWeek,
|
||||
CalendarWeekRule.FirstFourDayWeek,
|
||||
DayOfWeek.Monday
|
||||
);
|
||||
return $"{lastWeek.Year}-W{weekNumber:D2}";
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user