@@ -0,0 +1,102 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ROLAC.API.Data.Logging;
|
||||
using ROLAC.API.Entities.Logging;
|
||||
|
||||
namespace ROLAC.API.Services.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// The single consumer that drains <see cref="SystemLogQueue"/> and batch-inserts rows through
|
||||
/// the dedicated <see cref="LogDbContext"/> (a fresh DI scope per batch). Persistence failures
|
||||
/// are swallowed to <c>Console.Error</c> only — they must never propagate back into the logging
|
||||
/// pipeline or crash the host.
|
||||
/// </summary>
|
||||
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<SystemLog>(MaxBatchSize);
|
||||
var auditBatch = new List<AuditLog>(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);
|
||||
}
|
||||
|
||||
/// <summary>Brief debounce so bursts coalesce; returns false once the window elapses.</summary>
|
||||
private static async Task<bool> WaitForMoreAsync(TimeSpan window, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(window, token);
|
||||
return false;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FlushAsync(
|
||||
List<SystemLog> systemBatch, List<AuditLog> auditBatch, CancellationToken token)
|
||||
{
|
||||
if (systemBatch.Count == 0 && auditBatch.Count == 0)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<LogDbContext>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user