Resolve all high-severity architecture review findings
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
+55
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user