Resolve all high-severity architecture review findings

This commit is contained in:
Joseph Doherty
2026-09-09 16:13:58 -04:00
parent 8bfe65512e
commit 146a13238f
192 changed files with 3160 additions and 753 deletions
@@ -0,0 +1,39 @@
using Grpc.Core;
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
namespace ZB.MOM.WW.ScadaBridge.Communication.Tests.Grpc;
public class AuditWireBudgetTests
{
[Fact]
public void OversizeSoftBudgetRow_TravelsAloneWithoutTruncation()
{
var batch = new AuditEventBatch();
var bytes = AuditWireBudget.InitialBytes;
var row = new AuditEventDto { EventId = "large", ErrorDetail = new string('x', GrpcMessageLimits.AuditBatchBytes + 1) };
Assert.True(AuditWireBudget.TryAppend(batch.Events, row, ref bytes, row.EventId));
Assert.False(AuditWireBudget.TryAppend(batch.Events, new AuditEventDto { EventId = "next" }, ref bytes, "next"));
Assert.Same(row, Assert.Single(batch.Events));
Assert.Equal(row.ErrorDetail, batch.Events[0].ErrorDetail);
Assert.True(bytes < GrpcMessageLimits.MaxMessageBytes);
}
[Fact]
public void HardOversizeRow_DoesNotBlockGoodPrefix_AndFailsExplicitlyWhenFirst()
{
var good = new AuditEventDto { EventId = "good", ErrorMessage = "small" };
var huge = new AuditEventDto { EventId = "retained-id", ErrorDetail = new string('x', GrpcMessageLimits.MaxMessageBytes) };
var batch = new AuditEventBatch();
var bytes = AuditWireBudget.InitialBytes;
Assert.True(AuditWireBudget.TryAppend(batch.Events, good, ref bytes, good.EventId));
Assert.False(AuditWireBudget.TryAppend(batch.Events, huge, ref bytes, huge.EventId));
Assert.Equal("good", Assert.Single(batch.Events).EventId);
var next = new AuditEventBatch();
bytes = AuditWireBudget.InitialBytes;
var ex = Assert.Throws<RpcException>(() => AuditWireBudget.TryAppend(next.Events, huge, ref bytes, huge.EventId));
Assert.Equal(StatusCode.ResourceExhausted, ex.StatusCode);
Assert.Contains("retained-id", ex.Status.Detail);
Assert.Contains("retained locally", ex.Status.Detail);
Assert.Empty(next.Events);
}
}
@@ -19,6 +19,61 @@ public class SitePairChannelProviderDisposalTests
Options.Create(new CommunicationOptions()),
NullLogger<SitePairChannelProvider>.Instance);
[Fact]
public async Task ApplyThenUnavailable_DoesNotReplayAnAmbiguousOperation()
{
using var provider = Build();
provider.UpdateSite("site-1", "http://localhost:15101", "http://localhost:15102");
var effects = 0;
var error = await Record.ExceptionAsync(() => provider.ExecuteAsync("site-1", (channel, ct) =>
{
effects++;
if (effects == 1)
throw new global::Grpc.Core.RpcException(new global::Grpc.Core.Status(global::Grpc.Core.StatusCode.Unavailable, "response connection lost after apply"));
return Task.FromResult(42);
}, CancellationToken.None));
Assert.Equal(1, effects);
Assert.NotNull(error);
}
[Fact]
public async Task SecondaryAppliesThenLosesResponse_AlsoReportsUnknownWithoutAnotherReplay()
{
using var provider = Build();
provider.UpdateSite("site-1", "http://localhost:15101", "http://localhost:15102");
var attempts = 0;
var effects = 0;
var error = await Assert.ThrowsAsync<global::Grpc.Core.RpcException>(() => provider.ExecuteAsync<int>("site-1", (channel, ct) =>
{
if (++attempts == 1) throw GrpcRetrySafety.NotDispatched("starting");
effects++;
throw new global::Grpc.Core.RpcException(new global::Grpc.Core.Status(global::Grpc.Core.StatusCode.Unavailable, "applied, reply lost"));
}, CancellationToken.None));
Assert.Equal(global::Grpc.Core.StatusCode.Aborted, error.StatusCode);
Assert.Contains("OUTCOME_UNKNOWN", error.Status.Detail);
Assert.Equal(2, attempts);
Assert.Equal(1, effects);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task SafeReadOrExplicitPreDispatchRejection_CanRetry(bool safeRead)
{
using var provider = Build();
provider.UpdateSite("site-1", "http://localhost:15101", "http://localhost:15102");
var attempts = 0;
var result = await provider.ExecuteAsync("site-1", (channel, ct) =>
{
if (++attempts == 1)
throw safeRead ? new global::Grpc.Core.RpcException(new global::Grpc.Core.Status(global::Grpc.Core.StatusCode.Unavailable, "lost reply"))
: GrpcRetrySafety.NotDispatched("not ready");
return Task.FromResult(42);
}, CancellationToken.None, retrySafe: safeRead);
Assert.Equal(42, result);
Assert.Equal(2, attempts);
}
[Fact]
public async Task UpdateSite_WhileCallInFlight_ParksTheOldChannelAndLetsTheCallComplete()
{
@@ -44,6 +44,34 @@ public class SiteStreamPullAuditEventsTests : TestKit
?? DateTime.SpecifyKind(new DateTime(2026, 5, 20, 10, 0, 0), DateTimeKind.Utc),
sourceSiteId: "site-1");
[Fact]
public async Task LargeAuditRows_ArePagedByWireBytes_AndCursorMakesProgress()
{
var rows = Enumerable.Range(0, 256).Select(i => ScadaBridgeAuditEventFactory.Create(
channel: AuditChannel.ApiOutbound, kind: AuditKind.ApiCall, status: AuditStatus.Failed,
occurredAtUtc: DateTime.UtcNow.AddSeconds(i), sourceSiteId: "site-1",
errorDetail: new string('界', 64 * 1024 / 3), errorMessage: new string('x', 2048))).ToList();
var queue = Substitute.For<ISiteAuditQueue>();
queue.ReadPendingSinceAsync(Arg.Any<DateTime>(), Arg.Any<int>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
.Returns(c => (IReadOnlyList<AuditEvent>)rows.Skip(c.ArgAt<string?>(2) is { } id
? rows.FindIndex(e => e.EventId.ToString() == id) + 1 : 0).Take(c.ArgAt<int>(1)).ToList());
var server = CreateServer();
server.SetSiteAuditQueue(queue);
var request = new PullAuditEventsRequest { BatchSize = 256 };
var seen = new HashSet<string>();
while (true)
{
var response = await server.PullAuditEvents(request, NewContext());
Assert.True(response.CalculateSize() + 5 <= 2 * 1024 * 1024);
foreach (var row in response.Events) Assert.True(seen.Add(row.EventId));
if (!response.MoreAvailable) break;
Assert.NotEmpty(response.Events);
request.AfterId = response.Events[^1].EventId;
request.SinceUtc = response.Events[^1].OccurredAtUtc;
}
Assert.Equal(256, seen.Count);
}
[Fact]
public async Task PullAuditEvents_NoQueueWired_ReturnsEmptyResponse()
{