Utopia-Canteen-System/UtopiaCanteen.BackendService/CanteenBackendWorker.cs

255 lines
10 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using UtopiaCanteenSystem.Api;
using UtopiaCanteenSystem.Data;
using UtopiaCanteenSystem.Services;
using UtopiaCanteenSystem.Services.Logging;
namespace UtopiaCanteen.BackendService;
public sealed class CanteenBackendWorker : BackgroundService
{
private readonly IConfigService _config;
private readonly DbContextFactory _dbFactory;
private readonly IConfiguration _configuration;
private readonly ILogger<CanteenBackendWorker> _logger;
private CancellationTokenSource? _apiCts;
private Task? _apiTask;
private System.Timers.Timer? _cacheSyncTimer;
private System.Timers.Timer? _orderSyncTimer;
private int _cacheSyncRunning;
private int _orderSyncRunning;
private CanteenBackendServices? _backend;
public CanteenBackendWorker(
IConfigService config,
DbContextFactory dbFactory,
IConfiguration configuration,
ILogger<CanteenBackendWorker> logger)
{
_config = config;
_dbFactory = dbFactory;
_configuration = configuration;
_logger = logger;
}
public override async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Service starting...");
FileLogger.Info("BackendService", "Service starting...");
try
{
using (var db = _dbFactory.CreateDbContext())
db.EnsureDatabaseCreated();
FileLogger.Info("BackendService", $"Configuration loaded. SQLite DB path={DatabasePath.GetDbPath()}");
FileLogger.Info(
"BackendService",
$"Device ID resolved. MachineName={Environment.MachineName}, DeviceId={_config.GetDeviceId()}");
FileLogger.Info("BackendService", $"ListenUrls={_config.GetLocalServerListenUrls()}");
FileLogger.Info(
"BackendService",
$"MySqlConnectionString={LogMasking.MaskConnectionString(_config.GetMySqlConnectionString())}");
FileLogger.Info(
"BackendService",
$"HrmsConnection(effective)={LogMasking.MaskConnectionString(_config.GetHrmsLookupConnectionString())}");
FileLogger.Info("BackendService", $"Logs directory={LogPaths.BackendLogsDirectory}");
}
catch (Exception ex)
{
FileLogger.Error("BackendService", "Failed during startup initialization.", ex);
throw;
}
var employeeRfidTagSync = new EmployeeRfidTagSyncService(_dbFactory, _config);
var mealMenuCacheSync = new MealMenuCacheSyncService(_dbFactory, _config);
var offlineCacheSync = new OfflineCacheSyncService(employeeRfidTagSync, mealMenuCacheSync);
var employeeLookup = new EmployeeLookupService(_dbFactory, _config);
var menuLookup = new MenuLookupService(_dbFactory);
var mealSessionResolver = new DbMealSessionResolver(_dbFactory);
var productionSync = new SyncService(_dbFactory, _config);
var rfid = new RfidService(_dbFactory, _config, employeeLookup, mealSessionResolver, menuLookup, productionSync);
_backend = new CanteenBackendServices(rfid, offlineCacheSync, productionSync, _config);
_apiCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var listenUrls = _config.GetLocalServerListenUrls();
var backend = _backend;
var cts = _apiCts;
_apiTask = Task.Run(async () =>
{
try
{
await Task.Delay(500, cts.Token).ConfigureAwait(false);
await CanteenBackendHost.RunAsync(backend, listenUrls, cts.Token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
FileLogger.Info("BackendService", "API host stopped (cancellation).");
}
catch (Exception ex)
{
FileLogger.Error("BackendService", "API host failed.", ex);
Logger.Log(ex, "CanteenBackendWorker.ApiHost");
}
}, CancellationToken.None);
_ = RunStartupCacheSyncAsync(cancellationToken);
var cacheMinutes = Math.Max(1, _configuration.GetValue("CacheSyncIntervalMinutes", 15));
if (!string.IsNullOrWhiteSpace(_config.GetHrmsLookupConnectionString()))
{
_cacheSyncTimer = new System.Timers.Timer(TimeSpan.FromMinutes(cacheMinutes).TotalMilliseconds) { AutoReset = true };
_cacheSyncTimer.Elapsed += async (_, _) => await RunCacheSyncSafeAsync("timer").ConfigureAwait(false);
_cacheSyncTimer.Start();
FileLogger.Info("BackendService", $"Background cache sync timer started. IntervalMinutes={cacheMinutes}");
}
else
{
FileLogger.Warn("BackendService", "Background cache sync timer not started (MySQL/HRMS connection not configured).");
}
var orderMinutes = Math.Max(1, _configuration.GetValue("ProductionSyncIntervalMinutes", 1));
if (_config.GetSyncServiceEnabled() && !string.IsNullOrWhiteSpace(_config.GetMySqlConnectionString()))
{
_orderSyncTimer = new System.Timers.Timer(TimeSpan.FromMinutes(orderMinutes).TotalMilliseconds) { AutoReset = true };
_orderSyncTimer.Elapsed += async (_, _) => await RunOrderSyncSafeAsync("timer").ConfigureAwait(false);
_orderSyncTimer.Start();
FileLogger.Info("BackendService", $"Background production sync timer started. IntervalMinutes={orderMinutes}");
}
else
{
FileLogger.Warn("BackendService", "Background production sync timer not started (disabled or MySQL not configured).");
}
_logger.LogInformation("Service started.");
FileLogger.Info("BackendService", "Service started.");
await base.StartAsync(cancellationToken).ConfigureAwait(false);
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Service stopping...");
FileLogger.Info("BackendService", "Service stopping...");
_cacheSyncTimer?.Stop();
_orderSyncTimer?.Stop();
_apiCts?.Cancel();
if (_apiTask != null)
{
try
{
await _apiTask.WaitAsync(TimeSpan.FromSeconds(10), cancellationToken).ConfigureAwait(false);
}
catch
{
// Best-effort
}
}
_cacheSyncTimer?.Dispose();
_orderSyncTimer?.Dispose();
_apiCts?.Dispose();
FileLogger.Info("BackendService", "Service stopped.");
await base.StopAsync(cancellationToken).ConfigureAwait(false);
}
protected override Task ExecuteAsync(CancellationToken stoppingToken) =>
Task.Delay(Timeout.Infinite, stoppingToken);
private async Task RunStartupCacheSyncAsync(CancellationToken cancellationToken)
{
if (_backend == null || string.IsNullOrWhiteSpace(_config.GetHrmsLookupConnectionString()))
{
FileLogger.Info("CacheSync", "Startup cache sync skipped (MySQL/HRMS connection not configured).");
return;
}
FileLogger.Info("CacheSync", "Startup cache sync started.");
for (var i = 0; i < 30 && !cancellationToken.IsCancellationRequested; i++)
{
try
{
await _backend.RunCacheSyncExclusiveAsync(async () =>
{
await _backend.OfflineCacheSync.SyncEmployeeAndMenuCacheAsync().ConfigureAwait(false);
return true;
}).ConfigureAwait(false);
FileLogger.Info("CacheSync", "Startup cache sync completed.");
return;
}
catch (Exception ex)
{
FileLogger.Warn("CacheSync", $"Startup cache sync attempt {i + 1} failed; retrying.", ex);
Logger.Log(ex, "CanteenBackendWorker.RunStartupCacheSyncAsync");
}
await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
}
FileLogger.Error("CacheSync", "Startup cache sync failed after retries.");
}
private async Task RunCacheSyncSafeAsync(string trigger)
{
if (_backend == null)
return;
if (Interlocked.Exchange(ref _cacheSyncRunning, 1) == 1)
{
FileLogger.Info("CacheSync", $"Background cache sync skipped ({trigger}): already running.");
return;
}
FileLogger.Info("CacheSync", $"Background cache sync started ({trigger}).");
try
{
await _backend.RunCacheSyncExclusiveAsync(async () =>
{
await _backend.OfflineCacheSync.SyncEmployeeAndMenuCacheAsync().ConfigureAwait(false);
return true;
}).ConfigureAwait(false);
FileLogger.Info("CacheSync", $"Background cache sync completed ({trigger}).");
}
catch (Exception ex)
{
FileLogger.Error("CacheSync", $"Background cache sync failed ({trigger}).", ex);
Logger.Log(ex, "CanteenBackendWorker.RunCacheSyncSafeAsync");
}
finally
{
Interlocked.Exchange(ref _cacheSyncRunning, 0);
}
}
private async Task RunOrderSyncSafeAsync(string trigger)
{
if (_backend == null)
return;
if (Interlocked.Exchange(ref _orderSyncRunning, 1) == 1)
{
FileLogger.Info("OrderSync", $"Background order sync skipped ({trigger}): already running.");
return;
}
FileLogger.Info("OrderSync", $"Background order sync started ({trigger}).");
try
{
await _backend.RunOrderSyncExclusiveAsync(async () =>
{
await _backend.ProductionSync.SyncNowAsync().ConfigureAwait(false);
return true;
}).ConfigureAwait(false);
FileLogger.Info("OrderSync", $"Background order sync completed ({trigger}).");
}
catch (Exception ex)
{
FileLogger.Error("OrderSync", $"Background order sync failed ({trigger}).", ex);
Logger.Log(ex, "CanteenBackendWorker.RunOrderSyncSafeAsync");
}
finally
{
Interlocked.Exchange(ref _orderSyncRunning, 0);
}
}
}