Skip to content

Commit 04ee79b

Browse files
committed
feat(xds): Add CachedChannelManager for caching channel instances
1 parent b28bd3c commit 04ee79b

File tree

2 files changed

+251
-0
lines changed

2 files changed

+251
-0
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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.concurrent.atomic.AtomicReference;
25+
import java.util.function.Function;
26+
27+
/**
28+
* Concrete class managing the lifecycle of a single ManagedChannel for a GrpcServiceConfig.
29+
*/
30+
public class CachedChannelManager {
31+
private final Function<GrpcServiceConfig, ManagedChannel> channelCreator;
32+
private final Object lock = new Object();
33+
34+
private final AtomicReference<ChannelHolder> channelHolder = new AtomicReference<>();
35+
36+
/**
37+
* Default constructor for production that creates a channel using the config's target and
38+
* credentials.
39+
*/
40+
public CachedChannelManager() {
41+
this(config -> {
42+
GoogleGrpcConfig googleGrpc = config.googleGrpc();
43+
return io.grpc.Grpc.newChannelBuilder(googleGrpc.target(),
44+
googleGrpc.configuredChannelCredentials().channelCredentials()).build();
45+
});
46+
}
47+
48+
/**
49+
* Constructor for testing to inject a channel creator.
50+
*/
51+
public CachedChannelManager(Function<GrpcServiceConfig, ManagedChannel> channelCreator) {
52+
this.channelCreator = checkNotNull(channelCreator, "channelCreator");
53+
}
54+
55+
/**
56+
* Returns a ManagedChannel for the given configuration. If the target or credentials config
57+
* changes, the old channel is shut down and a new one is created.
58+
*/
59+
public ManagedChannel getChannel(GrpcServiceConfig config) {
60+
GoogleGrpcConfig googleGrpc = config.googleGrpc();
61+
ChannelKey newChannelKey = ChannelKey.of(
62+
googleGrpc.target(),
63+
googleGrpc.configuredChannelCredentials().channelCredsConfig());
64+
65+
// 1. Fast path: Lock-free read
66+
ChannelHolder holder = channelHolder.get();
67+
if (holder != null && holder.channelKey().equals(newChannelKey)) {
68+
return holder.channel();
69+
}
70+
71+
ManagedChannel oldChannel = null;
72+
ManagedChannel newChannel;
73+
74+
// 2. Slow path: Update with locking
75+
synchronized (lock) {
76+
holder = channelHolder.get(); // Double check
77+
if (holder != null && holder.channelKey().equals(newChannelKey)) {
78+
return holder.channel();
79+
}
80+
81+
// 3. Create inside lock to avoid creation storms
82+
newChannel = channelCreator.apply(config);
83+
ChannelHolder newHolder = ChannelHolder.create(newChannelKey, newChannel);
84+
85+
if (holder != null) {
86+
oldChannel = holder.channel();
87+
}
88+
channelHolder.set(newHolder);
89+
}
90+
91+
// 4. Shutdown outside lock
92+
if (oldChannel != null) {
93+
oldChannel.shutdown();
94+
}
95+
96+
return newChannel;
97+
}
98+
99+
/** Removes underlying resources on shutdown. */
100+
public void close() {
101+
ChannelHolder holder = channelHolder.get();
102+
if (holder != null) {
103+
holder.channel().shutdown();
104+
}
105+
}
106+
107+
@AutoValue
108+
abstract static class ChannelKey {
109+
static ChannelKey of(String target, ChannelCredsConfig credentialsConfig) {
110+
return new AutoValue_CachedChannelManager_ChannelKey(target, credentialsConfig);
111+
}
112+
113+
abstract String target();
114+
115+
abstract ChannelCredsConfig channelCredsConfig();
116+
}
117+
118+
@AutoValue
119+
abstract static class ChannelHolder {
120+
static ChannelHolder create(ChannelKey channelKey, ManagedChannel channel) {
121+
return new AutoValue_CachedChannelManager_ChannelHolder(channelKey, channel);
122+
}
123+
124+
abstract ChannelKey channelKey();
125+
126+
abstract ManagedChannel channel();
127+
}
128+
}
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)