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
|
||||
// loop surfaced (and keeps its cursor), minus the collateral of the rows behind it.
|
||||
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 parameters = new List<object>(chunk.Count * 15);
|
||||
for (var i = 0; i < chunk.Count; i++)
|
||||
{
|
||||
var siteCall = chunk[i] ?? throw new ArgumentException("A null SiteCall row was supplied.", nameof(siteCalls));
|
||||
var rank = GetRankOrThrow(siteCall.Status);
|
||||
var siteCall = chunk[i];
|
||||
var suffix = "_" + i.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
sql.AppendLine(SuffixParameters(InsertIfAbsentSql, 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;
|
||||
parameters.Add(p);
|
||||
|
||||
+34
@@ -1018,6 +1018,40 @@ public class SiteCallAuditRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
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)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<ScadaBridgeDbContext>()
|
||||
|
||||
Reference in New Issue
Block a user