feat(cv)!: queue durable processing
CV upload now returns 202 with an owner-scoped operation instead of holding the request through parsing. Existing review approval remains required. BREAKING CHANGE: profile-cv upload responses use the durable operation contract.
This commit is contained in:
@@ -152,6 +152,12 @@ public sealed partial class ProfileCvController : ControllerBase
|
||||
}
|
||||
|
||||
var artifact = await SaveUploadArtifactAsync(user, file, HttpContext.RequestAborted);
|
||||
var activeRun = await FindActiveRunAsync(user.Id, "upload", null, artifact.Sha256, HttpContext.RequestAborted);
|
||||
if (activeRun is not null)
|
||||
{
|
||||
TryDeleteCvArtifactFile(artifact.StoragePath);
|
||||
return await EnqueueRunAsync(activeRun, HttpContext.RequestAborted);
|
||||
}
|
||||
_db.CvUploadArtifacts.Add(artifact);
|
||||
await _db.SaveChangesAsync(HttpContext.RequestAborted);
|
||||
|
||||
@@ -163,42 +169,12 @@ public sealed partial class ProfileCvController : ControllerBase
|
||||
ParserVersion = ParserVersion,
|
||||
NormalizerVersion = NormalizerVersion,
|
||||
LlmPromptVersion = LlmPromptVersion,
|
||||
Status = "running",
|
||||
Status = "queued",
|
||||
StartedAtUtc = DateTimeOffset.UtcNow,
|
||||
};
|
||||
_db.CvExtractionRuns.Add(run);
|
||||
await _db.SaveChangesAsync(HttpContext.RequestAborted);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await ExtractStructuredCvFromFileAsync(file, extension, HttpContext.RequestAborted);
|
||||
run.RawExtractedText = result.RawText;
|
||||
run.NormalizedText = result.NormalizedText;
|
||||
run.StructuredProfileJson = StructuredCvProfileJson.Serialize(result.StructuredCv);
|
||||
run.Status = "pending_review";
|
||||
run.CompletedAtUtc = DateTimeOffset.UtcNow;
|
||||
await _db.SaveChangesAsync(HttpContext.RequestAborted);
|
||||
await PruneExtractionRunsAsync(user.Id, HttpContext.RequestAborted);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
imported = false,
|
||||
pendingReview = true,
|
||||
characters = result.NormalizedText.Length,
|
||||
artifactId = artifact.Id,
|
||||
extractionRunId = run.Id,
|
||||
status = run.Status,
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
run.Status = "failed";
|
||||
run.ErrorMessage = ex.Message;
|
||||
run.CompletedAtUtc = DateTimeOffset.UtcNow;
|
||||
await _db.SaveChangesAsync(HttpContext.RequestAborted);
|
||||
await PruneExtractionRunsAsync(user.Id, HttpContext.RequestAborted);
|
||||
throw;
|
||||
}
|
||||
return await EnqueueRunAsync(run, HttpContext.RequestAborted);
|
||||
}
|
||||
|
||||
[HttpGet("runs")]
|
||||
@@ -221,11 +197,23 @@ public sealed partial class ProfileCvController : ControllerBase
|
||||
x.ParserVersion,
|
||||
x.NormalizerVersion,
|
||||
x.LlmPromptVersion,
|
||||
x.ErrorMessage));
|
||||
x.ErrorMessage,
|
||||
null));
|
||||
var runs = _db.Database.IsSqlite()
|
||||
? (await runsQuery.ToListAsync(HttpContext.RequestAborted)).OrderByDescending(x => x.StartedAtUtc).Take(10).ToList()
|
||||
: await runsQuery.OrderByDescending(x => x.StartedAtUtc).Take(10).ToListAsync(HttpContext.RequestAborted);
|
||||
|
||||
var runIds = runs.Select(run => run.Id.ToString(System.Globalization.CultureInfo.InvariantCulture)).ToList();
|
||||
var operations = await _db.UserOperations.AsNoTracking()
|
||||
.Where(operation => operation.TaskType == CvProcessingQueue.TaskType && operation.SubjectId != null && runIds.Contains(operation.SubjectId))
|
||||
.ToListAsync(HttpContext.RequestAborted);
|
||||
var latestOperations = operations
|
||||
.GroupBy(operation => operation.SubjectId!, StringComparer.Ordinal)
|
||||
.ToDictionary(group => group.Key, group => group.MaxBy(operation => operation.CreatedAtUtc)!);
|
||||
runs = runs.Select(run => latestOperations.TryGetValue(run.Id.ToString(System.Globalization.CultureInfo.InvariantCulture), out var operation)
|
||||
? run with { Operation = OperationDto.From(operation) }
|
||||
: run).ToList();
|
||||
|
||||
return Ok(runs);
|
||||
}
|
||||
|
||||
@@ -313,8 +301,7 @@ public sealed partial class ProfileCvController : ControllerBase
|
||||
}
|
||||
|
||||
var run = await CreateQueuedRunAsync(user.Id, artifact.Id, "reprocess", HttpContext.RequestAborted);
|
||||
await _cvProcessingQueue.EnqueueAsync(run.Id, HttpContext.RequestAborted);
|
||||
return Accepted(new { queued = true, extractionRunId = run.Id, status = run.Status });
|
||||
return await EnqueueRunAsync(run, HttpContext.RequestAborted);
|
||||
}
|
||||
|
||||
[HttpPost("rebuild")]
|
||||
@@ -326,8 +313,7 @@ public sealed partial class ProfileCvController : ControllerBase
|
||||
if (string.IsNullOrWhiteSpace(user.ProfileCvText)) return BadRequest("Add or import CV text before rebuilding it.");
|
||||
|
||||
var run = await CreateQueuedRunAsync(user.Id, user.CurrentCvUploadArtifactId, "rebuild", HttpContext.RequestAborted);
|
||||
await _cvProcessingQueue.EnqueueAsync(run.Id, HttpContext.RequestAborted);
|
||||
return Accepted(new { queued = true, extractionRunId = run.Id, status = run.Status });
|
||||
return await EnqueueRunAsync(run, HttpContext.RequestAborted);
|
||||
}
|
||||
|
||||
[HttpPost("rewrite-section")]
|
||||
@@ -527,8 +513,7 @@ public sealed partial class ProfileCvController : ControllerBase
|
||||
if (string.IsNullOrWhiteSpace(user.ProfileCvText)) return BadRequest("Add or import CV text before improving it.");
|
||||
|
||||
var run = await CreateQueuedRunAsync(user.Id, user.CurrentCvUploadArtifactId, "improve", HttpContext.RequestAborted);
|
||||
await _cvProcessingQueue.EnqueueAsync(run.Id, HttpContext.RequestAborted);
|
||||
return Accepted(new { queued = true, extractionRunId = run.Id, status = run.Status });
|
||||
return await EnqueueRunAsync(run, HttpContext.RequestAborted);
|
||||
}
|
||||
|
||||
private static string BuildRewriteSourceText(string? sectionName, string? sourceText, StructuredCvProfile structuredCv)
|
||||
|
||||
Reference in New Issue
Block a user