Skip to content

Commit 9c7df78

Browse files
committed
feat(xds): Add CachedChannelManager for caching channel instances
1 parent 4bfd5e6 commit 9c7df78

2 files changed

Lines changed: 232 additions & 0 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2026 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.xds.internal.grpcservice;
18+
19+
import static com.google.common.base.Preconditions.checkNotNull;
20+
21+
import com.google.auto.value.AutoValue;
22+
import io.grpc.ManagedChannel;
23+
import io.grpc.xds.internal.grpcservice.GrpcServiceConfig.GoogleGrpcConfig;
24+
import java.util.Optional;
25+
import java.util.function.Function;
26+
import javax.annotation.concurrent.GuardedBy;
27+
28+
/**
29+
* Concrete class managing the lifecycle of a single ManagedChannel for a GrpcServiceConfig.
30+
*/
31+
public class CachedChannelManager {
32+
private final Function<GrpcServiceConfig, ManagedChannel> channelCreator;
33+
private final Object lock = new Object();
34+
35+
@GuardedBy("lock")
36+
private Optional<ChannelHolder> channelHolder = Optional.empty();
37+
38+
/**
39+
* Default constructor for production that creates a channel using the config's target and
40+
* credentials.
41+
*/
42+
public CachedChannelManager() {
43+
this(config -> {
44+
GoogleGrpcConfig googleGrpc = config.googleGrpc();
45+
return io.grpc.Grpc.newChannelBuilder(googleGrpc.target(),
46+
googleGrpc.configuredChannelCredentials().channelCredentials()).build();
47+
});
48+
}
49+
50+
/**
51+
* Constructor for testing to inject a channel creator.
52+
*/
53+
public CachedChannelManager(Function<GrpcServiceConfig, ManagedChannel> channelCreator) {
54+
this.channelCreator = checkNotNull(channelCreator, "channelCreator");
55+
}
56+
57+
/**
58+
* Returns a ManagedChannel for the given configuration. If the target or credentials config
59+
* changes, the old channel is shut down and a new one is created.
60+
*/
61+
public ManagedChannel getChannel(GrpcServiceConfig config) {
62+
GoogleGrpcConfig googleGrpc = config.googleGrpc();
63+
ChannelKey newChannelKey = ChannelKey.of(
64+
googleGrpc.target(),
65+
googleGrpc.configuredChannelCredentials().channelCredsConfig());
66+
67+
synchronized (lock) {
68+
if (channelHolder.isPresent() && channelHolder.get().channelKey().equals(newChannelKey)) {
69+
return channelHolder.get().channel();
70+
}
71+
Optional<ManagedChannel> oldChannel = channelHolder.map(ChannelHolder::channel);
72+
ManagedChannel newChannel = channelCreator.apply(config);
73+
74+
channelHolder = Optional.of(ChannelHolder.create(newChannelKey, newChannel));
75+
oldChannel.ifPresent(ManagedChannel::shutdown);
76+
77+
return newChannel;
78+
}
79+
}
80+
81+
/** Removes underlying resources on shutdown. */
82+
public void close() {
83+
synchronized (lock) {
84+
channelHolder.ifPresent(holder -> holder.channel().shutdown());
85+
}
86+
}
87+
88+
@AutoValue
89+
abstract static class ChannelKey {
90+
static ChannelKey of(String target, ChannelCredsConfig credentialsConfig) {
91+
return new AutoValue_CachedChannelManager_ChannelKey(target, credentialsConfig);
92+
}
93+
94+
abstract String target();
95+
96+
abstract ChannelCredsConfig channelCredsConfig();
97+
}
98+
99+
@AutoValue
100+
abstract static class ChannelHolder {
101+
static ChannelHolder create(ChannelKey channelKey, ManagedChannel channel) {
102+
return new AutoValue_CachedChannelManager_ChannelHolder(channelKey, channel);
103+
}
104+
105+
abstract ChannelKey channelKey();
106+
107+
abstract ManagedChannel channel();
108+
}
109+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright 2026 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.xds.internal.grpcservice;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
import static org.mockito.Mockito.mock;
21+
import static org.mockito.Mockito.verify;
22+
import static org.mockito.Mockito.when;
23+
24+
import com.google.common.collect.ImmutableList;
25+
import io.grpc.ManagedChannel;
26+
import io.grpc.xds.internal.grpcservice.GrpcServiceConfig.GoogleGrpcConfig;
27+
import java.util.function.Function;
28+
import org.junit.Before;
29+
import org.junit.Rule;
30+
import org.junit.Test;
31+
import org.junit.runner.RunWith;
32+
import org.junit.runners.JUnit4;
33+
import org.mockito.Mock;
34+
import org.mockito.junit.MockitoJUnit;
35+
import org.mockito.junit.MockitoRule;
36+
37+
/**
38+
* Unit tests for {@link CachedChannelManager}.
39+
*/
40+
@RunWith(JUnit4.class)
41+
public class CachedChannelManagerTest {
42+
43+
@Rule
44+
public final MockitoRule mocks = MockitoJUnit.rule();
45+
46+
@Mock
47+
private Function<GrpcServiceConfig, ManagedChannel> mockCreator;
48+
49+
@Mock
50+
private ManagedChannel mockChannel1;
51+
52+
@Mock
53+
private ManagedChannel mockChannel2;
54+
55+
private CachedChannelManager manager;
56+
57+
private GrpcServiceConfig config1;
58+
private GrpcServiceConfig config2;
59+
60+
@Before
61+
public void setUp() {
62+
manager = new CachedChannelManager(mockCreator);
63+
64+
config1 = buildConfig("authz.service.com", "creds1");
65+
config2 = buildConfig("authz.service.com", "creds2"); // Different creds instance
66+
}
67+
68+
private GrpcServiceConfig buildConfig(String target, String credsType) {
69+
ChannelCredsConfig credsConfig = mock(ChannelCredsConfig.class);
70+
when(credsConfig.type()).thenReturn(credsType);
71+
72+
ConfiguredChannelCredentials creds = ConfiguredChannelCredentials.create(
73+
mock(io.grpc.ChannelCredentials.class), credsConfig);
74+
75+
GoogleGrpcConfig googleGrpc = GoogleGrpcConfig.builder()
76+
.target(target)
77+
.configuredChannelCredentials(creds)
78+
.build();
79+
80+
return GrpcServiceConfig.newBuilder()
81+
.googleGrpc(googleGrpc)
82+
.initialMetadata(ImmutableList.of())
83+
.build();
84+
}
85+
86+
@Test
87+
public void getChannel_sameConfig_returnsCached() {
88+
when(mockCreator.apply(config1)).thenReturn(mockChannel1);
89+
90+
ManagedChannel channela = manager.getChannel(config1);
91+
ManagedChannel channelb = manager.getChannel(config1);
92+
93+
assertThat(channela).isSameInstanceAs(mockChannel1);
94+
assertThat(channelb).isSameInstanceAs(mockChannel1);
95+
verify(mockCreator, org.mockito.Mockito.times(1)).apply(config1);
96+
}
97+
98+
@Test
99+
public void getChannel_differentConfig_shutsDownOldAndReturnsNew() {
100+
when(mockCreator.apply(config1)).thenReturn(mockChannel1);
101+
when(mockCreator.apply(config2)).thenReturn(mockChannel2);
102+
103+
ManagedChannel channel1 = manager.getChannel(config1);
104+
assertThat(channel1).isSameInstanceAs(mockChannel1);
105+
106+
ManagedChannel channel2 = manager.getChannel(config2);
107+
assertThat(channel2).isSameInstanceAs(mockChannel2);
108+
109+
verify(mockChannel1).shutdown();
110+
verify(mockCreator, org.mockito.Mockito.times(1)).apply(config1);
111+
verify(mockCreator, org.mockito.Mockito.times(1)).apply(config2);
112+
}
113+
114+
@Test
115+
public void close_shutsDownChannel() {
116+
when(mockCreator.apply(config1)).thenReturn(mockChannel1);
117+
118+
manager.getChannel(config1);
119+
manager.close();
120+
121+
verify(mockChannel1).shutdown();
122+
}
123+
}

0 commit comments

Comments
 (0)