diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/NamedChannel.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/NamedChannel.java deleted file mode 100644 index 7982f9e..0000000 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/NamedChannel.java +++ /dev/null @@ -1,348 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.grpc.client; - -import java.time.Duration; -import java.util.function.Consumer; - -import org.springframework.util.unit.DataSize; - -import io.grpc.ManagedChannel; - -/** - * Represents the configuration for a {@link ManagedChannel gRPC channel}. - * - * @author Chris Bono - */ -public class NamedChannel { - - /** - * The target address uri to connect to. - */ - private String address = "static://localhost:9090"; - - public String getAddress() { - return this.address; - } - - public void setAddress(final String address) { - this.address = address; - } - - // -------------------------------------------------- - // defaultLoadBalancingPolicy - // -------------------------------------------------- - - /** - * The default load balancing policy the channel should use. - */ - private String defaultLoadBalancingPolicy = "round_robin"; - - public String getDefaultLoadBalancingPolicy() { - return this.defaultLoadBalancingPolicy; - } - - public void setDefaultLoadBalancingPolicy(final String defaultLoadBalancingPolicy) { - this.defaultLoadBalancingPolicy = defaultLoadBalancingPolicy; - } - - // -------------------------------------------------- - - private final Health health = new Health(); - - public Health getHealth() { - return this.health; - } - - /** - * The negotiation type for the channel. - */ - private NegotiationType negotiationType = NegotiationType.PLAINTEXT; - - public NegotiationType getNegotiationType() { - return this.negotiationType; - } - - public void setNegotiationType(NegotiationType negotiationType) { - this.negotiationType = negotiationType; - } - - // -------------------------------------------------- - // KeepAlive - // -------------------------------------------------- - - /** - * Whether keep alive is enabled on the channel. - */ - private boolean enableKeepAlive = false; - - public boolean isEnableKeepAlive() { - return this.enableKeepAlive; - } - - public void setEnableKeepAlive(boolean enableKeepAlive) { - this.enableKeepAlive = enableKeepAlive; - } - - // -------------------------------------------------- - - /** - * The duration without ongoing RPCs before going to idle mode. - */ - private Duration idleTimeout = Duration.ofSeconds(20); - - public Duration getIdleTimeout() { - return this.idleTimeout; - } - - public void setIdleTimeout(Duration idleTimeout) { - this.idleTimeout = idleTimeout; - } - - // -------------------------------------------------- - - /** - * The delay before sending a keepAlive. Note that shorter intervals increase the - * network burden for the server and this value can not be lower than - * 'permitKeepAliveTime' on the server. - */ - private Duration keepAliveTime = Duration.ofMinutes(5); - - public Duration getKeepAliveTime() { - return this.keepAliveTime; - } - - public void setKeepAliveTime(Duration keepAliveTime) { - this.keepAliveTime = keepAliveTime; - } - - // -------------------------------------------------- - - /** - * The default timeout for a keepAlives ping request. - */ - private Duration keepAliveTimeout = Duration.ofSeconds(20); - - public Duration getKeepAliveTimeout() { - return this.keepAliveTimeout; - } - - public void setKeepAliveTimeout(Duration keepAliveTimeout) { - this.keepAliveTimeout = keepAliveTimeout; - } - - // -------------------------------------------------- - - /** - * Whether a keepAlive will be performed when there are no outstanding RPC on a - * connection. - */ - private boolean keepAliveWithoutCalls = false; - - public boolean isKeepAliveWithoutCalls() { - return this.keepAliveWithoutCalls; - } - - public void setKeepAliveWithoutCalls(boolean keepAliveWithoutCalls) { - this.keepAliveWithoutCalls = keepAliveWithoutCalls; - } - - // -------------------------------------------------- - // Message Transfer - // -------------------------------------------------- - - /** - * Maximum message size allowed to be received by the channel (default 4MiB). Set to - * '-1' to use the highest possible limit (not recommended). - */ - private DataSize maxInboundMessageSize = DataSize.ofBytes(4194304); - - /** - * Maximum metadata size allowed to be received by the channel (default 8KiB). Set to - * '-1' to use the highest possible limit (not recommended). - */ - private DataSize maxInboundMetadataSize = DataSize.ofBytes(8192); - - public DataSize getMaxInboundMessageSize() { - return this.maxInboundMessageSize; - } - - public void setMaxInboundMessageSize(final DataSize maxInboundMessageSize) { - this.setMaxInboundSize(maxInboundMessageSize, (s) -> this.maxInboundMessageSize = s, "maxInboundMesssageSize"); - } - - public DataSize getMaxInboundMetadataSize() { - return this.maxInboundMetadataSize; - } - - public void setMaxInboundMetadataSize(DataSize maxInboundMetadataSize) { - this.setMaxInboundSize(maxInboundMetadataSize, (s) -> this.maxInboundMetadataSize = s, - "maxInboundMetadataSize"); - } - - private void setMaxInboundSize(DataSize maxSize, Consumer setter, String propertyName) { - if (maxSize != null && maxSize.toBytes() >= 0) { - setter.accept(maxSize); - } - else if (maxSize != null && maxSize.toBytes() == -1) { - setter.accept(DataSize.ofBytes(Integer.MAX_VALUE)); - } - else { - throw new IllegalArgumentException("Unsupported %s: %s".formatted(propertyName, maxSize)); - } - } - - // -------------------------------------------------- - - /** - * The custom User-Agent for the channel. - */ - private String userAgent = null; - - public String getUserAgent() { - return this.userAgent; - } - - public void setUserAgent(final String userAgent) { - this.userAgent = userAgent; - } - - /** - * Provide a copy of the channel instance. - * @return a copy of the channel instance. - */ - public NamedChannel copy() { - NamedChannel copy = new NamedChannel(); - copy.address = this.address; - copy.defaultLoadBalancingPolicy = this.defaultLoadBalancingPolicy; - copy.negotiationType = this.negotiationType; - copy.enableKeepAlive = this.enableKeepAlive; - copy.idleTimeout = this.idleTimeout; - copy.keepAliveTime = this.keepAliveTime; - copy.keepAliveTimeout = this.keepAliveTimeout; - copy.keepAliveWithoutCalls = this.keepAliveWithoutCalls; - copy.maxInboundMessageSize = this.maxInboundMessageSize; - copy.maxInboundMetadataSize = this.maxInboundMetadataSize; - copy.userAgent = this.userAgent; - copy.health.copyValuesFrom(this.getHealth()); - copy.ssl.copyValuesFrom(this.getSsl()); - return copy; - } - - // -------------------------------------------------- - - /** - * Flag to say that strict SSL checks are not enabled (so the remote certificate could - * be anonymous). - */ - private boolean secure = true; - - public boolean isSecure() { - return this.secure; - } - - public void setSecure(boolean secure) { - this.secure = secure; - } - - // -------------------------------------------------- - - private final Ssl ssl = new Ssl(); - - public Ssl getSsl() { - return this.ssl; - } - - public static class Ssl { - - /** - * Whether to enable SSL support. Enabled automatically if "bundle" is provided - * unless specified otherwise. - */ - private Boolean enabled; - - /** - * SSL bundle name. - */ - private String bundle; - - public boolean isEnabled() { - return (this.enabled != null) ? this.enabled : this.bundle != null; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public String getBundle() { - return this.bundle; - } - - public void setBundle(String bundle) { - this.bundle = bundle; - } - - /** - * Copies the values from another instance. - * @param other instance to copy values from - */ - public void copyValuesFrom(Ssl other) { - this.enabled = other.enabled; - this.bundle = other.bundle; - } - - } - - public static class Health { - - /** - * Whether to enable client-side health check for the channel. - */ - private boolean enabled = false; - - /** - * Name of the service to check health on. - */ - private String serviceName; - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public String getServiceName() { - return this.serviceName; - } - - public void setServiceName(String serviceName) { - this.serviceName = serviceName; - } - - /** - * Copies the values from another instance. - * @param other instance to copy values from - */ - public void copyValuesFrom(Health other) { - this.enabled = other.enabled; - this.serviceName = other.serviceName; - } - - } - -} diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/NamedChannelRegistry.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/NamedChannelRegistry.java deleted file mode 100644 index e2f0236..0000000 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/NamedChannelRegistry.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.grpc.client; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.springframework.context.EnvironmentAware; -import org.springframework.core.env.Environment; -import org.springframework.core.env.StandardEnvironment; - -/** - * Provides access to configured {@link NamedChannel channels}. - * - * @author Chris Bono - */ -public class NamedChannelRegistry implements EnvironmentAware, VirtualTargets { - - private final NamedChannel defaultChannel; - - private final Map channels; - - private Environment environment; - - public NamedChannelRegistry(NamedChannel defaultChannel, Map configuredChannels) { - this.defaultChannel = defaultChannel; - this.channels = new ConcurrentHashMap<>(configuredChannels); - this.environment = new StandardEnvironment(); - } - - @Override - public void setEnvironment(Environment environment) { - this.environment = environment; - } - - /** - * Gets the default channel configuration. - * @return the default channel - */ - public NamedChannel getDefaultChannel() { - return this.defaultChannel; - } - - /** - * Gets the configured channel with the given name. If no channel is configured for - * the specified name then one is created using the default channel as a template. - * @param name the name of the channel - * @return the configured channel if found, or a newly created channel using the - * default channel as a template - */ - public NamedChannel getChannel(String name) { - if ("default".equals(name)) { - return this.defaultChannel; - } - return this.channels.computeIfAbsent(name, authority -> { - NamedChannel channel = this.defaultChannel.copy(); - if (!authority.contains(":/") && !authority.startsWith("unix:")) { - authority = "static://" + authority; - } - channel.setAddress(authority); - return channel; - }); - } - - @Override - public String getTarget(String authority) { - NamedChannel channel = this.getChannel(authority); - String address = channel.getAddress(); - if (address.startsWith("static:") || address.startsWith("tcp:")) { - address = address.substring(address.indexOf(":") + 1).replaceFirst("/*", ""); - } - return this.environment.resolvePlaceholders(address); - } - -} diff --git a/spring-grpc-core/src/test/java/org/springframework/grpc/client/NamedChannelRegistryTests.java b/spring-grpc-core/src/test/java/org/springframework/grpc/client/NamedChannelRegistryTests.java deleted file mode 100644 index cc5fbce..0000000 --- a/spring-grpc-core/src/test/java/org/springframework/grpc/client/NamedChannelRegistryTests.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.grpc.client; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; - -import java.time.Duration; -import java.util.Map; - -import org.assertj.core.api.InstanceOfAssertFactories; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import org.springframework.mock.env.MockEnvironment; -import org.springframework.util.unit.DataSize; - -/** - * Tests for {@link NamedChannelRegistry}. - */ -class NamedChannelRegistryTests { - - @Nested - class GetChannelAPI { - - @Test - void withDefaultNameReturnsDefaultChannel() { - var defaultChannel = new NamedChannel(); - var registry = new NamedChannelRegistry(defaultChannel, Map.of()); - assertThat(registry.getChannel("default")).isSameAs(defaultChannel); - assertThat(registry.getDefaultChannel()).isSameAs(defaultChannel); - assertThat(registry).extracting("channels", InstanceOfAssertFactories.MAP).isEmpty(); - } - - @Test - void withKnownNameReturnsKnownChannel() { - var defaultChannel = new NamedChannel(); - var channel1 = new NamedChannel(); - var registry = new NamedChannelRegistry(defaultChannel, Map.of("c1", channel1)); - assertThat(registry.getChannel("c1")).isSameAs(channel1); - assertThat(registry).extracting("channels", InstanceOfAssertFactories.MAP) - .containsExactly(entry("c1", channel1)); - } - - @Test - void withUnknownNameReturnsNewChannelWithCopiedDefaults() { - var defaultChannel = new NamedChannel(); - defaultChannel.setAddress("static://my-server:9999"); - defaultChannel.setDefaultLoadBalancingPolicy("custom"); - defaultChannel.getHealth().setEnabled(true); - defaultChannel.getHealth().setServiceName("custom-service"); - defaultChannel.setEnableKeepAlive(true); - defaultChannel.setIdleTimeout(Duration.ofMinutes(1)); - defaultChannel.setKeepAliveTime(Duration.ofMinutes(4)); - defaultChannel.setKeepAliveTimeout(Duration.ofMinutes(6)); - defaultChannel.setKeepAliveWithoutCalls(true); - defaultChannel.setMaxInboundMessageSize(DataSize.ofMegabytes(100)); - defaultChannel.setMaxInboundMetadataSize(DataSize.ofMegabytes(200)); - defaultChannel.setUserAgent("me"); - defaultChannel.getSsl().setEnabled(true); - defaultChannel.getSsl().setBundle("custom-bundle"); - var registry = new NamedChannelRegistry(defaultChannel, Map.of()); - var newChannel = registry.getChannel("new-channel"); - assertThat(newChannel).usingRecursiveComparison().ignoringFields("address").isEqualTo(defaultChannel); - assertThat(registry).extracting("channels", InstanceOfAssertFactories.MAP) - .containsExactly(entry("new-channel", newChannel)); - } - - } - - @Nested - class GetTargetAPI { - - @Test - void channelWithStaticAddressReturnsStrippedAddress() { - var defaultChannel = new NamedChannel(); - var channel1 = new NamedChannel(); - channel1.setAddress("static://my-server:8888"); - var registry = new NamedChannelRegistry(defaultChannel, Map.of("c1", channel1)); - assertThat(registry.getTarget("c1")).isEqualTo("my-server:8888"); - assertThat(registry).extracting("channels", InstanceOfAssertFactories.MAP) - .containsExactly(entry("c1", channel1)); - } - - @Test - void channelWithTcpAddressReturnsStrippedAddress() { - var defaultChannel = new NamedChannel(); - var channel1 = new NamedChannel(); - channel1.setAddress("tcp://my-server:8888"); - var registry = new NamedChannelRegistry(defaultChannel, Map.of("c1", channel1)); - assertThat(registry.getTarget("c1")).isEqualTo("my-server:8888"); - assertThat(registry).extracting("channels", InstanceOfAssertFactories.MAP) - .containsExactly(entry("c1", channel1)); - } - - @Test - void channelWithAddressPropertyPlaceholdersPopulatesFromEnvironment() { - var defaultChannel = new NamedChannel(); - var channel1 = new NamedChannel(); - channel1.setAddress("my-server-${channelName}:8888"); - var registry = new NamedChannelRegistry(defaultChannel, Map.of("c1", channel1)); - var env = new MockEnvironment(); - env.setProperty("channelName", "foo"); - registry.setEnvironment(env); - assertThat(registry.getTarget("c1")).isEqualTo("my-server-foo:8888"); - assertThat(registry).extracting("channels", InstanceOfAssertFactories.MAP) - .containsExactly(entry("c1", channel1)); - } - - } - - @Nested - class CopyDefaultsAPI { - - @Test - void copyFromDefaultChannel() { - var registry = new NamedChannelRegistry(new NamedChannel(), Map.of()); - var defaultChannel = registry.getDefaultChannel(); - var newChannel = defaultChannel.copy(); - assertThat(newChannel).usingRecursiveComparison().isEqualTo(defaultChannel); - } - - } - -} diff --git a/spring-grpc-docs/src/main/antora/modules/ROOT/partials/_configprops.adoc b/spring-grpc-docs/src/main/antora/modules/ROOT/partials/_configprops.adoc index 7c4667f..02b709e 100644 --- a/spring-grpc-docs/src/main/antora/modules/ROOT/partials/_configprops.adoc +++ b/spring-grpc-docs/src/main/antora/modules/ROOT/partials/_configprops.adoc @@ -1,7 +1,7 @@ |=== |Name | Default | Description -|spring.grpc.client.channels | | +|spring.grpc.client.channels | | Map of channels configured by name. |spring.grpc.client.default-channel.address | `+++static://localhost:9090+++` | The target address uri to connect to. |spring.grpc.client.default-channel.default-load-balancing-policy | `+++round_robin+++` | The default load balancing policy the channel should use. |spring.grpc.client.default-channel.enable-keep-alive | `+++false+++` | Whether keep alive is enabled on the channel. @@ -46,4 +46,4 @@ |spring.grpc.server.ssl.enabled | | Whether to enable SSL support. Enabled automatically if "bundle" is provided unless specified otherwise. |spring.grpc.server.ssl.secure | `+++true+++` | Flag to indicate that client authentication is secure (i.e. certificates are checked). Do not set this to false in production. -|=== +|=== \ No newline at end of file diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/ClientPropertiesChannelBuilderCustomizer.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/ClientPropertiesChannelBuilderCustomizer.java index f0c9bb6..506bdd4 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/ClientPropertiesChannelBuilderCustomizer.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/ClientPropertiesChannelBuilderCustomizer.java @@ -22,9 +22,8 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.grpc.autoconfigure.client.GrpcClientProperties.ChannelConfig; import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; -import org.springframework.grpc.client.NamedChannel; -import org.springframework.grpc.client.NamedChannelRegistry; import org.springframework.util.unit.DataSize; import io.grpc.ManagedChannelBuilder; @@ -40,18 +39,15 @@ import io.grpc.ManagedChannelBuilder; class ClientPropertiesChannelBuilderCustomizer> implements GrpcChannelBuilderCustomizer { - private final NamedChannelRegistry channelRegistry; + private final GrpcClientProperties properties; - ClientPropertiesChannelBuilderCustomizer(NamedChannelRegistry channelRegistry) { - this.channelRegistry = channelRegistry; + ClientPropertiesChannelBuilderCustomizer(GrpcClientProperties properties) { + this.properties = properties; } @Override public void customize(String authority, T builder) { - NamedChannel channel = this.channelRegistry.getChannel(authority); - if (channel == null) { - return; - } + ChannelConfig channel = this.properties.getChannel(authority); PropertyMapper mapper = PropertyMapper.get().alwaysApplyingWhenNonNull(); mapper.from(channel.getUserAgent()).to(builder::userAgent); if (!authority.startsWith("unix:")) { diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcChannelFactoryConfigurations.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcChannelFactoryConfigurations.java index 1b77ea0..970c3f7 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcChannelFactoryConfigurations.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcChannelFactoryConfigurations.java @@ -27,7 +27,6 @@ import org.springframework.grpc.client.ChannelCredentialsProvider; import org.springframework.grpc.client.ClientInterceptorsConfigurer; import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; import org.springframework.grpc.client.GrpcChannelFactory; -import org.springframework.grpc.client.NamedChannelRegistry; import org.springframework.grpc.client.NettyGrpcChannelFactory; import org.springframework.grpc.client.ShadedNettyGrpcChannelFactory; @@ -47,14 +46,14 @@ class GrpcChannelFactoryConfigurations { static class ShadedNettyChannelFactoryConfiguration { @Bean - ShadedNettyGrpcChannelFactory shadedNettyGrpcChannelFactory(NamedChannelRegistry namedChannelRegistry, + ShadedNettyGrpcChannelFactory shadedNettyGrpcChannelFactory(GrpcClientProperties properties, ChannelBuilderCustomizers channelBuilderCustomizers, ClientInterceptorsConfigurer interceptorsConfigurer, ChannelCredentialsProvider credentials) { List> builderCustomizers = List .of(channelBuilderCustomizers::customize); var factory = new ShadedNettyGrpcChannelFactory(builderCustomizers, interceptorsConfigurer); factory.setCredentialsProvider(credentials); - factory.setVirtualTargets(namedChannelRegistry); + factory.setVirtualTargets(properties); return factory; } @@ -67,14 +66,14 @@ class GrpcChannelFactoryConfigurations { static class NettyChannelFactoryConfiguration { @Bean - NettyGrpcChannelFactory nettyGrpcChannelFactory(NamedChannelRegistry namedChannelRegistry, + NettyGrpcChannelFactory nettyGrpcChannelFactory(GrpcClientProperties properties, ChannelBuilderCustomizers channelBuilderCustomizers, ClientInterceptorsConfigurer interceptorsConfigurer, ChannelCredentialsProvider credentials) { List> builderCustomizers = List .of(channelBuilderCustomizers::customize); var factory = new NettyGrpcChannelFactory(builderCustomizers, interceptorsConfigurer); factory.setCredentialsProvider(credentials); - factory.setVirtualTargets(namedChannelRegistry); + factory.setVirtualTargets(properties); return factory; } diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfiguration.java index a13a3f4..d023142 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfiguration.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfiguration.java @@ -26,7 +26,6 @@ import org.springframework.context.annotation.Import; import org.springframework.grpc.autoconfigure.common.codec.GrpcCodecConfiguration; import org.springframework.grpc.client.ChannelCredentialsProvider; import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; -import org.springframework.grpc.client.NamedChannelRegistry; import io.grpc.CompressorRegistry; import io.grpc.DecompressorRegistry; @@ -39,23 +38,16 @@ import io.grpc.ManagedChannelBuilder; GrpcChannelFactoryConfigurations.NettyChannelFactoryConfiguration.class }) public class GrpcClientAutoConfiguration { - @Bean - @ConditionalOnMissingBean - NamedChannelRegistry namedChannelRegistry(GrpcClientProperties properties) { - return new NamedChannelRegistry(properties.getDefaultChannel(), properties.getChannels()); - } - @Bean @ConditionalOnMissingBean(ChannelCredentialsProvider.class) - NamedChannelCredentialsProvider channelCredentialsProvider(SslBundles bundles, - NamedChannelRegistry channelRegistry) { - return new NamedChannelCredentialsProvider(bundles, channelRegistry); + NamedChannelCredentialsProvider channelCredentialsProvider(SslBundles bundles, GrpcClientProperties properties) { + return new NamedChannelCredentialsProvider(bundles, properties); } @Bean > GrpcChannelBuilderCustomizer clientPropertiesChannelCustomizer( - NamedChannelRegistry channelRegistry) { - return new ClientPropertiesChannelBuilderCustomizer<>(channelRegistry); + GrpcClientProperties properties) { + return new ClientPropertiesChannelBuilderCustomizer<>(properties); } @ConditionalOnBean(CompressorRegistry.class) diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientProperties.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientProperties.java index 1d29803..0a8af57 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientProperties.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientProperties.java @@ -15,35 +15,415 @@ */ package org.springframework.grpc.autoconfigure.client; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.Map; +import java.util.function.Consumer; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; -import org.springframework.grpc.client.NamedChannel; +import org.springframework.boot.convert.DurationUnit; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.grpc.client.NegotiationType; +import org.springframework.grpc.client.VirtualTargets; +import org.springframework.util.unit.DataSize; + +import io.grpc.ManagedChannel; @ConfigurationProperties(prefix = "spring.grpc.client") -public class GrpcClientProperties { +public class GrpcClientProperties implements EnvironmentAware, VirtualTargets { - @NestedConfigurationProperty - private final NamedChannel defaultChannel = new NamedChannel(); + /** + * The default channel configuration to use for new channels. + */ + private final ChannelConfig defaultChannel = new ChannelConfig(); - private final Map channels = new HashMap<>(); + /** + * Map of channels configured by name. + */ + private final Map channels = new HashMap<>(); + + private Environment environment; GrpcClientProperties() { this.defaultChannel.setAddress("static://localhost:9090"); + this.environment = new StandardEnvironment(); } - public Map getChannels() { - return this.channels; - } - - /** - * Gets the default {@link NamedChannel} configured for the GRPC client. - * @return the default {@link NamedChannel} - */ - public NamedChannel getDefaultChannel() { + public ChannelConfig getDefaultChannel() { return this.defaultChannel; } + public Map getChannels() { + return this.channels; + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + /** + * Gets the configured channel with the given name. If no channel is configured for + * the specified name then one is created using the default channel as a template. + * @param name the name of the channel + * @return the configured channel if found, or a newly created channel using the + * default channel as a template + */ + public ChannelConfig getChannel(String name) { + if ("default".equals(name)) { + return this.defaultChannel; + } + ChannelConfig channel = this.channels.get(name); + if (channel != null) { + return channel; + } + channel = this.defaultChannel.copy(); + String address = name; + if (!name.contains(":/") && !name.startsWith("unix:")) { + address = "static://" + name; + } + channel.setAddress(address); + return channel; + } + + @Override + public String getTarget(String authority) { + ChannelConfig channel = this.getChannel(authority); + String address = channel.getAddress(); + if (address.startsWith("static:") || address.startsWith("tcp:")) { + address = address.substring(address.indexOf(":") + 1).replaceFirst("/*", ""); + } + return this.environment.resolvePlaceholders(address); + } + + /** + * Represents the configuration for a {@link ManagedChannel gRPC channel}. + */ + public static class ChannelConfig { + + /** + * The target address uri to connect to. + */ + private String address = "static://localhost:9090"; + + public String getAddress() { + return this.address; + } + + public void setAddress(final String address) { + this.address = address; + } + + // -------------------------------------------------- + // defaultLoadBalancingPolicy + // -------------------------------------------------- + + /** + * The default load balancing policy the channel should use. + */ + private String defaultLoadBalancingPolicy = "round_robin"; + + public String getDefaultLoadBalancingPolicy() { + return this.defaultLoadBalancingPolicy; + } + + public void setDefaultLoadBalancingPolicy(final String defaultLoadBalancingPolicy) { + this.defaultLoadBalancingPolicy = defaultLoadBalancingPolicy; + } + + // -------------------------------------------------- + + private final Health health = new Health(); + + public Health getHealth() { + return this.health; + } + + /** + * The negotiation type for the channel. + */ + private NegotiationType negotiationType = NegotiationType.PLAINTEXT; + + public NegotiationType getNegotiationType() { + return this.negotiationType; + } + + public void setNegotiationType(NegotiationType negotiationType) { + this.negotiationType = negotiationType; + } + + // -------------------------------------------------- + // KeepAlive + // -------------------------------------------------- + + /** + * Whether keep alive is enabled on the channel. + */ + private boolean enableKeepAlive = false; + + public boolean isEnableKeepAlive() { + return this.enableKeepAlive; + } + + public void setEnableKeepAlive(boolean enableKeepAlive) { + this.enableKeepAlive = enableKeepAlive; + } + + // -------------------------------------------------- + + /** + * The duration without ongoing RPCs before going to idle mode. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration idleTimeout = Duration.ofSeconds(20); + + public Duration getIdleTimeout() { + return this.idleTimeout; + } + + public void setIdleTimeout(Duration idleTimeout) { + this.idleTimeout = idleTimeout; + } + + // -------------------------------------------------- + + /** + * The delay before sending a keepAlive. Note that shorter intervals increase the + * network burden for the server and this value can not be lower than + * 'permitKeepAliveTime' on the server. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration keepAliveTime = Duration.ofMinutes(5); + + public Duration getKeepAliveTime() { + return this.keepAliveTime; + } + + public void setKeepAliveTime(Duration keepAliveTime) { + this.keepAliveTime = keepAliveTime; + } + + // -------------------------------------------------- + + /** + * The default timeout for a keepAlives ping request. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration keepAliveTimeout = Duration.ofSeconds(20); + + public Duration getKeepAliveTimeout() { + return this.keepAliveTimeout; + } + + public void setKeepAliveTimeout(Duration keepAliveTimeout) { + this.keepAliveTimeout = keepAliveTimeout; + } + + // -------------------------------------------------- + + /** + * Whether a keepAlive will be performed when there are no outstanding RPC on a + * connection. + */ + private boolean keepAliveWithoutCalls = false; + + public boolean isKeepAliveWithoutCalls() { + return this.keepAliveWithoutCalls; + } + + public void setKeepAliveWithoutCalls(boolean keepAliveWithoutCalls) { + this.keepAliveWithoutCalls = keepAliveWithoutCalls; + } + + // -------------------------------------------------- + // Message Transfer + // -------------------------------------------------- + + /** + * Maximum message size allowed to be received by the channel (default 4MiB). Set + * to '-1' to use the highest possible limit (not recommended). + */ + private DataSize maxInboundMessageSize = DataSize.ofBytes(4194304); + + /** + * Maximum metadata size allowed to be received by the channel (default 8KiB). Set + * to '-1' to use the highest possible limit (not recommended). + */ + private DataSize maxInboundMetadataSize = DataSize.ofBytes(8192); + + public DataSize getMaxInboundMessageSize() { + return this.maxInboundMessageSize; + } + + public void setMaxInboundMessageSize(final DataSize maxInboundMessageSize) { + this.setMaxInboundSize(maxInboundMessageSize, (s) -> this.maxInboundMessageSize = s, + "maxInboundMesssageSize"); + } + + public DataSize getMaxInboundMetadataSize() { + return this.maxInboundMetadataSize; + } + + public void setMaxInboundMetadataSize(DataSize maxInboundMetadataSize) { + this.setMaxInboundSize(maxInboundMetadataSize, (s) -> this.maxInboundMetadataSize = s, + "maxInboundMetadataSize"); + } + + private void setMaxInboundSize(DataSize maxSize, Consumer setter, String propertyName) { + if (maxSize != null && maxSize.toBytes() >= 0) { + setter.accept(maxSize); + } + else if (maxSize != null && maxSize.toBytes() == -1) { + setter.accept(DataSize.ofBytes(Integer.MAX_VALUE)); + } + else { + throw new IllegalArgumentException("Unsupported %s: %s".formatted(propertyName, maxSize)); + } + } + + // -------------------------------------------------- + + /** + * The custom User-Agent for the channel. + */ + private String userAgent = null; + + public String getUserAgent() { + return this.userAgent; + } + + public void setUserAgent(final String userAgent) { + this.userAgent = userAgent; + } + + /** + * Provide a copy of the channel instance. + * @return a copy of the channel instance. + */ + public ChannelConfig copy() { + ChannelConfig copy = new ChannelConfig(); + copy.address = this.address; + copy.defaultLoadBalancingPolicy = this.defaultLoadBalancingPolicy; + copy.negotiationType = this.negotiationType; + copy.enableKeepAlive = this.enableKeepAlive; + copy.idleTimeout = this.idleTimeout; + copy.keepAliveTime = this.keepAliveTime; + copy.keepAliveTimeout = this.keepAliveTimeout; + copy.keepAliveWithoutCalls = this.keepAliveWithoutCalls; + copy.maxInboundMessageSize = this.maxInboundMessageSize; + copy.maxInboundMetadataSize = this.maxInboundMetadataSize; + copy.userAgent = this.userAgent; + copy.health.copyValuesFrom(this.getHealth()); + copy.ssl.copyValuesFrom(this.getSsl()); + return copy; + } + + // -------------------------------------------------- + + /** + * Flag to say that strict SSL checks are not enabled (so the remote certificate + * could be anonymous). + */ + private boolean secure = true; + + public boolean isSecure() { + return this.secure; + } + + public void setSecure(boolean secure) { + this.secure = secure; + } + + // -------------------------------------------------- + + private final Ssl ssl = new Ssl(); + + public Ssl getSsl() { + return this.ssl; + } + + public static class Ssl { + + /** + * Whether to enable SSL support. Enabled automatically if "bundle" is + * provided unless specified otherwise. + */ + private Boolean enabled; + + /** + * SSL bundle name. + */ + private String bundle; + + public boolean isEnabled() { + return (this.enabled != null) ? this.enabled : this.bundle != null; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getBundle() { + return this.bundle; + } + + public void setBundle(String bundle) { + this.bundle = bundle; + } + + /** + * Copies the values from another instance. + * @param other instance to copy values from + */ + public void copyValuesFrom(Ssl other) { + this.enabled = other.enabled; + this.bundle = other.bundle; + } + + } + + public static class Health { + + /** + * Whether to enable client-side health check for the channel. + */ + private boolean enabled = false; + + /** + * Name of the service to check health on. + */ + private String serviceName; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getServiceName() { + return this.serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + /** + * Copies the values from another instance. + * @param other instance to copy values from + */ + public void copyValuesFrom(Health other) { + this.enabled = other.enabled; + this.serviceName = other.serviceName; + } + + } + + } + } diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/NamedChannelCredentialsProvider.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/NamedChannelCredentialsProvider.java index 59436a1..e58426e 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/NamedChannelCredentialsProvider.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/NamedChannelCredentialsProvider.java @@ -19,9 +19,8 @@ import javax.net.ssl.TrustManagerFactory; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; +import org.springframework.grpc.autoconfigure.client.GrpcClientProperties.ChannelConfig; import org.springframework.grpc.client.ChannelCredentialsProvider; -import org.springframework.grpc.client.NamedChannel; -import org.springframework.grpc.client.NamedChannelRegistry; import org.springframework.grpc.client.NegotiationType; import org.springframework.grpc.internal.InsecureTrustManagerFactory; @@ -29,20 +28,25 @@ import io.grpc.ChannelCredentials; import io.grpc.InsecureChannelCredentials; import io.grpc.TlsChannelCredentials; +/** + * Provides channel credentials using channel configuration and {@link SslBundles}. + * + * @author David Syer + */ public class NamedChannelCredentialsProvider implements ChannelCredentialsProvider { private final SslBundles bundles; - private final NamedChannelRegistry channelRegistry; + private final GrpcClientProperties properties; - public NamedChannelCredentialsProvider(SslBundles bundles, NamedChannelRegistry channelRegistry) { + public NamedChannelCredentialsProvider(SslBundles bundles, GrpcClientProperties properties) { this.bundles = bundles; - this.channelRegistry = channelRegistry; + this.properties = properties; } @Override public ChannelCredentials getChannelCredentials(String path) { - NamedChannel channel = this.channelRegistry.getChannel(path); + ChannelConfig channel = this.properties.getChannel(path); if (!channel.getSsl().isEnabled() && channel.getNegotiationType() == NegotiationType.PLAINTEXT) { return InsecureChannelCredentials.create(); } diff --git a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfigurationTests.java b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfigurationTests.java index b70ff32..423d3c8 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfigurationTests.java +++ b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfigurationTests.java @@ -43,8 +43,6 @@ import org.springframework.core.annotation.Order; import org.springframework.grpc.client.ChannelCredentialsProvider; import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; import org.springframework.grpc.client.GrpcChannelFactory; -import org.springframework.grpc.client.NamedChannel; -import org.springframework.grpc.client.NamedChannelRegistry; import org.springframework.grpc.client.NettyGrpcChannelFactory; import org.springframework.grpc.client.ShadedNettyGrpcChannelFactory; @@ -67,26 +65,6 @@ class GrpcClientAutoConfigurationTests { .withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class, SslAutoConfiguration.class)); } - @Test - void whenHasUserDefinedChannelRegistryDoesNotAutoConfigureBean() { - NamedChannelRegistry customChannelRegistry = mock(NamedChannelRegistry.class); - this.contextRunner() - .withBean("customChannelRegistry", NamedChannelRegistry.class, () -> customChannelRegistry) - .run((context) -> assertThat(context).getBean(NamedChannelRegistry.class).isSameAs(customChannelRegistry)); - } - - @Test - void channelRegistryAutoConfiguredAsExpected() { - this.contextRunner() - .run((context) -> assertThat(context).getBean(NamedChannelRegistry.class).satisfies((channelRegistry) -> { - var properties = context.getBean(GrpcClientProperties.class); - assertThat(channelRegistry.getDefaultChannel()).isEqualTo(properties.getDefaultChannel()); - assertThat(channelRegistry) - .extracting("channels", InstanceOfAssertFactories.map(String.class, NamedChannel.class)) - .containsExactlyInAnyOrderEntriesOf(properties.getChannels()); - })); - } - @Test void whenHasUserDefinedCredentialsProviderDoesNotAutoConfigureBean() { ChannelCredentialsProvider customCredentialsProvider = mock(ChannelCredentialsProvider.class); @@ -100,11 +78,20 @@ class GrpcClientAutoConfigurationTests { void credentialsProviderAutoConfiguredAsExpected() { this.contextRunner() .run((context) -> assertThat(context).getBean(NamedChannelCredentialsProvider.class) - .hasFieldOrPropertyWithValue("channelRegistry", context.getBean(NamedChannelRegistry.class)) + .hasFieldOrPropertyWithValue("properties", context.getBean(GrpcClientProperties.class)) .extracting("bundles") .isInstanceOf(SslBundles.class)); } + @Test + void clientPropertiesAutoConfiguredResolvesPlaceholders() { + this.contextRunner() + .withPropertyValues("spring.grpc.client.channels.c1.address=my-server-${channelName}:8888", + "channelName=foo") + .run((context) -> assertThat(context).getBean(GrpcClientProperties.class) + .satisfies((properties) -> assertThat(properties.getTarget("c1")).isEqualTo("my-server-foo:8888"))); + } + @Test void clientPropertiesChannelCustomizerAutoConfiguredWithHealthAsExpected() { this.contextRunner() @@ -249,7 +236,7 @@ class GrpcClientAutoConfigurationTests { .isInstanceOf(expectedChannelFactoryType) .hasFieldOrPropertyWithValue("credentials", context.getBean(NamedChannelCredentialsProvider.class)) .extracting("targets") - .isInstanceOf(NamedChannelRegistry.class)); + .isInstanceOf(GrpcClientProperties.class)); } @Test diff --git a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientPropertiesTests.java b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientPropertiesTests.java index 8f5f20d..0a47b34 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientPropertiesTests.java +++ b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientPropertiesTests.java @@ -17,19 +17,23 @@ package org.springframework.grpc.autoconfigure.client; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.function.Function; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; -import org.springframework.grpc.client.NamedChannel; +import org.springframework.grpc.autoconfigure.client.GrpcClientProperties.ChannelConfig; import org.springframework.grpc.client.NegotiationType; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.unit.DataSize; /** @@ -45,6 +49,13 @@ class GrpcClientPropertiesTests { .get(); } + private GrpcClientProperties newProperties(ChannelConfig defaultChannel, Map channels) { + var properties = new GrpcClientProperties(); + ReflectionTestUtils.setField(properties, "defaultChannel", defaultChannel); + ReflectionTestUtils.setField(properties, "channels", channels); + return properties; + } + @Nested class BindPropertiesAPI { @@ -55,11 +66,11 @@ class GrpcClientPropertiesTests { @Test void specificChannelWithDefaultValues() { - this.withDefaultValues("channels.c1", (p) -> p.getChannels().get("c1")); + this.withDefaultValues("channels.c1", (p) -> p.getChannel("c1")); } private void withDefaultValues(String channelName, - Function channelFromProperties) { + Function channelFromProperties) { Map map = new HashMap<>(); // we have to at least bind one property or bind() fails map.put("spring.grpc.client.%s.enable-keep-alive".formatted(channelName), "false"); @@ -91,11 +102,11 @@ class GrpcClientPropertiesTests { @Test void specificChannelWithSpecifiedValues() { - this.withSpecifiedValues("channels.c1", (p) -> p.getChannels().get("c1")); + this.withSpecifiedValues("channels.c1", (p) -> p.getChannel("c1")); } private void withSpecifiedValues(String channelName, - Function channelFromProperties) { + Function channelFromProperties) { Map map = new HashMap<>(); var propPrefix = "spring.grpc.client.%s.".formatted(channelName); map.put("%s.address".formatted(propPrefix), "static://my-server:8888"); @@ -138,9 +149,9 @@ class GrpcClientPropertiesTests { @Test void withoutKeepAliveUnitsSpecified() { Map map = new HashMap<>(); - map.put("spring.grpc.client.default-channel.idle-timeout", "1000"); - map.put("spring.grpc.client.default-channel.keep-alive-time", "60000"); - map.put("spring.grpc.client.default-channel.keep-alive-timeout", "5000"); + map.put("spring.grpc.client.default-channel.idle-timeout", "1"); + map.put("spring.grpc.client.default-channel.keep-alive-time", "60"); + map.put("spring.grpc.client.default-channel.keep-alive-timeout", "5"); GrpcClientProperties properties = bindProperties(map); var defaultChannel = properties.getDefaultChannel(); assertThat(defaultChannel.getIdleTimeout()).isEqualTo(Duration.ofSeconds(1)); @@ -161,4 +172,104 @@ class GrpcClientPropertiesTests { } + @Nested + class GetChannelAPI { + + @Test + void withDefaultNameReturnsDefaultChannel() { + var properties = new GrpcClientProperties(); + var defaultChannel = properties.getChannel("default"); + assertThat(properties).extracting("defaultChannel").isSameAs(defaultChannel); + assertThat(properties).extracting("channels", InstanceOfAssertFactories.MAP).isEmpty(); + } + + @Test + void withKnownNameReturnsKnownChannel() { + Map map = new HashMap<>(); + // we have to at least bind one property or bind() fails + map.put("spring.grpc.client.channels.c1.enable-keep-alive", "false"); + GrpcClientProperties properties = bindProperties(map); + var channel = properties.getChannel("c1"); + assertThat(properties).extracting("channels", InstanceOfAssertFactories.MAP) + .containsExactly(entry("c1", channel)); + } + + @Test + void withUnknownNameReturnsNewChannelWithCopiedDefaults() { + var defaultChannel = new ChannelConfig(); + defaultChannel.setAddress("static://my-server:9999"); + defaultChannel.setDefaultLoadBalancingPolicy("custom"); + defaultChannel.getHealth().setEnabled(true); + defaultChannel.getHealth().setServiceName("custom-service"); + defaultChannel.setEnableKeepAlive(true); + defaultChannel.setIdleTimeout(Duration.ofMinutes(1)); + defaultChannel.setKeepAliveTime(Duration.ofMinutes(4)); + defaultChannel.setKeepAliveTimeout(Duration.ofMinutes(6)); + defaultChannel.setKeepAliveWithoutCalls(true); + defaultChannel.setMaxInboundMessageSize(DataSize.ofMegabytes(100)); + defaultChannel.setMaxInboundMetadataSize(DataSize.ofMegabytes(200)); + defaultChannel.setUserAgent("me"); + defaultChannel.getSsl().setEnabled(true); + defaultChannel.getSsl().setBundle("custom-bundle"); + var properties = newProperties(defaultChannel, Map.of()); + var newChannel = properties.getChannel("new-channel"); + assertThat(newChannel).usingRecursiveComparison().ignoringFields("address").isEqualTo(defaultChannel); + assertThat(newChannel).hasFieldOrPropertyWithValue("address", "static://new-channel"); + assertThat(properties).extracting("channels", InstanceOfAssertFactories.MAP).isEmpty(); + } + + } + + @Nested + class GetTargetAPI { + + @Test + void channelWithStaticAddressReturnsStrippedAddress() { + var defaultChannel = new ChannelConfig(); + var channel1 = new ChannelConfig(); + channel1.setAddress("static://my-server:8888"); + var properties = newProperties(defaultChannel, Map.of("c1", channel1)); + assertThat(properties.getTarget("c1")).isEqualTo("my-server:8888"); + assertThat(properties).extracting("channels", InstanceOfAssertFactories.MAP) + .containsExactly(entry("c1", channel1)); + } + + @Test + void channelWithTcpAddressReturnsStrippedAddress() { + var defaultChannel = new ChannelConfig(); + var channel1 = new ChannelConfig(); + channel1.setAddress("tcp://my-server:8888"); + var properties = newProperties(defaultChannel, Map.of("c1", channel1)); + assertThat(properties.getTarget("c1")).isEqualTo("my-server:8888"); + assertThat(properties).extracting("channels", InstanceOfAssertFactories.MAP) + .containsExactly(entry("c1", channel1)); + } + + @Test + void channelWithAddressPropertyPlaceholdersPopulatesFromEnvironment() { + var defaultChannel = new ChannelConfig(); + var channel1 = new ChannelConfig(); + channel1.setAddress("my-server-${channelName}:8888"); + var properties = newProperties(defaultChannel, Map.of("c1", channel1)); + var env = new MockEnvironment(); + env.setProperty("channelName", "foo"); + properties.setEnvironment(env); + assertThat(properties.getTarget("c1")).isEqualTo("my-server-foo:8888"); + } + + } + + @Nested + class CopyDefaultsAPI { + + @Test + void copyFromDefaultChannel() { + var properties = new GrpcClientProperties(); + var defaultChannel = properties.getDefaultChannel(); + var newChannel = defaultChannel.copy(); + assertThat(newChannel).usingRecursiveComparison().isEqualTo(defaultChannel); + } + + } + }