fix(cv-qa): protect import lifecycle races and terminal receipts
This commit is contained in:
@@ -53,15 +53,25 @@ public sealed class CvImportRetentionHostedService(IServiceScopeFactory scopes,
|
||||
catch (IOException) { continue; }
|
||||
catch (UnauthorizedAccessException) { continue; }
|
||||
artifact.StoragePath = "";
|
||||
run.DocumentJson = null;
|
||||
run.RawExtractedText = null;
|
||||
run.NormalizedText = null;
|
||||
if (run.Status != "applied")
|
||||
for (var attempt = 0; ; attempt++)
|
||||
{
|
||||
run.StructuredProfileJson = null;
|
||||
if (run.Status is "pending_review" or "queued" or "running") run.Status = "expired";
|
||||
run.CompletedAtUtc ??= now;
|
||||
run.ErrorMessage = "The source document expired. Upload it again to start a new review.";
|
||||
run.DocumentJson = null;
|
||||
run.RawExtractedText = null;
|
||||
run.NormalizedText = null;
|
||||
if (run.Status != "applied")
|
||||
{
|
||||
run.StructuredProfileJson = null;
|
||||
if (run.Status is "pending_review" or "queued" or "running") run.Status = "expired";
|
||||
run.CompletedAtUtc ??= now;
|
||||
run.ErrorMessage = "The source document expired. Upload it again to start a new review.";
|
||||
}
|
||||
try { await db.SaveChangesAsync(token); break; }
|
||||
catch (DbUpdateConcurrencyException ex) when (attempt < 2 && ex.Entries.All(e => ReferenceEquals(e.Entity, run)))
|
||||
{
|
||||
// Retry source cleanup against the new status; never replay canonical profile writes.
|
||||
await db.Entry(run).ReloadAsync(token);
|
||||
if (db.Entry(run).State == EntityState.Detached) break;
|
||||
}
|
||||
}
|
||||
removed++;
|
||||
}
|
||||
|
||||
@@ -355,6 +355,8 @@ public sealed class UserOperationStore(JobTrackerContext db, TimeProvider timePr
|
||||
await using var transaction = await BeginTransactionAsync(cancellationToken);
|
||||
var operation = await db.UserOperations.FirstOrDefaultAsync(item => item.Id == operationId, cancellationToken);
|
||||
if (operation is null || operation.Status is not (OperationStatuses.Failed or OperationStatuses.Cancelled)) return false;
|
||||
if (operation.TaskType == CvProcessingQueue.TaskType && operation.SubjectType == CvProcessingQueue.SubjectType
|
||||
&& await SynchronizeCvRunAsync(operation, OperationStatuses.Queued, null, UtcNow, cancellationToken) != 1) return false;
|
||||
operation.Status = OperationStatuses.Queued;
|
||||
operation.AttemptCount = 0;
|
||||
operation.AvailableAtUtc = UtcNow;
|
||||
@@ -371,7 +373,6 @@ public sealed class UserOperationStore(JobTrackerContext db, TimeProvider timePr
|
||||
var existingNotification = await db.UserNotifications.FirstOrDefaultAsync(item => item.OperationId == operationId, cancellationToken);
|
||||
if (existingNotification is not null) db.UserNotifications.Remove(existingNotification);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
await SynchronizeCvRunAsync(operation, OperationStatuses.Queued, null, UtcNow, cancellationToken);
|
||||
if (transaction is not null) await transaction.CommitAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
@@ -475,7 +476,8 @@ public sealed class UserOperationStore(JobTrackerContext db, TimeProvider timePr
|
||||
: (DateTimeOffset?)null;
|
||||
|
||||
return db.CvExtractionRuns.IgnoreQueryFilters()
|
||||
.Where(run => run.Id == runId && run.OwnerUserId == operation.OwnerUserId && run.Status != "expired" && run.Status != "applied")
|
||||
.Where(run => run.Id == runId && run.OwnerUserId == operation.OwnerUserId
|
||||
&& (run.Status == "queued" || run.Status == "running" || run.Status == "pending_review" || run.Status == "failed" || run.Status == "cancelled"))
|
||||
.ExecuteUpdateAsync(setters => setters
|
||||
.SetProperty(run => run.Status, runStatus)
|
||||
.SetProperty(run => run.ErrorMessage, message)
|
||||
|
||||
Reference in New Issue
Block a user