From f09b7d9adadd9272d3afe0949c21398f5d3cfd42 Mon Sep 17 00:00:00 2001 From: Konstantin Kalin Date: Wed, 27 May 2026 13:57:00 -0700 Subject: [PATCH 1/3] Reset configSelector on realChannel while entering IDLE state --- core/src/main/java/io/grpc/internal/ManagedChannelImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index e423220e3ad..fa6ef41f334 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -424,6 +424,7 @@ private void enterIdleMode() { // which are bugs. shutdownNameResolverAndLoadBalancer(true); delayedTransport.reprocess(null); + realChannel.updateConfigSelector(INITIAL_PENDING_SELECTOR); channelLogger.log(ChannelLogLevel.INFO, "Entering IDLE state"); channelStateManager.gotoState(IDLE); // If the inUseStateAggregator still considers pending calls to be queued up or the delayed From 3fc92ba2dc4b9cea1372efe999e4b436206a6222 Mon Sep 17 00:00:00 2001 From: Konstantin Kalin Date: Wed, 27 May 2026 14:43:17 -0700 Subject: [PATCH 2/3] Don't reprocess pending calls if configSelector still points to initial. --- core/src/main/java/io/grpc/internal/ManagedChannelImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index fa6ef41f334..b8f57c25950 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -919,7 +919,8 @@ public void run() { void updateConfigSelector(@Nullable InternalConfigSelector config) { InternalConfigSelector prevConfig = configSelector.get(); configSelector.set(config); - if (prevConfig == INITIAL_PENDING_SELECTOR && pendingCalls != null) { + if (prevConfig == INITIAL_PENDING_SELECTOR + && config != INITIAL_PENDING_SELECTOR && pendingCalls != null) { for (RealChannel.PendingCall pendingCall : pendingCalls) { pendingCall.reprocess(); } From bbf11255e3ae01fb5d5327f4c85e8ff8295fa6af Mon Sep 17 00:00:00 2001 From: Konstantin Kalin Date: Fri, 10 Jul 2026 12:36:13 -0700 Subject: [PATCH 3/3] Add unit tests --- .../ManagedChannelImplIdlenessTest.java | 120 +++++++++++++++++- 1 file changed, 115 insertions(+), 5 deletions(-) diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplIdlenessTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplIdlenessTest.java index 97e92be7fdd..0f5c6658e29 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplIdlenessTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplIdlenessTest.java @@ -42,9 +42,11 @@ import io.grpc.ChannelLogger; import io.grpc.ClientCall; import io.grpc.ClientInterceptor; +import io.grpc.ClientStreamTracer; import io.grpc.ConnectivityState; import io.grpc.EquivalentAddressGroup; import io.grpc.IntegerMarshaller; +import io.grpc.InternalConfigSelector; import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.CreateSubchannelArgs; import io.grpc.LoadBalancer.Helper; @@ -61,6 +63,7 @@ import io.grpc.MethodDescriptor; import io.grpc.MethodDescriptor.MethodType; import io.grpc.NameResolver; +import io.grpc.NameResolver.ConfigOrError; import io.grpc.NameResolver.ResolutionResult; import io.grpc.NameResolverProvider; import io.grpc.Status; @@ -79,6 +82,7 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.junit.After; import org.junit.Before; @@ -314,6 +318,66 @@ public void pendingCallExitsIdleAfterEnter() throws Exception { verify(mockNameResolver, times(2)).start(any(NameResolver.Listener2.class)); } + @Test + public void newCallAfterIdleWaitsForFreshConfigSelector() { + AtomicInteger staleSelectCount = new AtomicInteger(); + AtomicInteger freshSelectCount = new AtomicInteger(); + Attributes staleSelectorAttrs = Attributes.newBuilder() + .set(InternalConfigSelector.KEY, countingConfigSelector(staleSelectCount)) + .build(); + Attributes freshSelectorAttrs = Attributes.newBuilder() + .set(InternalConfigSelector.KEY, countingConfigSelector(freshSelectCount)) + .build(); + ArgumentCaptor helperCaptor = ArgumentCaptor.forClass(Helper.class); + + assertEquals(ConnectivityState.IDLE, channel.getState(true)); + deliverResolutionResult(staleSelectorAttrs, 1); + + channel.enterIdle(); + + Metadata headers = new Metadata(); + ClientCall call = channel.newCall(method, CallOptions.DEFAULT); + call.start(mockCallListener, headers); + + assertEquals(0, staleSelectCount.get()); + assertEquals(0, freshSelectCount.get()); + verify(mockLoadBalancerProvider, times(2)).newLoadBalancer(helperCaptor.capture()); + Helper helper = helperCaptor.getAllValues().get(1); + + deliverResolutionResult(freshSelectorAttrs, 2); + + assertEquals(0, staleSelectCount.get()); + verifyPendingCallDrainedToTransport(helper); + assertEquals(1, freshSelectCount.get()); + + channel.enterIdle(); + } + + @Test + public void enterIdleWithInitialPendingCallDoesNotReprocessUntilConfigArrives() { + ArgumentCaptor helperCaptor = ArgumentCaptor.forClass(Helper.class); + Metadata headers = new Metadata(); + + assertEquals(ConnectivityState.IDLE, channel.getState(true)); + ClientCall call = channel.newCall(method, CallOptions.DEFAULT); + call.start(mockCallListener, headers); + + channel.enterIdle(); + + assertFalse(channel.isInPanicMode()); + verify(mockCallListener, never()).onClose(any(Status.class), any(Metadata.class)); + verify(mockLoadBalancerProvider, times(2)).newLoadBalancer(helperCaptor.capture()); + Helper helper = helperCaptor.getAllValues().get(1); + + deliverResolutionResult(Attributes.EMPTY, 2); + + assertFalse(channel.isInPanicMode()); + verify(mockCallListener, never()).onClose(any(Status.class), any(Metadata.class)); + verifyPendingCallDrainedToTransport(helper); + + channel.enterIdle(); + } + @Test public void delayedTransportExitsIdleAfterEnter() throws Exception { // Start a new call that will go to the delayed transport @@ -612,18 +676,64 @@ public void run() { } private void deliverResolutionResult() { - verify(mockNameResolver).start(nameResolverListenerCaptor.capture()); + deliverResolutionResult(Attributes.EMPTY, 1); + } + + private void deliverResolutionResult(Attributes attributes, int nameResolverStartCount) { + verify(mockNameResolver, times(nameResolverStartCount)).start( + nameResolverListenerCaptor.capture()); // Simulate new address resolved to make sure the LoadBalancer is correctly linked to // the NameResolver. - ResolutionResult resolutionResult = + ResolutionResult.Builder resultBuilder = ResolutionResult.newBuilder() .setAddressesOrError(StatusOr.fromValue(servers)) - .setAttributes(Attributes.EMPTY) - .build(); - nameResolverListenerCaptor.getValue().onResult(resolutionResult); + .setAttributes(attributes); + if (attributes.get(InternalConfigSelector.KEY) != null) { + resultBuilder.setServiceConfig( + ConfigOrError.fromConfig(ManagedChannelServiceConfig.empty())); + } + ResolutionResult resolutionResult = resultBuilder.build(); + List listeners = nameResolverListenerCaptor.getAllValues(); + listeners.get(listeners.size() - 1).onResult(resolutionResult); executor.runDueTasks(); } + private void verifyPendingCallDrainedToTransport(Helper helper) { + ClientStream mockStream = mock(ClientStream.class); + Subchannel subchannel = createSubchannelSafely(helper, servers.get(0), Attributes.EMPTY); + requestConnectionSafely(helper, subchannel); + MockClientTransportInfo transportInfo = newTransports.poll(); + when(transportInfo.transport.newStream( + same(method), any(Metadata.class), any(CallOptions.class), + any(ClientStreamTracer[].class))) + .thenReturn(mockStream); + transportInfo.listener.transportReady(); + SubchannelPicker picker = mock(SubchannelPicker.class); + when(picker.pickSubchannel(any(PickSubchannelArgs.class))) + .thenReturn(PickResult.withSubchannel(subchannel)); + + updateBalancingStateSafely(helper, READY, picker); + executor.runDueTasks(); + + verify(transportInfo.transport).newStream( + same(method), any(Metadata.class), any(CallOptions.class), + any(ClientStreamTracer[].class)); + verify(mockStream).start(any(ClientStreamListener.class)); + } + + private static InternalConfigSelector countingConfigSelector( + final AtomicInteger selectCount) { + return new InternalConfigSelector() { + @Override + public Result selectConfig(PickSubchannelArgs args) { + selectCount.incrementAndGet(); + return Result.newBuilder() + .setConfig(ManagedChannelServiceConfig.empty()) + .build(); + } + }; + } + private static void requestConnectionSafely(Helper helper, final Subchannel subchannel) { helper.getSynchronizationContext().execute( new Runnable() {