fix(site-call-audit): an unknown-status row no longer aborts the rest of a batched page (review 2026-10-21 06/PERF-2 follow-up)
This commit is contained in:
+26
-5
@@ -196,19 +196,40 @@ public class SiteCallAuditRepository : ISiteCallAuditRepository
|
|||||||
// rethrown at the end, so the caller sees the same exception the old per-row
|
// rethrown at the end, so the caller sees the same exception the old per-row
|
||||||
// loop surfaced (and keeps its cursor), minus the collateral of the rows behind it.
|
// loop surfaced (and keeps its cursor), minus the collateral of the rows behind it.
|
||||||
System.Runtime.ExceptionServices.ExceptionDispatchInfo? firstFailure = null;
|
System.Runtime.ExceptionServices.ExceptionDispatchInfo? firstFailure = null;
|
||||||
for (var offset = 0; offset < siteCalls.Count; offset += UpsertManyChunkSize)
|
// Rows UpsertAsync would reject before touching the database (a null row, an
|
||||||
|
// unknown status) are validated up front and SKIPPED with their failure captured,
|
||||||
|
// so one malformed row cannot stop the rest of the page from applying.
|
||||||
|
var valid = new List<(SiteCall Row, int Rank)>(siteCalls.Count);
|
||||||
|
foreach (var siteCall in siteCalls)
|
||||||
{
|
{
|
||||||
var chunk = siteCalls.Skip(offset).Take(UpsertManyChunkSize).ToList();
|
try
|
||||||
|
{
|
||||||
|
if (siteCall is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("A null SiteCall row was supplied.", nameof(siteCalls));
|
||||||
|
}
|
||||||
|
|
||||||
|
valid.Add((siteCall, GetRankOrThrow(siteCall.Status)));
|
||||||
|
}
|
||||||
|
catch (ArgumentException invalid)
|
||||||
|
{
|
||||||
|
firstFailure ??= System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(invalid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var offset = 0; offset < valid.Count; offset += UpsertManyChunkSize)
|
||||||
|
{
|
||||||
|
var chunk = valid.Skip(offset).Take(UpsertManyChunkSize).Select(v => v.Row).ToList();
|
||||||
|
var ranks = valid.Skip(offset).Take(UpsertManyChunkSize).Select(v => v.Rank).ToList();
|
||||||
var sql = new System.Text.StringBuilder();
|
var sql = new System.Text.StringBuilder();
|
||||||
var parameters = new List<object>(chunk.Count * 15);
|
var parameters = new List<object>(chunk.Count * 15);
|
||||||
for (var i = 0; i < chunk.Count; i++)
|
for (var i = 0; i < chunk.Count; i++)
|
||||||
{
|
{
|
||||||
var siteCall = chunk[i] ?? throw new ArgumentException("A null SiteCall row was supplied.", nameof(siteCalls));
|
var siteCall = chunk[i];
|
||||||
var rank = GetRankOrThrow(siteCall.Status);
|
|
||||||
var suffix = "_" + i.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
var suffix = "_" + i.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
sql.AppendLine(SuffixParameters(InsertIfAbsentSql, suffix));
|
sql.AppendLine(SuffixParameters(InsertIfAbsentSql, suffix));
|
||||||
sql.AppendLine(SuffixParameters(MonotonicUpdateSql, suffix));
|
sql.AppendLine(SuffixParameters(MonotonicUpdateSql, suffix));
|
||||||
foreach (SqlParameter p in BuildUpsertParameters(siteCall, siteCall.TrackedOperationId.Value.ToString("D"), rank))
|
foreach (SqlParameter p in BuildUpsertParameters(siteCall, siteCall.TrackedOperationId.Value.ToString("D"), ranks[i]))
|
||||||
{
|
{
|
||||||
p.ParameterName += suffix;
|
p.ParameterName += suffix;
|
||||||
parameters.Add(p);
|
parameters.Add(p);
|
||||||
|
|||||||
+34
@@ -1018,6 +1018,40 @@ public class SiteCallAuditRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
|||||||
Assert.Equal("Submitted", stored.Status);
|
Assert.Equal("Submitted", stored.Status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that a row with an unknown status does not stop the rest of the page: every
|
||||||
|
/// valid row is applied and the unknown-status failure is rethrown at the end (adversarial
|
||||||
|
/// review of 06/PERF-2).
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[SkippableFact]
|
||||||
|
public async Task UpsertManyAsync_UnknownStatus_StillAppliesTheRestOfThePage()
|
||||||
|
{
|
||||||
|
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
|
||||||
|
var site = NewSiteId();
|
||||||
|
var good1 = TrackedOperationId.New();
|
||||||
|
var bad = TrackedOperationId.New();
|
||||||
|
var good2 = TrackedOperationId.New();
|
||||||
|
var page = new List<SiteCall>
|
||||||
|
{
|
||||||
|
NewRow(good1, site, status: "Submitted"),
|
||||||
|
NewRow(bad, site, status: "NotAStatus"),
|
||||||
|
NewRow(good2, site, status: "Submitted"),
|
||||||
|
};
|
||||||
|
|
||||||
|
await using (var context = CreateContext())
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<ArgumentException>(
|
||||||
|
() => new SiteCallAuditRepository(context).UpsertManyAsync(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var read = CreateContext();
|
||||||
|
var repo = new SiteCallAuditRepository(read);
|
||||||
|
Assert.NotNull(await repo.GetAsync(good1));
|
||||||
|
Assert.NotNull(await repo.GetAsync(good2));
|
||||||
|
Assert.Null(await repo.GetAsync(bad));
|
||||||
|
}
|
||||||
|
|
||||||
private ScadaBridgeDbContext CreateContext(DbCommandInterceptor interceptor)
|
private ScadaBridgeDbContext CreateContext(DbCommandInterceptor interceptor)
|
||||||
{
|
{
|
||||||
var options = new DbContextOptionsBuilder<ScadaBridgeDbContext>()
|
var options = new DbContextOptionsBuilder<ScadaBridgeDbContext>()
|
||||||
|
|||||||
Reference in New Issue
Block a user