Skip to content

Commit cb029de

Browse files
authored
Merge pull request #1046 from onyn/feat/service-config
feat: apply grpc service config from consul (#1045)
2 parents 8239f8a + dec5eff commit cb029de

File tree

5 files changed

+224
-1
lines changed

5 files changed

+224
-1
lines changed

docs/en/client/configuration.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ There are a number of supported schemes, that you can use to determine the targe
6969
- `discovery` (Prio 6): \
7070
(Optional) Uses spring-cloud's `DiscoveryClient` to lookup appropriate targets. The connections will be refreshed
7171
automatically during `HeartbeatEvent`s. Uses the `gRPC_port` metadata to determine the port, otherwise uses the
72-
service port. \
72+
service port. Uses the `gRPC_service_config` metadata to determine [service config](https://grpc.github.io/grpc/core/md_doc_service_config.html). \
7373
Example: `discovery:///service-name`
7474
- `self` (Prio 0): \
7575
The self address or scheme is a keyword that is available, if you also use `grpc-server-spring-boot-starter` and

grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/nameresolver/DiscoveryClientNameResolver.java

+56
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static net.devh.boot.grpc.client.nameresolver.DiscoveryClientResolverFactory.DISCOVERY_INSTANCE_ID_KEY;
2323
import static net.devh.boot.grpc.client.nameresolver.DiscoveryClientResolverFactory.DISCOVERY_SERVICE_NAME_KEY;
2424
import static net.devh.boot.grpc.common.util.GrpcUtils.CLOUD_DISCOVERY_METADATA_PORT;
25+
import static net.devh.boot.grpc.common.util.GrpcUtils.CLOUD_DISCOVERY_METADATA_SERVICE_CONFIG;
2526

2627
import java.net.InetSocketAddress;
2728
import java.util.List;
@@ -35,6 +36,8 @@
3536
import org.springframework.util.CollectionUtils;
3637

3738
import com.google.common.collect.Lists;
39+
import com.google.gson.Gson;
40+
import com.google.gson.JsonSyntaxException;
3841

3942
import io.grpc.Attributes;
4043
import io.grpc.Attributes.Builder;
@@ -58,13 +61,15 @@ public class DiscoveryClientNameResolver extends NameResolver {
5861
@Deprecated
5962
private static final String LEGACY_CLOUD_DISCOVERY_METADATA_PORT = "gRPC.port";
6063
private static final List<ServiceInstance> KEEP_PREVIOUS = null;
64+
private static final Gson GSON = new Gson();
6165

6266
private final String name;
6367
private final DiscoveryClient client;
6468
private final SynchronizationContext syncContext;
6569
private final Consumer<DiscoveryClientNameResolver> shutdownHook;
6670
private final SharedResourceHolder.Resource<Executor> executorResource;
6771
private final boolean usingExecutorResource;
72+
private final ServiceConfigParser serviceConfigParser;
6873

6974
// The field must be accessed from syncContext, although the methods on an Listener2 can be called
7075
// from any thread.
@@ -93,6 +98,7 @@ public DiscoveryClientNameResolver(final String name, final DiscoveryClient clie
9398
this.executor = args.getOffloadExecutor();
9499
this.usingExecutorResource = this.executor == null;
95100
this.executorResource = executorResource;
101+
this.serviceConfigParser = args.getServiceConfigParser();
96102
}
97103

98104
/**
@@ -187,6 +193,55 @@ protected int getGrpcPort(final ServiceInstance instance) {
187193
}
188194
}
189195

196+
/**
197+
* Extracts and parse gRPC service config from the given service instances.
198+
*
199+
* @param instances The list of instances to extract the service config from.
200+
* @return Parsed gRPC service config or null.
201+
*/
202+
private ConfigOrError resolveServiceConfig(List<ServiceInstance> instances) {
203+
final String serviceConfig = getServiceConfig(instances);
204+
if (serviceConfig == null) {
205+
return null;
206+
}
207+
log.debug("Found service config for {}", getName());
208+
if (log.isTraceEnabled()) {
209+
// This is to avoid blowing log into several lines if newlines present in service config string.
210+
final String logStr = serviceConfig.replace("\r", "\\r").replace("\n", "\\n");
211+
log.trace("Service config for {}: {}", getName(), logStr);
212+
}
213+
try {
214+
@SuppressWarnings("unchecked")
215+
Map<String, ?> parsedServiceConfig = GSON.fromJson(serviceConfig, Map.class);
216+
return serviceConfigParser.parseServiceConfig(parsedServiceConfig);
217+
} catch (JsonSyntaxException e) {
218+
return ConfigOrError.fromError(
219+
Status.UNKNOWN
220+
.withDescription("Failed to parse grpc service config")
221+
.withCause(e));
222+
}
223+
}
224+
225+
/**
226+
* Extracts the gRPC service config string from the given service instances.
227+
*
228+
* @param instances The list of instances to extract the service config from.
229+
* @return The gRPC service config or null.
230+
*/
231+
protected String getServiceConfig(final List<ServiceInstance> instances) {
232+
for (final ServiceInstance inst : instances) {
233+
final Map<String, String> metadata = inst.getMetadata();
234+
if (metadata == null || metadata.isEmpty()) {
235+
continue;
236+
}
237+
final String metaValue = metadata.get(CLOUD_DISCOVERY_METADATA_SERVICE_CONFIG);
238+
if (metaValue != null && !metaValue.isEmpty()) {
239+
return metaValue;
240+
}
241+
}
242+
return null;
243+
}
244+
190245
/**
191246
* Gets the attributes from the service instance for later use in a load balancer. Can be overwritten to convert
192247
* custom attributes.
@@ -318,6 +373,7 @@ private List<ServiceInstance> resolveInternal() {
318373
log.debug("Ready to update server list for {}", getName());
319374
this.savedListener.onResult(ResolutionResult.newBuilder()
320375
.setAddresses(toTargets(newInstanceList))
376+
.setServiceConfig(resolveServiceConfig(newInstanceList))
321377
.build());
322378
log.info("Done updating server list for {}", getName());
323379
return newInstanceList;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright (c) 2016-2023 The gRPC-Spring 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 net.devh.boot.grpc.client.nameresolver;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import java.net.InetSocketAddress;
22+
import java.util.List;
23+
import java.util.Map;
24+
25+
import org.junit.jupiter.api.Test;
26+
import org.springframework.cloud.client.DefaultServiceInstance;
27+
import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClient;
28+
import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryProperties;
29+
30+
import io.grpc.NameResolver;
31+
import io.grpc.Status;
32+
import io.grpc.SynchronizationContext;
33+
import io.grpc.internal.AutoConfiguredLoadBalancerFactory;
34+
import io.grpc.internal.GrpcUtil;
35+
import io.grpc.internal.ScParser;
36+
import net.devh.boot.grpc.common.util.GrpcUtils;
37+
38+
/**
39+
* Test for {@link DiscoveryClientNameResolver}.
40+
*/
41+
public class DiscoveryClientNameResolverTest {
42+
43+
private final NameResolver.Args args = NameResolver.Args.newBuilder()
44+
.setDefaultPort(1212)
45+
.setProxyDetector(GrpcUtil.DEFAULT_PROXY_DETECTOR)
46+
.setSynchronizationContext(
47+
new SynchronizationContext((t, e) -> {
48+
throw new AssertionError(e);
49+
}))
50+
.setServiceConfigParser(new ScParser(true, 10, 10, new AutoConfiguredLoadBalancerFactory("pick_first")))
51+
.setOffloadExecutor(Runnable::run)
52+
.build();
53+
54+
@Test
55+
void testValidServiceConfig() {
56+
String validServiceConfig = """
57+
{
58+
"loadBalancingConfig": [
59+
{"round_robin": {}}
60+
],
61+
"methodConfig": [
62+
{
63+
"name": [{}],
64+
"retryPolicy": {
65+
"maxAttempts": 5,
66+
"initialBackoff": "0.05s",
67+
"maxBackoff": "1s",
68+
"backoffMultiplier": 2,
69+
"retryableStatusCodes": [
70+
"UNAVAILABLE",
71+
"ABORTED",
72+
"DATA_LOSS",
73+
"INTERNAL",
74+
"DEADLINE_EXCEEDED"
75+
]
76+
},
77+
"timeout": "5s"
78+
}
79+
]
80+
}
81+
""";
82+
TestableListener listener = resolveServiceAndVerify("test1", validServiceConfig);
83+
NameResolver.ConfigOrError serviceConf = listener.getResult().getServiceConfig();
84+
assertThat(serviceConf).isNotNull();
85+
assertThat(serviceConf.getConfig()).isNotNull();
86+
assertThat(serviceConf.getError()).isNull();
87+
}
88+
89+
@Test
90+
void testBrokenServiceConfig() {
91+
TestableListener listener = resolveServiceAndVerify("test2", "intentionally invalid service config");
92+
NameResolver.ConfigOrError serviceConf = listener.getResult().getServiceConfig();
93+
assertThat(serviceConf).isNotNull();
94+
assertThat(serviceConf.getConfig()).isNull();
95+
assertThat(serviceConf.getError()).extracting(Status::getCode).isEqualTo(Status.Code.UNKNOWN);
96+
}
97+
98+
private TestableListener resolveServiceAndVerify(String serviceName, String serviceConfig) {
99+
SimpleDiscoveryProperties props = new SimpleDiscoveryProperties();
100+
DefaultServiceInstance service = new DefaultServiceInstance(
101+
serviceName + "-1", serviceName, "127.0.0.1", 3322, false);
102+
Map<String, String> meta = service.getMetadata();
103+
meta.put(GrpcUtils.CLOUD_DISCOVERY_METADATA_PORT, "6688");
104+
meta.put(GrpcUtils.CLOUD_DISCOVERY_METADATA_SERVICE_CONFIG, serviceConfig);
105+
props.setInstances(Map.of(serviceName, List.of(service)));
106+
SimpleDiscoveryClient disco = new SimpleDiscoveryClient(props);
107+
DiscoveryClientNameResolver dcnr = new DiscoveryClientNameResolver(serviceName, disco, args, null, null);
108+
109+
TestableListener listener = new TestableListener();
110+
dcnr.start(listener);
111+
112+
assertThat(listener.isErrorWasSet()).isFalse();
113+
assertThat(listener.isResultWasSet()).isTrue();
114+
InetSocketAddress addr = (InetSocketAddress) listener.getResult().getAddresses().get(0).getAddresses().get(0);
115+
assertThat(addr.getPort()).isEqualTo(6688);
116+
assertThat(addr.getHostString()).isEqualTo("127.0.0.1");
117+
return listener;
118+
}
119+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (c) 2016-2023 The gRPC-Spring 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 net.devh.boot.grpc.client.nameresolver;
18+
19+
import io.grpc.NameResolver;
20+
import io.grpc.Status;
21+
import lombok.Getter;
22+
23+
@Getter
24+
public class TestableListener extends NameResolver.Listener2 {
25+
26+
private NameResolver.ResolutionResult result;
27+
private Status error;
28+
private boolean resultWasSet = false;
29+
private boolean errorWasSet = false;
30+
31+
@Override
32+
public void onResult(NameResolver.ResolutionResult resolutionResult) {
33+
this.result = resolutionResult;
34+
resultWasSet = true;
35+
}
36+
37+
@Override
38+
public void onError(Status error) {
39+
this.error = error;
40+
errorWasSet = true;
41+
}
42+
43+
}

grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/util/GrpcUtils.java

+5
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ public final class GrpcUtils {
4040
*/
4141
public static final String CLOUD_DISCOVERY_METADATA_PORT = "gRPC_port";
4242

43+
/**
44+
* The cloud discovery metadata key used to identify service config.
45+
*/
46+
public static final String CLOUD_DISCOVERY_METADATA_SERVICE_CONFIG = "gRPC_service_config";
47+
4348
/**
4449
* The constant for the grpc server port, -1 represents don't start an inter process server.
4550
*/

0 commit comments

Comments
 (0)