using Microsoft.EntityFrameworkCore; using ROLAC.API.Data.Logging; using ROLAC.API.Entities.Logging; namespace ROLAC.API.Services.Logging; /// /// The single consumer that drains and batch-inserts rows through /// the dedicated (a fresh DI scope per batch). Persistence failures /// are swallowed to Console.Error only — they must never propagate back into the logging /// pipeline or crash the host. /// public sealed class LogWriterBackgroundService : BackgroundService { private const int MaxBatchSize = 200; private static readonly TimeSpan FlushInterval = TimeSpan.FromSeconds(1); private readonly SystemLogQueue _queue; private readonly IServiceScopeFactory _scopeFactory; public LogWriterBackgroundService(SystemLogQueue queue, IServiceScopeFactory scopeFactory) { _queue = queue; _scopeFactory = scopeFactory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var systemBatch = new List(MaxBatchSize); var auditBatch = new List(MaxBatchSize); try { await foreach (var envelope in _queue.ReadAllAsync(stoppingToken)) { if (envelope.System is not null) systemBatch.Add(envelope.System); if (envelope.Audit is not null) auditBatch.Add(envelope.Audit); // Coalesce a short burst into one round-trip; flush on size or a brief idle. if (systemBatch.Count + auditBatch.Count >= MaxBatchSize) { await FlushAsync(systemBatch, auditBatch, stoppingToken); continue; } if (!await WaitForMoreAsync(FlushInterval, stoppingToken)) await FlushAsync(systemBatch, auditBatch, stoppingToken); } } catch (OperationCanceledException) { // Shutting down — drain whatever is buffered. } await FlushAsync(systemBatch, auditBatch, CancellationToken.None); } /// Brief debounce so bursts coalesce; returns false once the window elapses. private static async Task WaitForMoreAsync(TimeSpan window, CancellationToken token) { try { await Task.Delay(window, token); return false; } catch (OperationCanceledException) { return false; } } private async Task FlushAsync( List systemBatch, List auditBatch, CancellationToken token) { if (systemBatch.Count == 0 && auditBatch.Count == 0) return; try { using var scope = _scopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); if (systemBatch.Count > 0) db.SystemLogs.AddRange(systemBatch); if (auditBatch.Count > 0) db.AuditLogs.AddRange(auditBatch); await db.SaveChangesAsync(token); } catch (Exception ex) { // Last resort: never throw out of the log writer. Include the inner exception — // the EF wrapper message alone ("An error occurred while saving...") hides the cause. var detail = ex.InnerException is null ? ex.Message : $"{ex.Message} -> {ex.InnerException.Message}"; await Console.Error.WriteLineAsync( $"[LogWriter] Failed to persist {systemBatch.Count} system + {auditBatch.Count} audit rows: {detail}"); } finally { systemBatch.Clear(); auditBatch.Clear(); } } }