Files
jobtrackingapp/JobTrackerApi/Services/OperationEventLog.cs
T

62 lines
3.7 KiB
C#

using JobTrackerApi.Data;
using JobTrackerApi.Models;
using Microsoft.EntityFrameworkCore;
namespace JobTrackerApi.Services;
public static class OperationEventLog
{
public const int RetentionDays = 30;
public static string SafeStatus(string? value) => value is
OperationStatuses.Queued or OperationStatuses.Running or OperationStatuses.WaitingForRetry or OperationStatuses.WaitingForExternalFallback
or OperationStatuses.Succeeded or OperationStatuses.Failed or OperationStatuses.Cancelled ? value : "unknown";
public static string? SafeStage(string? value) => value is "cv_validating_file" or "cv_extracting" or "cv_identifying_sections"
or "cv_analysing" or "cv_validating_details" or "cv_preparing_review" or "cv_review_ready" ? value : null;
public static IReadOnlyList<string> FailureCodes { get; } = Array.AsReadOnly(new[]
{
"timeout", "lease_expired", "deadline_exceeded", "entitlement_changed", "document_unavailable",
"sidecar_unavailable", "upstream_unavailable", "invalid_response", "circuit_open", "rate_limited",
"permission_denied", "cancelled", "upload_rejected", "parser_failed", "malformed_output", "provider_unavailable",
});
public static string? SafeFailure(string? value) => value is null ? null : FailureCodes.Contains(value, StringComparer.Ordinal) ? value : "processing_failed";
public static UserOperationEvent Create(UserOperation operation, OperationEventKind kind, DateTime now,
string? status = null, string? stage = null, string? failure = null, int? attempt = null) => new()
{
OperationId = operation.Id, OwnerUserId = operation.OwnerUserId, AtUtc = now, Kind = kind.ToString(),
Status = SafeStatus(status ?? operation.Status), Stage = SafeStage(stage ?? operation.ProgressStage),
Attempt = Math.Clamp(attempt ?? operation.AttemptCount, 0, 10), FailureCode = SafeFailure(failure),
};
public static async Task AppendAsync(JobTrackerContext db, UserOperation operation, OperationEventKind kind, DateTime now,
CancellationToken token, string? status = null, string? stage = null, string? failure = null, int? attempt = null)
{
var item = Create(operation, kind, now, status, stage, failure, attempt);
if (!db.Database.IsRelational())
{
db.UserOperationEvents.Add(item);
await db.SaveChangesAsync(token);
return;
}
if (db.Database.CurrentTransaction is null) throw new InvalidOperationException("Operation history requires the state transition transaction.");
// Parameterized insert avoids flushing unrelated entities tracked by a caller.
await db.Database.ExecuteSqlInterpolatedAsync($"INSERT INTO `UserOperationEvents` (`OperationId`,`OwnerUserId`,`AtUtc`,`Kind`,`Status`,`Stage`,`Attempt`,`FailureCode`) VALUES ({item.OperationId},{item.OwnerUserId},{item.AtUtc},{item.Kind},{item.Status},{item.Stage},{item.Attempt},{item.FailureCode})", token);
}
public static async Task<int> PruneAsync(JobTrackerContext db, DateTime now, CancellationToken token)
{
var cutoff = now.AddDays(-RetentionDays);
var ids = await db.UserOperationEvents.IgnoreQueryFilters().Where(item => item.AtUtc < cutoff)
.OrderBy(item => item.Id).Select(item => item.Id).Take(5000).ToListAsync(token);
if (!db.Database.IsRelational())
{
var items = await db.UserOperationEvents.IgnoreQueryFilters().Where(item => ids.Contains(item.Id)).ToListAsync(token);
db.UserOperationEvents.RemoveRange(items);
return await db.SaveChangesAsync(token);
}
return await db.UserOperationEvents.IgnoreQueryFilters().Where(item => ids.Contains(item.Id)).ExecuteDeleteAsync(token);
}
}