feat(ipc): negotiate worker frame max, add gRPC headroom, bound DrainEvents (IPC-02/03/04 gateway half)
Proto foundation + gateway-side of the size/backpressure pass: - IPC-02: add GatewayHello.max_frame_bytes (regen Generated/); gateway sends its negotiated worker-frame max in the handshake so the worker can adopt it instead of a hard-coded default. Worker read-half lands separately. - IPC-03: give the pipe frame max envelope-overhead headroom above the public gRPC cap (WorkerFrameProtocolOptions.EnvelopeOverheadReserveBytes = 64 KiB; default Worker.MaxMessageBytes bumped to 16 MiB + reserve), cross-validate the headroom at startup, and pre-check command envelope size in WorkerClient so an oversized command fails only that correlation (ResourceExhausted) instead of faulting the whole session. - IPC-04: reject DrainEvents max_events above a public ceiling in the request validator (worker per-reply cap lands with the worker half). Docs: GatewayConfiguration, WorkerFrameProtocol, gateway.md. Tests: headroom validation, DrainEvents bound, oversized-command per-command failure (pipe-harness, verified on windev).
This commit is contained in:
@@ -56,6 +56,52 @@ public sealed class WorkerClientTests
|
||||
Assert.Equal(MxCommandKind.Ping, reply.Reply.Kind);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a command whose serialized envelope exceeds the negotiated worker-frame maximum
|
||||
/// fails only that command with <see cref="WorkerClientErrorCode.CommandTooLarge"/> at the enqueue
|
||||
/// boundary, leaving the client ready for subsequent commands (IPC-03). Without the pre-check the
|
||||
/// oversized frame would reach the write loop and fault the whole session.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task InvokeAsync_WhenCommandExceedsFrameMax_FailsOnlyThatCommandAndStaysReady()
|
||||
{
|
||||
await using PipePair pipePair = await PipePair.CreateAsync();
|
||||
await using WorkerClient client = CreateClient(pipePair, maxMessageBytes: 4096);
|
||||
await CompleteHandshakeAsync(client, pipePair);
|
||||
|
||||
WorkerCommand oversized = new()
|
||||
{
|
||||
Command = new MxCommand
|
||||
{
|
||||
Kind = MxCommandKind.Write,
|
||||
Write = new WriteCommand
|
||||
{
|
||||
ServerHandle = 1,
|
||||
ItemHandle = 2,
|
||||
Value = new MxValue { StringValue = new string('x', 8192) },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
WorkerClientException exception = await Assert.ThrowsAsync<WorkerClientException>(
|
||||
async () => await client.InvokeAsync(oversized, TestTimeout, CancellationToken.None));
|
||||
Assert.Equal(WorkerClientErrorCode.CommandTooLarge, exception.ErrorCode);
|
||||
Assert.Equal(WorkerClientState.Ready, client.State);
|
||||
|
||||
// A subsequent normally-sized command still round-trips: the session was not faulted.
|
||||
Task<WorkerCommandReply> nextInvoke = client.InvokeAsync(
|
||||
CreateCommand(MxCommandKind.Ping),
|
||||
TestTimeout,
|
||||
CancellationToken.None);
|
||||
WorkerEnvelope commandEnvelope = await pipePair.WorkerReader.ReadAsync().AsTask().WaitAsync(TestTimeout);
|
||||
await pipePair.WorkerWriter.WriteAsync(
|
||||
CreateCommandReplyEnvelope(commandEnvelope.CorrelationId, MxCommandKind.Ping));
|
||||
WorkerCommandReply reply = await nextInvoke.WaitAsync(TestTimeout);
|
||||
Assert.Equal(MxCommandKind.Ping, reply.Reply.Kind);
|
||||
Assert.Equal(WorkerClientState.Ready, client.State);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that InvokeAsync ignores late replies and keeps the client ready.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
@@ -564,9 +610,13 @@ public sealed class WorkerClientTests
|
||||
WorkerClientOptions? options = null,
|
||||
GatewayMetrics? metrics = null,
|
||||
WorkerProcessHandle? processHandle = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
TimeProvider? timeProvider = null,
|
||||
int maxMessageBytes = WorkerFrameProtocolOptions.DefaultMaxMessageBytes)
|
||||
{
|
||||
WorkerFrameProtocolOptions frameOptions = new(SessionId);
|
||||
WorkerFrameProtocolOptions frameOptions = new(
|
||||
SessionId,
|
||||
GatewayContractInfo.WorkerProtocolVersion,
|
||||
maxMessageBytes);
|
||||
WorkerClientConnection connection = new(
|
||||
SessionId,
|
||||
Nonce,
|
||||
|
||||
Reference in New Issue
Block a user