62 lines
3.7 KiB
C#
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);
|
|
}
|
|
}
|