core: Propagate authority override from LB exactly once

Setting the authority is only useful when creating a real stream, as
there will be a following pick otherwise. In addition, DelayedStream
will buffer each call to setAuthority() in a list and we don't want that
memory usage. Note that no LBs are using this feature yet, so users
would not have been exposed to the memory use.

We also needed to setAuthority() when the LB selected a subchannel on
the first pick attempt.
This commit is contained in:
Eric Anderson 2025-01-29 14:26:35 -08:00
parent 7153ff8522
commit 90aefb26e7
2 changed files with 36 additions and 41 deletions

View File

@ -140,9 +140,15 @@ final class DelayedClientTransport implements ManagedClientTransport {
ClientTransport transport = GrpcUtil.getTransportFromPickResult(pickResult, ClientTransport transport = GrpcUtil.getTransportFromPickResult(pickResult,
callOptions.isWaitForReady()); callOptions.isWaitForReady());
if (transport != null) { if (transport != null) {
return transport.newStream( ClientStream stream = transport.newStream(
args.getMethodDescriptor(), args.getHeaders(), callOptions, args.getMethodDescriptor(), args.getHeaders(), callOptions,
tracers); tracers);
// User code provided authority takes precedence over the LB provided one; this will be
// overwritten by ClientCallImpl if the application sets an authority override
if (pickResult.getAuthorityOverride() != null) {
stream.setAuthority(pickResult.getAuthorityOverride());
}
return stream;
} }
} }
// This picker's conclusion is "buffer". If there hasn't been a newer picker set (possible // This picker's conclusion is "buffer". If there hasn't been a newer picker set (possible
@ -287,10 +293,6 @@ final class DelayedClientTransport implements ManagedClientTransport {
for (final PendingStream stream : toProcess) { for (final PendingStream stream : toProcess) {
PickResult pickResult = picker.pickSubchannel(stream.args); PickResult pickResult = picker.pickSubchannel(stream.args);
CallOptions callOptions = stream.args.getCallOptions(); CallOptions callOptions = stream.args.getCallOptions();
// User code provided authority takes precedence over the LB provided one.
if (callOptions.getAuthority() == null && pickResult.getAuthorityOverride() != null) {
stream.setAuthority(pickResult.getAuthorityOverride());
}
final ClientTransport transport = GrpcUtil.getTransportFromPickResult(pickResult, final ClientTransport transport = GrpcUtil.getTransportFromPickResult(pickResult,
callOptions.isWaitForReady()); callOptions.isWaitForReady());
if (transport != null) { if (transport != null) {
@ -301,7 +303,7 @@ final class DelayedClientTransport implements ManagedClientTransport {
if (callOptions.getExecutor() != null) { if (callOptions.getExecutor() != null) {
executor = callOptions.getExecutor(); executor = callOptions.getExecutor();
} }
Runnable runnable = stream.createRealStream(transport); Runnable runnable = stream.createRealStream(transport, pickResult.getAuthorityOverride());
if (runnable != null) { if (runnable != null) {
executor.execute(runnable); executor.execute(runnable);
} }
@ -354,7 +356,7 @@ final class DelayedClientTransport implements ManagedClientTransport {
} }
/** Runnable may be null. */ /** Runnable may be null. */
private Runnable createRealStream(ClientTransport transport) { private Runnable createRealStream(ClientTransport transport, String authorityOverride) {
ClientStream realStream; ClientStream realStream;
Context origContext = context.attach(); Context origContext = context.attach();
try { try {
@ -364,6 +366,13 @@ final class DelayedClientTransport implements ManagedClientTransport {
} finally { } finally {
context.detach(origContext); context.detach(origContext);
} }
if (authorityOverride != null) {
// User code provided authority takes precedence over the LB provided one; this will be
// overwritten by an enqueud call from ClientCallImpl if the application sets an authority
// override. We must call the real stream directly because stream.start() has likely already
// been called on the delayed stream.
realStream.setAuthority(authorityOverride);
}
return setStream(realStream); return setStream(realStream);
} }

View File

@ -503,26 +503,11 @@ public class DelayedClientTransportTest {
} }
@Test @Test
public void reprocess_authorityOverridePresentInCallOptions_authorityOverrideFromLbIsIgnored() { public void reprocess_authorityOverrideFromLb() {
DelayedStream delayedStream = (DelayedStream) delayedTransport.newStream( InOrder inOrder = inOrder(mockRealStream);
method, headers, callOptions, tracers);
delayedStream.start(mock(ClientStreamListener.class));
SubchannelPicker picker = mock(SubchannelPicker.class);
PickResult pickResult = PickResult.withSubchannel(
mockSubchannel, null, "authority-override-hostname-from-lb");
when(picker.pickSubchannel(any(PickSubchannelArgs.class))).thenReturn(pickResult);
delayedTransport.reprocess(picker);
fakeExecutor.runDueTasks();
verify(mockRealStream, never()).setAuthority("authority-override-hostname-from-lb");
}
@Test
public void
reprocess_authorityOverrideNotInCallOptions_authorityOverrideFromLbIsSetIntoStream() {
DelayedStream delayedStream = (DelayedStream) delayedTransport.newStream( DelayedStream delayedStream = (DelayedStream) delayedTransport.newStream(
method, headers, callOptions.withAuthority(null), tracers); method, headers, callOptions.withAuthority(null), tracers);
delayedStream.setAuthority("authority-override-from-calloptions");
delayedStream.start(mock(ClientStreamListener.class)); delayedStream.start(mock(ClientStreamListener.class));
SubchannelPicker picker = mock(SubchannelPicker.class); SubchannelPicker picker = mock(SubchannelPicker.class);
PickResult pickResult = PickResult.withSubchannel( PickResult pickResult = PickResult.withSubchannel(
@ -536,7 +521,10 @@ public class DelayedClientTransportTest {
delayedTransport.reprocess(picker); delayedTransport.reprocess(picker);
fakeExecutor.runDueTasks(); fakeExecutor.runDueTasks();
verify(mockRealStream).setAuthority("authority-override-hostname-from-lb"); // Must be set before start(), and may be overwritten
inOrder.verify(mockRealStream).setAuthority("authority-override-hostname-from-lb");
inOrder.verify(mockRealStream).setAuthority("authority-override-from-calloptions");
inOrder.verify(mockRealStream).start(any(ClientStreamListener.class));
} }
@Test @Test
@ -563,28 +551,26 @@ public class DelayedClientTransportTest {
} }
@Test @Test
public void newStream_assignsTransport_authorityFromCallOptionsSupersedesAuthorityFromLB() { public void newStream_authorityOverrideFromLb() {
InOrder inOrder = inOrder(mockRealStream);
SubchannelPicker picker = mock(SubchannelPicker.class); SubchannelPicker picker = mock(SubchannelPicker.class);
AbstractSubchannel subchannel = mock(AbstractSubchannel.class);
when(subchannel.getInternalSubchannel()).thenReturn(mockInternalSubchannel);
PickResult pickResult = PickResult.withSubchannel( PickResult pickResult = PickResult.withSubchannel(
subchannel, null, "authority-override-hostname-from-lb"); mockSubchannel, null, "authority-override-hostname-from-lb");
when(picker.pickSubchannel(any(PickSubchannelArgs.class))).thenReturn(pickResult); when(picker.pickSubchannel(any(PickSubchannelArgs.class))).thenReturn(pickResult);
ArgumentCaptor<CallOptions> callOptionsArgumentCaptor =
ArgumentCaptor.forClass(CallOptions.class);
when(mockRealTransport.newStream( when(mockRealTransport.newStream(
any(MethodDescriptor.class), any(Metadata.class), callOptionsArgumentCaptor.capture(), any(MethodDescriptor.class), any(Metadata.class), any(CallOptions.class), any()))
ArgumentMatchers.<ClientStreamTracer[]>any()))
.thenReturn(mockRealStream); .thenReturn(mockRealStream);
delayedTransport.reprocess(picker); delayedTransport.reprocess(picker);
verifyNoMoreInteractions(picker);
verifyNoMoreInteractions(transportListener);
CallOptions callOptions = ClientStream stream = delayedTransport.newStream(method, headers, callOptions, tracers);
CallOptions.DEFAULT.withAuthority("authority-override-hosstname-from-calloptions"); assertThat(stream).isSameInstanceAs(mockRealStream);
delayedTransport.newStream(method, headers, callOptions, tracers); stream.setAuthority("authority-override-from-calloptions");
assertThat(callOptionsArgumentCaptor.getValue().getAuthority()).isEqualTo( stream.start(mock(ClientStreamListener.class));
"authority-override-hosstname-from-calloptions");
// Must be set before start(), and may be overwritten
inOrder.verify(mockRealStream).setAuthority("authority-override-hostname-from-lb");
inOrder.verify(mockRealStream).setAuthority("authority-override-from-calloptions");
inOrder.verify(mockRealStream).start(any(ClientStreamListener.class));
} }
@Test @Test