255 lines
10 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|