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 _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 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); } } }