fix(IPC-33): bound the DrainEvents byte budget against the gRPC send cap, not just the pipe frame max
The worker budgeted a DrainEvents reply against the negotiated pipe frame maximum less one 64 KiB reserve, but the gateway returns that same MxCommandReply verbatim through Invoke, where MaxSendMessageSize applies. The two limits were coupled only from below (pipe >= gRPC + reserve), so any configuration the validator permitted with a wider gap let the worker pack a reply the pipe carried and the gRPC layer then refused with ResourceExhausted -- after Drain had already removed the events from the worker queue, losing them to both the caller and the event stream. Option (a) from the finding: close the coupling at both ends. - GatewayOptionsValidator adds the upper half of the headroom rule (Worker:MaxMessageBytes <= Protocol:MaxGrpcMessageBytes + 2 x reserve), with a message naming the DrainEvents budget as the reason and the two knobs to move. - WorkerPipeSession.ResolveDrainReplyByteBudget subtracts the reserve twice -- once for the pipe frame, once so the MxCommandReply wrapper clears the gRPC send cap -- keeping the MaxMessageBytes/2 floor that makes small frame maxima usable. The two changes are load-bearing for each other: the validator band is what makes "frame max - 2 x reserve" provably at or below the gRPC cap, and the doubled subtraction is what makes the band sufficient. Docs updated in the same change (GatewayConfiguration.md both option rows plus the headroom prose, WorkerFrameProtocol.md drain budget, gateway.md DrainEvents byte axis). Tests: new GatewayOptionsValidatorTests cases for the upper bound (reject) and both edges of the band (accept); GatewayOptionsTests' override case now moves both message-size knobs together, since the band makes moving one alone illegal. The worker half cannot be built on macOS; verified on windev separately.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
using ZB.MOM.WW.MxGateway.Server.Workers;
|
||||
using LdapTransport = ZB.MOM.WW.Auth.Abstractions.Ldap.LdapTransport;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Configuration;
|
||||
@@ -1256,6 +1257,57 @@ public sealed class GatewayOptionsValidatorTests
|
||||
Assert.Contains(result.Failures!, f => f.Contains("MaxMessageBytes") && f.Contains("reserve"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the headroom rule's upper half (IPC-33): a worker-frame maximum more than two
|
||||
/// envelope reserves above the gRPC cap is rejected. The worker budgets a DrainEvents reply at
|
||||
/// the frame maximum less two reserves and the gateway returns that same reply through
|
||||
/// <c>Invoke</c>, so a wider gap lets the worker pack a reply the pipe carries and the gRPC send
|
||||
/// cap refuses — with the drained events already removed from the worker queue.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_Fails_WhenWorkerFrameMaxExceedsGrpcMaxByMoreThanTwoReserves()
|
||||
{
|
||||
const int grpcMax = 16 * 1024 * 1024;
|
||||
int tooLarge = grpcMax + (2 * WorkerFrameProtocolOptions.EnvelopeOverheadReserveBytes) + 1;
|
||||
|
||||
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(
|
||||
null,
|
||||
WithWorkerAndProtocol(
|
||||
new WorkerOptions { MaxMessageBytes = tooLarge },
|
||||
new ProtocolOptions { MaxGrpcMessageBytes = grpcMax }));
|
||||
|
||||
Assert.True(result.Failed);
|
||||
Assert.Contains(
|
||||
result.Failures!,
|
||||
f => f.Contains("MaxMessageBytes", StringComparison.Ordinal)
|
||||
&& f.Contains("at most", StringComparison.Ordinal)
|
||||
&& f.Contains("DrainEvents", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies both edges of the two-sided headroom rule are themselves accepted: exactly one
|
||||
/// reserve above the gRPC cap (the shipped default's shape) and exactly two reserves above it
|
||||
/// (the widest legal gap). Pinning the boundaries keeps a future retune of the reserve from
|
||||
/// silently making the shipped default illegal.
|
||||
/// </summary>
|
||||
/// <param name="reserveMultiple">Number of envelope reserves between the gRPC cap and the frame maximum.</param>
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
public void Validate_Succeeds_AtBothEdgesOfTheFrameHeadroomBand(int reserveMultiple)
|
||||
{
|
||||
const int grpcMax = 16 * 1024 * 1024;
|
||||
int workerMax = grpcMax + (reserveMultiple * WorkerFrameProtocolOptions.EnvelopeOverheadReserveBytes);
|
||||
|
||||
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(
|
||||
null,
|
||||
WithWorkerAndProtocol(
|
||||
new WorkerOptions { MaxMessageBytes = workerMax },
|
||||
new ProtocolOptions { MaxGrpcMessageBytes = grpcMax }));
|
||||
|
||||
Assert.True(result.Succeeded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the shipped worker event-queue capacity default passes validation and still matches
|
||||
/// the worker-side <c>MxAccessEventQueue.DefaultCapacity</c> the environment variable falls back
|
||||
|
||||
Reference in New Issue
Block a user