fix(GWC-29): drop the wasted request clone on the Invoke hot path

Invoke deep-cloned the whole MxCommandRequest — including its command
payload, potentially a large bulk-write graph — only to overwrite the
cloned command with commandToInvoke and discard it. MapCommand then did
the one clone actually needed. Net cost: a full wasted command deep-clone
per Invoke, worst for exactly the bulk writes that are largest.

Adds a MapCommand(MxCommand) overload (MapCommand reads nothing else off
the request) and has Invoke pass commandToInvoke directly; the request
overload delegates so other callers are untouched.

The remaining clone inside MapCommand stays and is now documented as
required rather than incidental: commandToInvoke may be the gRPC-owned
request.Command, and the caller reads it again after dispatch via
TrackCommandReply, so ownership transfer (à la GWC-07) is not safe here.
That clone is what keeps WorkerClient.CreateCommandEnvelope's no-aliasing
invariant true.

Tests: MxAccessGrpcMapperTests.MapCommandFromCommandClonesPayload
(mutating the input leaves the mapped command untouched; both overloads
produce equal results under a fixed TimeProvider).
This commit is contained in:
Joseph Doherty
2026-08-07 06:15:31 -04:00
parent f27eb28063
commit a044f92c5d
3 changed files with 68 additions and 4 deletions
@@ -116,9 +116,12 @@ public sealed class MxAccessGatewayService(
return bulkConstraintPlan.CreateDeniedReply(request);
}
MxCommandRequest invokeRequest = request.Clone();
invokeRequest.Command = commandToInvoke;
WorkerCommand workerCommand = mapper.MapCommand(invokeRequest);
// Map from the command alone: cloning the whole request only to overwrite its command with
// commandToInvoke deep-cloned the (potentially large) original payload for nothing, since
// MapCommand reads nothing but the command (GWC-29). The one clone that matters still
// happens inside MapCommand, which is what keeps the worker-bound graph unaliased from
// commandToInvoke — the caller still reads it below via TrackCommandReply.
WorkerCommand workerCommand = mapper.MapCommand(commandToInvoke);
WorkerCommandReply workerReply = await sessionManager
.InvokeAsync(request.SessionId, workerCommand, context.CancellationToken)
.ConfigureAwait(false);
@@ -29,9 +29,27 @@ public sealed class MxAccessGrpcMapper
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(request.Command);
return MapCommand(request.Command);
}
/// <summary>
/// Maps a gRPC MX command to a worker command. Callers that already hold the command — including
/// the constraint pipeline, whose rewritten command is not the one on the request — use this
/// overload rather than cloning a whole request to carry a single field (GWC-29); nothing outside
/// the command is read here.
/// </summary>
/// <param name="command">Command payload.</param>
/// <returns>The mapped <see cref="WorkerCommand"/> ready for worker dispatch.</returns>
public WorkerCommand MapCommand(MxCommand command)
{
ArgumentNullException.ThrowIfNull(command);
// The clone is required and must stay: the caller may hand us the gRPC-owned request command,
// and the caller keeps reading it after dispatch (TrackCommandReply). Cloning here is what makes
// WorkerClient.CreateCommandEnvelope's no-aliasing invariant true.
return new WorkerCommand
{
Command = request.Command.Clone(),
Command = command.Clone(),
EnqueueTimestamp = Timestamp.FromDateTimeOffset(_timeProvider.GetUtcNow()),
};
}