From 89e9a3400d6db4bc39e6eaf2e6bbc5a4724a760c Mon Sep 17 00:00:00 2001 From: wushengju <1205902694@qq.com> Date: Wed, 11 Aug 2021 09:40:47 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AF=B9ServiceConfig?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AbstractChannelFactory.java | 57 +++++++- .../client/config/GrpcChannelProperties.java | 55 ++++++++ .../boot/grpc/client/config/MethodConfig.java | 64 +++++++++ .../boot/grpc/client/config/NameConfig.java | 63 +++++++++ .../grpc/client/config/RetryPolicyConfig.java | 128 ++++++++++++++++++ 5 files changed, 360 insertions(+), 7 deletions(-) create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/MethodConfig.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/NameConfig.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/RetryPolicyConfig.java diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java index ad6c3d04b..c92d7936c 100644 --- a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java @@ -22,10 +22,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; @@ -34,6 +31,7 @@ import javax.annotation.PreDestroy; import javax.annotation.concurrent.GuardedBy; +import net.devh.boot.grpc.client.config.*; import org.springframework.util.unit.DataSize; import com.google.common.collect.Lists; @@ -45,10 +43,7 @@ import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import lombok.extern.slf4j.Slf4j; -import net.devh.boot.grpc.client.config.GrpcChannelProperties; import net.devh.boot.grpc.client.config.GrpcChannelProperties.Security; -import net.devh.boot.grpc.client.config.GrpcChannelsProperties; -import net.devh.boot.grpc.client.config.NegotiationType; import net.devh.boot.grpc.client.interceptor.GlobalClientInterceptorRegistry; /** @@ -168,11 +163,59 @@ protected void configure(final T builder, final String name) { configureSecurity(builder, name); configureLimits(builder, name); configureCompression(builder, name); + configureRetryEnabled(builder, name); for (final GrpcChannelConfigurer channelConfigurer : this.channelConfigurers) { channelConfigurer.accept(builder, name); } } + /** + * Configures the retry options that should be used by the channel. + * + * @param builder The channel builder to configure. + * @param name The name of the client to configure. + */ + protected void configureRetryEnabled(final T builder, final String name) { + final GrpcChannelProperties properties = getPropertiesFor(name); + if (properties.isRetryEnabled()) { + builder.enableRetry(); + //build retry policy by default service config + builder.defaultServiceConfig(buildDefaultServiceConfig(properties)); + } + } + + /** + * build service config object + * + * @param properties The properties of + */ + protected Map buildDefaultServiceConfig(final GrpcChannelProperties properties) { + Map serviceConfig = new HashMap<>(); + List methodConfigList = properties.getMethodConfig(); + if (null == methodConfigList || methodConfigList.isEmpty()) { + return serviceConfig; + } + List> methodConfigJsonList = new ArrayList<>(); + methodConfigList.forEach(methodConfig -> { + Map methodConfigMap = new HashMap<>(); + //set method config + List nameConfigList = methodConfig.getName(); + if (null != nameConfigList && !nameConfigList.isEmpty()) { + List> list = new ArrayList<>(); + nameConfigList.forEach(nameConfig -> list.add(nameConfig.buildMap())); + methodConfigMap.put("name", list); + } + //set retry policy + RetryPolicyConfig retryConfig = methodConfig.getRetryPolicy(); + if (retryConfig != null) { + methodConfigMap.put("retryPolicy", retryConfig.buildMap()); + } + methodConfigJsonList.add(methodConfigMap); + }); + serviceConfig.put("methodConfig", methodConfigJsonList); + return serviceConfig; + } + /** * Configures the keep alive options that should be used by the channel. * diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java index 42cdb6ecb..ea938f9d8 100644 --- a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java @@ -22,6 +22,7 @@ import java.net.URI; import java.time.Duration; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; @@ -421,6 +422,47 @@ public void setImmediateConnectTimeout(final Duration immediateConnectTimeout) { // -------------------------------------------------- + private Boolean retryEnabled; + private static final boolean DEFAULT_RETRY_ENABLED = false; + + /** + * Gets whether retry should be enabled. + * + * @return True, if retry should be enabled. False otherwise. + * @see #setRetryEnabled(Boolean) + */ + public boolean isRetryEnabled() { + return this.retryEnabled == null ? DEFAULT_RETRY_ENABLED : this.retryEnabled; + } + + /** + * Set Retry enable + * + * @param retryEnabled Whether retry enabled or null to use the fallback. + * @see ManagedChannelBuilder#enableRetry() + */ + public void setRetryEnabled(final Boolean retryEnabled) { + this.retryEnabled = retryEnabled; + } + + public Boolean getRetryEnabled() { + return retryEnabled; + } + + // -------------------------------------------------- + + private List methodConfig; + + public List getMethodConfig() { + return methodConfig; + } + + public void setMethodConfig(List methodConfig) { + this.methodConfig = methodConfig; + } + + // -------------------------------------------------- + private final Security security = new Security(); /** @@ -475,6 +517,19 @@ public void copyDefaultsFrom(final GrpcChannelProperties config) { if (this.immediateConnectTimeout == null) { this.immediateConnectTimeout = config.immediateConnectTimeout; } + if (this.retryEnabled == null) { + this.retryEnabled = config.retryEnabled; + } + if (this.methodConfig == null || this.methodConfig.isEmpty()) { + this.methodConfig = new ArrayList<>(); + if (config.getMethodConfig() != null && !config.getMethodConfig().isEmpty()) { + config.getMethodConfig().forEach(cf -> { + MethodConfig newMethodConfig = new MethodConfig(); + newMethodConfig.copyDefaultsFrom(cf); + this.methodConfig.add(newMethodConfig); + }); + } + } this.security.copyDefaultsFrom(config.security); } diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/MethodConfig.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/MethodConfig.java new file mode 100644 index 000000000..d912512b3 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/MethodConfig.java @@ -0,0 +1,64 @@ +package net.devh.boot.grpc.client.config; + +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.ArrayList; +import java.util.List; + +/** + * @Title 方法配置信息实体, 请参考如下链接: + * https://github.com/grpc/proposal/blob/37e658b12f1684f29b3acca04f0167b84d502876/A6-client-retries.md#grpc-retry-design + * https://github.com/grpc/grpc/blob/master/doc/service_config.md + * @Description MethodConfig + * @Program spring-cloud-tcl-starter + * @Author wushengju + * @Version 1.0 + * @Date 2021-08-10 13:38 + * @Copyright Copyright (c) 2021 TCL Inc. All rights reserved + **/ +@ToString +@EqualsAndHashCode +public class MethodConfig { + /** + * retry policy config + */ + private RetryPolicyConfig retryPolicy; + /** + * name for list + */ + private List name; + + public RetryPolicyConfig getRetryPolicy() { + return retryPolicy; + } + + public void setRetryPolicy(RetryPolicyConfig retryPolicy) { + this.retryPolicy = retryPolicy; + } + + public List getName() { + return name; + } + + public void setName(List name) { + this.name = name; + } + + public void copyDefaultsFrom(final MethodConfig config) { + if (this == config) { + return; + } + this.retryPolicy.copyDefaultsFrom(config.retryPolicy); + if (this.name == null || this.name.isEmpty()) { + if (config.getName() != null && !config.getName().isEmpty()) { + this.name = new ArrayList<>(); + config.getName().forEach(nameConfig -> { + NameConfig newConfig = new NameConfig(); + newConfig.copyDefaultsFrom(nameConfig); + this.name.add(newConfig); + }); + } + } + } +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/NameConfig.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/NameConfig.java new file mode 100644 index 000000000..2ee97f2ad --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/NameConfig.java @@ -0,0 +1,63 @@ +package net.devh.boot.grpc.client.config; + +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Title method config + * @Description MethodConfig + * @Program spring-cloud-tcl-starter + * @Author wushengju + * @Version 1.0 + * @Date 2021-08-10 10:55 + * @Copyright Copyright (c) 2021 TCL Inc. All rights reserved + **/ +@ToString +@EqualsAndHashCode +public class NameConfig { + /** + * the service name which defined in xx.proto + */ + private String service; + /** + * the method name which you will call + */ + private String method; + + public Map buildMap() { + Map map = new HashMap<>(); + map.put("service", this.getService()); + map.put("method", this.getMethod()); + return map; + } + + public void copyDefaultsFrom(final NameConfig config) { + if (this == config) { + return; + } + if (this.service == null) { + this.service = config.service; + } + if (this.method == null) { + this.method = config.method; + } + } + public String getService() { + return service; + } + + public void setService(String service) { + this.service = service; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/RetryPolicyConfig.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/RetryPolicyConfig.java new file mode 100644 index 000000000..bdca5eff1 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/RetryPolicyConfig.java @@ -0,0 +1,128 @@ +package net.devh.boot.grpc.client.config; + +import io.grpc.Status; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.util.CollectionUtils; + +import java.util.*; + +/** + * @Title 重试配置信息 + * @Description RetryPolicyProperties + * @Program spring-cloud-tcl-starter + * @Author wushengju + * @Version 1.0 + * @Date 2021-08-09 15:25 + * @Copyright Copyright (c) 2021 TCL Inc. All rights reserved + **/ +@ToString +@EqualsAndHashCode +public class RetryPolicyConfig { + /** + * The maximum number of RPC attempts, including the original RPC. + * This field is required and must be two or greater. + */ + String maxAttempts; + /** + * Exponential backoff parameters. The initial retry attempt will occur at + * random(0, initialBackoff). In general, the nth attempt since the last + * server pushback response (if any), will occur at random(0, min(initialBackoff*backoffMultiplier**(n-1), maxBackoff)). + * Exponential backoff parameters,Duration is seconds + * Required. Must be greater than zero + */ + Double initialBackoff; + /** + * Required. Must be greater than zero,Duration is seconds + */ + Double maxBackoff; + /** + * Required. Must be greater than zero. + */ + Double backoffMultiplier; + /** + * The set of status codes which may be retried. + * Status codes are specified in the integer form or the case-insensitive string form (eg. [14], ["UNAVAILABLE"] or ["unavailable"]) + * This field is required and must be non-empty. + */ + Set retryableStatusCodes; + + public Map buildMap() { + Map map = new HashMap<>(); + + map.put("maxAttempts", Double.valueOf(this.getMaxAttempts())); + map.put("initialBackoff", String.format("%ss", this.getInitialBackoff())); + map.put("maxBackoff", String.format("%ss", this.getMaxBackoff())); + map.put("backoffMultiplier", Double.valueOf(this.getBackoffMultiplier())); + List statusCodesList = new ArrayList<>(); + if (!CollectionUtils.isEmpty(this.getRetryableStatusCodes())) { + this.getRetryableStatusCodes().forEach(code -> statusCodesList.add(code.name())); + } + map.put("retryableStatusCodes", statusCodesList); + return map; + } + + public void copyDefaultsFrom(final RetryPolicyConfig config) { + if (this == config) { + return; + } + if (this.maxAttempts == null) { + this.maxAttempts = config.maxAttempts; + } + if (this.initialBackoff == null) { + this.initialBackoff = config.initialBackoff; + } + if (this.maxBackoff == null) { + this.maxBackoff = config.maxBackoff; + } + if (this.backoffMultiplier == null) { + this.backoffMultiplier = config.backoffMultiplier; + } + if (this.retryableStatusCodes == null || retryableStatusCodes.isEmpty()) { + this.retryableStatusCodes = new HashSet<>(); + if (config.retryableStatusCodes != null && !config.retryableStatusCodes.isEmpty()) { + this.retryableStatusCodes.addAll(config.retryableStatusCodes); + } + } + } + + public String getMaxAttempts() { + return maxAttempts; + } + + public void setMaxAttempts(String maxAttempts) { + this.maxAttempts = maxAttempts; + } + + public double getInitialBackoff() { + return initialBackoff; + } + + public void setInitialBackoff(double initialBackoff) { + this.initialBackoff = initialBackoff; + } + + public double getMaxBackoff() { + return maxBackoff; + } + + public void setMaxBackoff(double maxBackoff) { + this.maxBackoff = maxBackoff; + } + + public double getBackoffMultiplier() { + return backoffMultiplier; + } + + public void setBackoffMultiplier(double backoffMultiplier) { + this.backoffMultiplier = backoffMultiplier; + } + + public Set getRetryableStatusCodes() { + return retryableStatusCodes; + } + + public void setRetryableStatusCodes(Set retryableStatusCodes) { + this.retryableStatusCodes = retryableStatusCodes; + } +} From d438e022a09aa493e9f8ad33c613816a008b8f85 Mon Sep 17 00:00:00 2001 From: wushengju <1205902694@qq.com> Date: Thu, 12 Aug 2021 15:06:15 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AF=B9ServiceConfig?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AbstractChannelFactory.java | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java index c92d7936c..149427c7e 100644 --- a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java @@ -22,7 +22,11 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import java.time.Duration; -import java.util.*; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.ArrayList; +import java.util.Collections; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; @@ -51,7 +55,6 @@ * connection pooling and thus needs to be {@link #close() closed} after usage. * * @param The type of builder used by this channel factory. - * * @author Michael (yidongnan@gmail.com) * @author Daniel Theuke (daniel.theuke@heuboe.de) * @since 5/17/16 @@ -74,13 +77,13 @@ public abstract class AbstractChannelFactory> /** * Creates a new AbstractChannelFactory with eager initialized references. * - * @param properties The properties for the channels to create. + * @param properties The properties for the channels to create. * @param globalClientInterceptorRegistry The interceptor registry to use. - * @param channelConfigurers The channel configurers to use. Can be empty. + * @param channelConfigurers The channel configurers to use. Can be empty. */ public AbstractChannelFactory(final GrpcChannelsProperties properties, - final GlobalClientInterceptorRegistry globalClientInterceptorRegistry, - final List channelConfigurers) { + final GlobalClientInterceptorRegistry globalClientInterceptorRegistry, + final List channelConfigurers) { this.properties = requireNonNull(properties, "properties"); this.globalClientInterceptorRegistry = requireNonNull(globalClientInterceptorRegistry, "globalClientInterceptorRegistry"); @@ -94,7 +97,7 @@ public final Channel createChannel(final String name) { @Override public Channel createChannel(final String name, final List customInterceptors, - final boolean sortInterceptors) { + final boolean sortInterceptors) { final Channel channel; synchronized (this) { if (this.shutdown) { @@ -156,7 +159,7 @@ protected final GrpcChannelProperties getPropertiesFor(final String name) { * by this library. * * @param builder The channel builder to configure. - * @param name The name of the client to configure. + * @param name The name of the client to configure. */ protected void configure(final T builder, final String name) { configureKeepAlive(builder, name); @@ -220,7 +223,7 @@ protected Map buildDefaultServiceConfig(final GrpcChannelPropert * Configures the keep alive options that should be used by the channel. * * @param builder The channel builder to configure. - * @param name The name of the client to configure. + * @param name The name of the client to configure. */ protected void configureKeepAlive(final T builder, final String name) { final GrpcChannelProperties properties = getPropertiesFor(name); @@ -235,7 +238,7 @@ protected void configureKeepAlive(final T builder, final String name) { * Configures the security options that should be used by the channel. * * @param builder The channel builder to configure. - * @param name The name of the client to configure. + * @param name The name of the client to configure. */ protected void configureSecurity(final T builder, final String name) { final GrpcChannelProperties properties = getPropertiesFor(name); @@ -265,7 +268,7 @@ protected boolean isNonNullAndNonBlank(final String value) { * Configures limits such as max message sizes that should be used by the channel. * * @param builder The channel builder to configure. - * @param name The name of the client to configure. + * @param name The name of the client to configure. */ protected void configureLimits(final T builder, final String name) { final GrpcChannelProperties properties = getPropertiesFor(name); @@ -279,7 +282,7 @@ protected void configureLimits(final T builder, final String name) { * Configures the compression options that should be used by the channel. * * @param builder The channel builder to configure. - * @param name The name of the client to configure. + * @param name The name of the client to configure. */ protected void configureCompression(final T builder, final String name) { final GrpcChannelProperties properties = getPropertiesFor(name); @@ -296,7 +299,7 @@ public Map getConnectivityState() { /** * Watch the given channel for connectivity changes. * - * @param name The name of the channel in the state overview. + * @param name The name of the channel in the state overview. * @param channel The channel to watch the state of. */ protected void watchConnectivityState(final String name, final ManagedChannel channel) { From 4bb0f8ff2b1db932e43db167069e4a850ab4eff9 Mon Sep 17 00:00:00 2001 From: wushengju <1205902694@qq.com> Date: Thu, 12 Aug 2021 16:58:17 +0800 Subject: [PATCH 3/6] add ServiceConfig for retry policy --- .../AbstractChannelFactory.java | 38 ++++---- .../client/config/GrpcChannelProperties.java | 12 +-- .../boot/grpc/client/config/MethodConfig.java | 58 ++++++++---- .../boot/grpc/client/config/NameConfig.java | 53 ++++++++--- .../grpc/client/config/RetryPolicyConfig.java | 91 +++++++++++-------- ...elPropertiesGivenMethodConfigUnitTest.java | 49 ++++++++++ 6 files changed, 204 insertions(+), 97 deletions(-) create mode 100644 grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGivenMethodConfigUnitTest.java diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java index 149427c7e..85ed7cfbb 100644 --- a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java @@ -22,11 +22,11 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.HashMap; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; @@ -35,7 +35,6 @@ import javax.annotation.PreDestroy; import javax.annotation.concurrent.GuardedBy; -import net.devh.boot.grpc.client.config.*; import org.springframework.util.unit.DataSize; import com.google.common.collect.Lists; @@ -47,6 +46,7 @@ import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.config.*; import net.devh.boot.grpc.client.config.GrpcChannelProperties.Security; import net.devh.boot.grpc.client.interceptor.GlobalClientInterceptorRegistry; @@ -77,13 +77,13 @@ public abstract class AbstractChannelFactory> /** * Creates a new AbstractChannelFactory with eager initialized references. * - * @param properties The properties for the channels to create. + * @param properties The properties for the channels to create. * @param globalClientInterceptorRegistry The interceptor registry to use. - * @param channelConfigurers The channel configurers to use. Can be empty. + * @param channelConfigurers The channel configurers to use. Can be empty. */ public AbstractChannelFactory(final GrpcChannelsProperties properties, - final GlobalClientInterceptorRegistry globalClientInterceptorRegistry, - final List channelConfigurers) { + final GlobalClientInterceptorRegistry globalClientInterceptorRegistry, + final List channelConfigurers) { this.properties = requireNonNull(properties, "properties"); this.globalClientInterceptorRegistry = requireNonNull(globalClientInterceptorRegistry, "globalClientInterceptorRegistry"); @@ -97,7 +97,7 @@ public final Channel createChannel(final String name) { @Override public Channel createChannel(final String name, final List customInterceptors, - final boolean sortInterceptors) { + final boolean sortInterceptors) { final Channel channel; synchronized (this) { if (this.shutdown) { @@ -159,7 +159,7 @@ protected final GrpcChannelProperties getPropertiesFor(final String name) { * by this library. * * @param builder The channel builder to configure. - * @param name The name of the client to configure. + * @param name The name of the client to configure. */ protected void configure(final T builder, final String name) { configureKeepAlive(builder, name); @@ -176,13 +176,13 @@ protected void configure(final T builder, final String name) { * Configures the retry options that should be used by the channel. * * @param builder The channel builder to configure. - * @param name The name of the client to configure. + * @param name The name of the client to configure. */ protected void configureRetryEnabled(final T builder, final String name) { final GrpcChannelProperties properties = getPropertiesFor(name); if (properties.isRetryEnabled()) { builder.enableRetry(); - //build retry policy by default service config + // build retry policy by default service config builder.defaultServiceConfig(buildDefaultServiceConfig(properties)); } } @@ -201,14 +201,14 @@ protected Map buildDefaultServiceConfig(final GrpcChannelPropert List> methodConfigJsonList = new ArrayList<>(); methodConfigList.forEach(methodConfig -> { Map methodConfigMap = new HashMap<>(); - //set method config + // set method config List nameConfigList = methodConfig.getName(); if (null != nameConfigList && !nameConfigList.isEmpty()) { List> list = new ArrayList<>(); nameConfigList.forEach(nameConfig -> list.add(nameConfig.buildMap())); methodConfigMap.put("name", list); } - //set retry policy + // set retry policy RetryPolicyConfig retryConfig = methodConfig.getRetryPolicy(); if (retryConfig != null) { methodConfigMap.put("retryPolicy", retryConfig.buildMap()); @@ -223,7 +223,7 @@ protected Map buildDefaultServiceConfig(final GrpcChannelPropert * Configures the keep alive options that should be used by the channel. * * @param builder The channel builder to configure. - * @param name The name of the client to configure. + * @param name The name of the client to configure. */ protected void configureKeepAlive(final T builder, final String name) { final GrpcChannelProperties properties = getPropertiesFor(name); @@ -238,7 +238,7 @@ protected void configureKeepAlive(final T builder, final String name) { * Configures the security options that should be used by the channel. * * @param builder The channel builder to configure. - * @param name The name of the client to configure. + * @param name The name of the client to configure. */ protected void configureSecurity(final T builder, final String name) { final GrpcChannelProperties properties = getPropertiesFor(name); @@ -268,7 +268,7 @@ protected boolean isNonNullAndNonBlank(final String value) { * Configures limits such as max message sizes that should be used by the channel. * * @param builder The channel builder to configure. - * @param name The name of the client to configure. + * @param name The name of the client to configure. */ protected void configureLimits(final T builder, final String name) { final GrpcChannelProperties properties = getPropertiesFor(name); @@ -282,7 +282,7 @@ protected void configureLimits(final T builder, final String name) { * Configures the compression options that should be used by the channel. * * @param builder The channel builder to configure. - * @param name The name of the client to configure. + * @param name The name of the client to configure. */ protected void configureCompression(final T builder, final String name) { final GrpcChannelProperties properties = getPropertiesFor(name); @@ -299,7 +299,7 @@ public Map getConnectivityState() { /** * Watch the given channel for connectivity changes. * - * @param name The name of the channel in the state overview. + * @param name The name of the channel in the state overview. * @param channel The channel to watch the state of. */ protected void watchConnectivityState(final String name, final ManagedChannel channel) { diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java index ea938f9d8..53d46759e 100644 --- a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java @@ -22,7 +22,6 @@ import java.net.URI; import java.time.Duration; import java.time.temporal.ChronoUnit; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; @@ -520,16 +519,7 @@ public void copyDefaultsFrom(final GrpcChannelProperties config) { if (this.retryEnabled == null) { this.retryEnabled = config.retryEnabled; } - if (this.methodConfig == null || this.methodConfig.isEmpty()) { - this.methodConfig = new ArrayList<>(); - if (config.getMethodConfig() != null && !config.getMethodConfig().isEmpty()) { - config.getMethodConfig().forEach(cf -> { - MethodConfig newMethodConfig = new MethodConfig(); - newMethodConfig.copyDefaultsFrom(cf); - this.methodConfig.add(newMethodConfig); - }); - } - } + MethodConfig.copyDefaultsFrom(this.methodConfig, config.methodConfig); this.security.copyDefaultsFrom(config.security); } diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/MethodConfig.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/MethodConfig.java index d912512b3..0c1a99675 100644 --- a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/MethodConfig.java +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/MethodConfig.java @@ -1,22 +1,35 @@ +/* + * Copyright (c) 2016-2021 Michael Zhang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package net.devh.boot.grpc.client.config; +import java.util.ArrayList; +import java.util.List; + import lombok.EqualsAndHashCode; import lombok.ToString; -import java.util.ArrayList; -import java.util.List; /** - * @Title 方法配置信息实体, 请参考如下链接: - * https://github.com/grpc/proposal/blob/37e658b12f1684f29b3acca04f0167b84d502876/A6-client-retries.md#grpc-retry-design - * https://github.com/grpc/grpc/blob/master/doc/service_config.md - * @Description MethodConfig - * @Program spring-cloud-tcl-starter - * @Author wushengju - * @Version 1.0 - * @Date 2021-08-10 13:38 - * @Copyright Copyright (c) 2021 TCL Inc. All rights reserved - **/ + * The method config for retry policy config. + * + * @author wushengju + * @since 8/12/2021 + */ @ToString @EqualsAndHashCode public class MethodConfig { @@ -45,18 +58,23 @@ public void setName(List name) { this.name = name; } - public void copyDefaultsFrom(final MethodConfig config) { + public void copyDefaultsFrom(MethodConfig config) { if (this == config) { return; } this.retryPolicy.copyDefaultsFrom(config.retryPolicy); - if (this.name == null || this.name.isEmpty()) { - if (config.getName() != null && !config.getName().isEmpty()) { - this.name = new ArrayList<>(); - config.getName().forEach(nameConfig -> { - NameConfig newConfig = new NameConfig(); - newConfig.copyDefaultsFrom(nameConfig); - this.name.add(newConfig); + NameConfig.copyDefaultsFrom(this.name, config.getName()); + } + + public static void copyDefaultsFrom(List methodConfig, final List config) { + if (methodConfig == null || methodConfig.isEmpty()) { + methodConfig = new ArrayList<>(); + if (config != null && !config.isEmpty()) { + List finalMethodConfig = methodConfig; + config.forEach(conf -> { + MethodConfig newMethodConfig = new MethodConfig(); + newMethodConfig.copyDefaultsFrom(conf); + finalMethodConfig.add(newMethodConfig); }); } } diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/NameConfig.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/NameConfig.java index 2ee97f2ad..0e716b4c2 100644 --- a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/NameConfig.java +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/NameConfig.java @@ -1,20 +1,36 @@ -package net.devh.boot.grpc.client.config; +/* + * Copyright (c) 2016-2021 Michael Zhang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ -import lombok.EqualsAndHashCode; -import lombok.ToString; +package net.devh.boot.grpc.client.config; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import lombok.EqualsAndHashCode; +import lombok.ToString; + /** - * @Title method config - * @Description MethodConfig - * @Program spring-cloud-tcl-starter - * @Author wushengju - * @Version 1.0 - * @Date 2021-08-10 10:55 - * @Copyright Copyright (c) 2021 TCL Inc. All rights reserved - **/ + * The name config for service and method. + * + * @author wushengju + * @since 8/12/2021 + */ @ToString @EqualsAndHashCode public class NameConfig { @@ -45,6 +61,21 @@ public void copyDefaultsFrom(final NameConfig config) { this.method = config.method; } } + + public static void copyDefaultsFrom(List nameConfigs, final List config) { + if (nameConfigs == null || nameConfigs.isEmpty()) { + nameConfigs = new ArrayList<>(); + if (config != null && !config.isEmpty()) { + List finalNameConfigs = nameConfigs; + config.forEach(nameConfig -> { + NameConfig newConfig = new NameConfig(); + newConfig.copyDefaultsFrom(nameConfig); + finalNameConfigs.add(newConfig); + }); + } + } + } + public String getService() { return service; } diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/RetryPolicyConfig.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/RetryPolicyConfig.java index bdca5eff1..fb1229e2c 100644 --- a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/RetryPolicyConfig.java +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/RetryPolicyConfig.java @@ -1,58 +1,77 @@ +/* + * Copyright (c) 2016-2021 Michael Zhang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package net.devh.boot.grpc.client.config; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.*; + +import org.springframework.boot.convert.DurationUnit; +import org.springframework.util.CollectionUtils; + import io.grpc.Status; import lombok.EqualsAndHashCode; import lombok.ToString; -import org.springframework.util.CollectionUtils; - -import java.util.*; /** - * @Title 重试配置信息 - * @Description RetryPolicyProperties - * @Program spring-cloud-tcl-starter - * @Author wushengju - * @Version 1.0 - * @Date 2021-08-09 15:25 - * @Copyright Copyright (c) 2021 TCL Inc. All rights reserved - **/ + * The retry policy config. + * + * @author wushengju + * @since 8/12/2021 + */ @ToString @EqualsAndHashCode public class RetryPolicyConfig { /** - * The maximum number of RPC attempts, including the original RPC. - * This field is required and must be two or greater. + * The maximum number of RPC attempts, including the original RPC. This field is required and must be two or + * greater. */ - String maxAttempts; + private Double maxAttempts; /** - * Exponential backoff parameters. The initial retry attempt will occur at - * random(0, initialBackoff). In general, the nth attempt since the last - * server pushback response (if any), will occur at random(0, min(initialBackoff*backoffMultiplier**(n-1), maxBackoff)). - * Exponential backoff parameters,Duration is seconds + * Exponential backoff parameters. The initial retry attempt will occur at random(0, initialBackoff). In general, + * the nth attempt since the last server pushback response (if any), will occur at random(0, + * min(initialBackoff*backoffMultiplier**(n-1), maxBackoff)). Exponential backoff parameters,Duration is seconds * Required. Must be greater than zero */ - Double initialBackoff; + @DurationUnit(ChronoUnit.SECONDS) + private Duration initialBackoff; /** * Required. Must be greater than zero,Duration is seconds */ - Double maxBackoff; + @DurationUnit(ChronoUnit.SECONDS) + private Duration maxBackoff; /** * Required. Must be greater than zero. */ - Double backoffMultiplier; + private Double backoffMultiplier; /** - * The set of status codes which may be retried. - * Status codes are specified in the integer form or the case-insensitive string form (eg. [14], ["UNAVAILABLE"] or ["unavailable"]) - * This field is required and must be non-empty. + * The set of status codes which may be retried. Status codes are specified in the integer form or the + * case-insensitive string form (eg. [14], ["UNAVAILABLE"] or ["unavailable"]) This field is required and must be + * non-empty. */ - Set retryableStatusCodes; + private Set retryableStatusCodes; public Map buildMap() { Map map = new HashMap<>(); - map.put("maxAttempts", Double.valueOf(this.getMaxAttempts())); - map.put("initialBackoff", String.format("%ss", this.getInitialBackoff())); - map.put("maxBackoff", String.format("%ss", this.getMaxBackoff())); + map.put("maxAttempts", this.getMaxAttempts()); + map.put("initialBackoff", String.format("%ss", this.getInitialBackoff().getSeconds())); + map.put("maxBackoff", String.format("%ss", this.getMaxBackoff().getSeconds())); map.put("backoffMultiplier", Double.valueOf(this.getBackoffMultiplier())); List statusCodesList = new ArrayList<>(); if (!CollectionUtils.isEmpty(this.getRetryableStatusCodes())) { @@ -86,35 +105,35 @@ public void copyDefaultsFrom(final RetryPolicyConfig config) { } } - public String getMaxAttempts() { + public Double getMaxAttempts() { return maxAttempts; } - public void setMaxAttempts(String maxAttempts) { + public void setMaxAttempts(Double maxAttempts) { this.maxAttempts = maxAttempts; } - public double getInitialBackoff() { + public Duration getInitialBackoff() { return initialBackoff; } - public void setInitialBackoff(double initialBackoff) { + public void setInitialBackoff(Duration initialBackoff) { this.initialBackoff = initialBackoff; } - public double getMaxBackoff() { + public Duration getMaxBackoff() { return maxBackoff; } - public void setMaxBackoff(double maxBackoff) { + public void setMaxBackoff(Duration maxBackoff) { this.maxBackoff = maxBackoff; } - public double getBackoffMultiplier() { + public Double getBackoffMultiplier() { return backoffMultiplier; } - public void setBackoffMultiplier(double backoffMultiplier) { + public void setBackoffMultiplier(Double backoffMultiplier) { this.backoffMultiplier = backoffMultiplier; } diff --git a/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGivenMethodConfigUnitTest.java b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGivenMethodConfigUnitTest.java new file mode 100644 index 000000000..1b100d755 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGivenMethodConfigUnitTest.java @@ -0,0 +1,49 @@ +package net.devh.boot.grpc.client.config; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @Title Test + * @Description GrpcChannelPropertiesGivenMethodConfigUnitTest + * @Program grpc-spring-boot-starter + * @Author wushengju + * @Version 1.0 + * @Date 2021-08-12 16:46 + * @Copyright Copyright (c) 2021 TCL Inc. All rights reserved + **/ +@ExtendWith(SpringExtension.class) +@SpringBootTest(properties = { + "grpc.client.test.retry-enabled=true", + "grpc.client.test.method-config[0].name[0].service=helloworld.Greeter", + "grpc.client.test.method-config[0].name[0].method=SayHello", + "grpc.client.test.method-config[0].retry-policy.max-attempts=2", + "grpc.client.test.method-config[0].retry-policy.initial-backoff=1", + "grpc.client.test.method-config[0].retry-policy.max-backoff=1", + "grpc.client.test.method-config[0].retry-policy.backoff-multiplier=2", + "grpc.client.test.method-config[0].retry-policy.retryable-status-codes=UNKNOWN,UNAVAILABLE" +}) +public class GrpcChannelPropertiesGivenMethodConfigUnitTest { + + @Autowired + private GrpcChannelsProperties grpcChannelsProperties; + + @Test + void test() { + final GrpcChannelProperties properties = this.grpcChannelsProperties.getChannel("test"); + assertEquals(true, properties.getRetryEnabled()); + assertEquals("helloworld.Greeter", properties.getMethodConfig().get(0).getName().get(0).getService()); + assertEquals("SayHello", properties.getMethodConfig().get(0).getName().get(0).getMethod()); + assertEquals(2, properties.getMethodConfig().get(0).getRetryPolicy().getMaxAttempts()); + assertEquals(1, properties.getMethodConfig().get(0).getRetryPolicy().getInitialBackoff().getSeconds()); + assertEquals(1, properties.getMethodConfig().get(0).getRetryPolicy().getMaxBackoff().getSeconds()); + assertEquals(2, properties.getMethodConfig().get(0).getRetryPolicy().getBackoffMultiplier()); + + assertEquals(2, properties.getMethodConfig().get(0).getRetryPolicy().getRetryableStatusCodes().size()); + } +} From 20c2d06bafc4a17e49e99a49ed89864a094821c4 Mon Sep 17 00:00:00 2001 From: wushengju <1205902694@qq.com> Date: Thu, 12 Aug 2021 17:06:13 +0800 Subject: [PATCH 4/6] add ServiceConfig for retry policy --- ...elPropertiesGivenMethodConfigUnitTest.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGivenMethodConfigUnitTest.java b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGivenMethodConfigUnitTest.java index 1b100d755..6872c8679 100644 --- a/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGivenMethodConfigUnitTest.java +++ b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGivenMethodConfigUnitTest.java @@ -1,13 +1,30 @@ +/* + * Copyright (c) 2016-2021 Michael Zhang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package net.devh.boot.grpc.client.config; +import static org.junit.jupiter.api.Assertions.assertEquals; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; -import static org.junit.jupiter.api.Assertions.assertEquals; - /** * @Title Test * @Description GrpcChannelPropertiesGivenMethodConfigUnitTest From 495fb111dc0f8fccd353e94b51d288c59e4f3e92 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Fri, 3 Sep 2021 00:09:06 +0200 Subject: [PATCH 5/6] Temp: Cleanup, slight refactor, add retry tests --- .../AbstractChannelFactory.java | 37 ++-- .../client/config/GrpcChannelProperties.java | 13 +- .../boot/grpc/client/config/MethodConfig.java | 91 +++++---- .../boot/grpc/client/config/NameConfig.java | 115 ++++++----- .../grpc/client/config/RetryPolicyConfig.java | 178 +++++++++--------- ...rpcChannelPropertiesMethodConfigTest.java} | 47 +++-- .../test/config/DynamicTestServiceConfig.java | 69 +++++++ .../test/setup/RetryServerClientTest.java | 118 ++++++++++++ .../boot/grpc/test/util/GrpcAssertions.java | 18 +- 9 files changed, 452 insertions(+), 234 deletions(-) rename grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/{GrpcChannelPropertiesGivenMethodConfigUnitTest.java => GrpcChannelPropertiesMethodConfigTest.java} (51%) create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/config/DynamicTestServiceConfig.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/RetryServerClientTest.java diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java index 85ed7cfbb..fdf08ba24 100644 --- a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java @@ -24,7 +24,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -46,8 +46,11 @@ import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import lombok.extern.slf4j.Slf4j; -import net.devh.boot.grpc.client.config.*; +import net.devh.boot.grpc.client.config.GrpcChannelProperties; import net.devh.boot.grpc.client.config.GrpcChannelProperties.Security; +import net.devh.boot.grpc.client.config.GrpcChannelsProperties; +import net.devh.boot.grpc.client.config.MethodConfig; +import net.devh.boot.grpc.client.config.NegotiationType; import net.devh.boot.grpc.client.interceptor.GlobalClientInterceptorRegistry; /** @@ -183,39 +186,23 @@ protected void configureRetryEnabled(final T builder, final String name) { if (properties.isRetryEnabled()) { builder.enableRetry(); // build retry policy by default service config + // TODO: Wrap field in defaultServiceConfig builder.defaultServiceConfig(buildDefaultServiceConfig(properties)); } } /** - * build service config object + * Builds the service config object. * * @param properties The properties of + * @return The json alike service config. */ protected Map buildDefaultServiceConfig(final GrpcChannelProperties properties) { - Map serviceConfig = new HashMap<>(); - List methodConfigList = properties.getMethodConfig(); - if (null == methodConfigList || methodConfigList.isEmpty()) { - return serviceConfig; + final Map serviceConfig = new LinkedHashMap<>(); + final List methodConfigList = properties.getMethodConfig(); + if (methodConfigList != null && !methodConfigList.isEmpty()) { + serviceConfig.put("methodConfig", MethodConfig.buildMaps(methodConfigList)); } - List> methodConfigJsonList = new ArrayList<>(); - methodConfigList.forEach(methodConfig -> { - Map methodConfigMap = new HashMap<>(); - // set method config - List nameConfigList = methodConfig.getName(); - if (null != nameConfigList && !nameConfigList.isEmpty()) { - List> list = new ArrayList<>(); - nameConfigList.forEach(nameConfig -> list.add(nameConfig.buildMap())); - methodConfigMap.put("name", list); - } - // set retry policy - RetryPolicyConfig retryConfig = methodConfig.getRetryPolicy(); - if (retryConfig != null) { - methodConfigMap.put("retryPolicy", retryConfig.buildMap()); - } - methodConfigJsonList.add(methodConfigMap); - }); - serviceConfig.put("methodConfig", methodConfigJsonList); return serviceConfig; } diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java index 53d46759e..da935a1a2 100644 --- a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java @@ -444,19 +444,15 @@ public void setRetryEnabled(final Boolean retryEnabled) { this.retryEnabled = retryEnabled; } - public Boolean getRetryEnabled() { - return retryEnabled; - } - // -------------------------------------------------- private List methodConfig; public List getMethodConfig() { - return methodConfig; + return this.methodConfig; } - public void setMethodConfig(List methodConfig) { + public void setMethodConfig(final List methodConfig) { this.methodConfig = methodConfig; } @@ -519,7 +515,10 @@ public void copyDefaultsFrom(final GrpcChannelProperties config) { if (this.retryEnabled == null) { this.retryEnabled = config.retryEnabled; } - MethodConfig.copyDefaultsFrom(this.methodConfig, config.methodConfig); + if (this.methodConfig == null || this.methodConfig.isEmpty()) { + // TBD: Should we smartly merge the method configs? + this.methodConfig = config.methodConfig == null ? null : MethodConfig.copy(config.methodConfig); + } this.security.copyDefaultsFrom(config.security); } diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/MethodConfig.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/MethodConfig.java index 0c1a99675..0ef6cadbf 100644 --- a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/MethodConfig.java +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/MethodConfig.java @@ -17,22 +17,29 @@ package net.devh.boot.grpc.client.config; -import java.util.ArrayList; +import static java.util.Objects.requireNonNull; + +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; -import lombok.EqualsAndHashCode; -import lombok.ToString; +import lombok.Data; /** * The method config for retry policy config. * + *

+ * For the exact specification refer to: + * A6-client-retries + *

+ * * @author wushengju - * @since 8/12/2021 */ -@ToString -@EqualsAndHashCode +@Data public class MethodConfig { + /** * retry policy config */ @@ -42,41 +49,57 @@ public class MethodConfig { */ private List name; - public RetryPolicyConfig getRetryPolicy() { - return retryPolicy; - } - public void setRetryPolicy(RetryPolicyConfig retryPolicy) { - this.retryPolicy = retryPolicy; - } - - public List getName() { - return name; + /** + * Creates a copy of this instance. + * + * @return The newly created copy. + */ + public MethodConfig copy() { + final MethodConfig copy = new MethodConfig(); + copy.retryPolicy = requireNonNull(this.retryPolicy, "retryPolicy").copy(); + copy.name = NameConfig.copy(this.name); + return copy; } - public void setName(List name) { - this.name = name; + /** + * Creates a copy of the given instances. + * + * @param configs The configs to copy. + * @return The copied instances. + */ + public static List copy(final List configs) { + return requireNonNull(configs, "configs").stream() + .map(MethodConfig::copy) + .collect(Collectors.toList()); } - public void copyDefaultsFrom(MethodConfig config) { - if (this == config) { - return; + /** + * Builds a json like map from this instance. + * + * @return The json like map representation of this instance. + */ + public Map buildMap() { + final Map map = new LinkedHashMap<>(); + if (this.name != null && !this.name.isEmpty()) { + map.put("name", NameConfig.buildMaps(this.name)); + } + if (this.retryPolicy != null) { + map.put("retryPolicy", this.retryPolicy.buildMap()); } - this.retryPolicy.copyDefaultsFrom(config.retryPolicy); - NameConfig.copyDefaultsFrom(this.name, config.getName()); + return map; } - public static void copyDefaultsFrom(List methodConfig, final List config) { - if (methodConfig == null || methodConfig.isEmpty()) { - methodConfig = new ArrayList<>(); - if (config != null && !config.isEmpty()) { - List finalMethodConfig = methodConfig; - config.forEach(conf -> { - MethodConfig newMethodConfig = new MethodConfig(); - newMethodConfig.copyDefaultsFrom(conf); - finalMethodConfig.add(newMethodConfig); - }); - } - } + /** + * Builds a json like map from the given instances. + * + * @param configs The configs to convert. + * @return The json like array of maps representation of the instances. + */ + public static List> buildMaps(final List configs) { + return requireNonNull(configs, "configs").stream() + .map(MethodConfig::buildMap) + .collect(Collectors.toList()); } + } diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/NameConfig.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/NameConfig.java index 0e716b4c2..b71810ae8 100644 --- a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/NameConfig.java +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/NameConfig.java @@ -17,78 +17,91 @@ package net.devh.boot.grpc.client.config; -import java.util.ArrayList; -import java.util.HashMap; +import static java.util.Objects.requireNonNull; + +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; -import lombok.EqualsAndHashCode; -import lombok.ToString; +import lombok.Data; /** * The name config for service and method. * + *

+ * If both the service and method name are empty, then this applies to all requests. + *

+ * + *

+ * If only the method name is empty, then this applies to all methods in the given service. + *

+ * + *

+ * For the exact specification refer to: + * A6-client-retries + *

+ * * @author wushengju - * @since 8/12/2021 */ -@ToString -@EqualsAndHashCode +@Data public class NameConfig { + /** - * the service name which defined in xx.proto + * The service name as defined in your proto file. May be empty to match all services, but may never be null. */ - private String service; + private String service = ""; /** - * the method name which you will call + * The method name which you will call. May be empty to match all method within the service, but may never be null. */ - private String method; - - public Map buildMap() { - Map map = new HashMap<>(); - map.put("service", this.getService()); - map.put("method", this.getMethod()); - return map; - } - - public void copyDefaultsFrom(final NameConfig config) { - if (this == config) { - return; - } - if (this.service == null) { - this.service = config.service; - } - if (this.method == null) { - this.method = config.method; - } - } + private String method = ""; - public static void copyDefaultsFrom(List nameConfigs, final List config) { - if (nameConfigs == null || nameConfigs.isEmpty()) { - nameConfigs = new ArrayList<>(); - if (config != null && !config.isEmpty()) { - List finalNameConfigs = nameConfigs; - config.forEach(nameConfig -> { - NameConfig newConfig = new NameConfig(); - newConfig.copyDefaultsFrom(nameConfig); - finalNameConfigs.add(newConfig); - }); - } - } + /** + * Creates a copy of this instance. + * + * @return The newly created copy. + */ + public NameConfig copy() { + final NameConfig copy = new NameConfig(); + copy.service = this.service; + copy.method = this.method; + return copy; } - public String getService() { - return service; + /** + * Creates a copy of the given instances. + * + * @param configs The configs to copy. + * @return The copied instances. + */ + public static List copy(final List configs) { + return requireNonNull(configs, "configs").stream() + .map(NameConfig::copy) + .collect(Collectors.toList()); } - public void setService(String service) { - this.service = service; + /** + * Builds a json like map from this instance. + * + * @return The json like map representation of this instance. + */ + public Map buildMap() { + final Map map = new LinkedHashMap<>(); + map.put("service", this.service); + map.put("method", this.method); + return map; } - public String getMethod() { - return method; + /** + * Builds a json like map from the given instances. + * + * @param configs The configs to convert. + * @return The json like array of maps representation of the instances. + */ + public static List> buildMaps(final List configs) { + return requireNonNull(configs, "configs").stream() + .map(NameConfig::buildMap) + .collect(Collectors.toList()); } - public void setMethod(String method) { - this.method = method; - } } diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/RetryPolicyConfig.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/RetryPolicyConfig.java index fb1229e2c..22f90ebca 100644 --- a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/RetryPolicyConfig.java +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/RetryPolicyConfig.java @@ -17,131 +17,129 @@ package net.devh.boot.grpc.client.config; +import static java.util.Objects.requireNonNull; + import java.time.Duration; import java.time.temporal.ChronoUnit; -import java.util.*; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.validation.constraints.Min; import org.springframework.boot.convert.DurationUnit; -import org.springframework.util.CollectionUtils; import io.grpc.Status; -import lombok.EqualsAndHashCode; -import lombok.ToString; +import io.grpc.Status.Code; +import lombok.Data; /** * The retry policy config. * + *

+ * For the exact specification refer to: + * A6-client-retries + *

+ * * @author wushengju - * @since 8/12/2021 */ -@ToString -@EqualsAndHashCode +@Data public class RetryPolicyConfig { + /** - * The maximum number of RPC attempts, including the original RPC. This field is required and must be two or - * greater. + * The maximum number of RPC attempts, including the original RPC. This field is required and must be one or + * greater. A value of {@code 1} indicates, no retries after the initial call. */ - private Double maxAttempts; + @Min(1) + private int maxAttempts; + /** - * Exponential backoff parameters. The initial retry attempt will occur at random(0, initialBackoff). In general, - * the nth attempt since the last server pushback response (if any), will occur at random(0, - * min(initialBackoff*backoffMultiplier**(n-1), maxBackoff)). Exponential backoff parameters,Duration is seconds - * Required. Must be greater than zero + * Exponential backoff parameter: Defines the upper limit for the first backoff time. + * + *

+ * The initial retry attempt will occur at {@code random(0, initialBackoff)}. In general, the n-th attempt since the + * last server pushback response (if any), will occur at + * {@code random(0, min(initialBackoff*backoffMultiplier**(n-1), maxBackoff))}. + *

+ * + *

+ * Default unit seconds. Must be greater than zero. + *

*/ @DurationUnit(ChronoUnit.SECONDS) private Duration initialBackoff; + /** - * Required. Must be greater than zero,Duration is seconds + * Exponential backoff parameter: The upper duration limit for backing off an attempt. + * + *

+ * Default unit seconds. Must be greater than zero. + *

*/ @DurationUnit(ChronoUnit.SECONDS) private Duration maxBackoff; + /** - * Required. Must be greater than zero. + * Exponential backoff parameter: The multiplier to apply to the (initial) backoff. Values below {@code 1.0}, will + * result in faster retries the more attempts have been made. {@code 1.0} will result in averagely same retry + * frequency. Values above {@code 1.0} will slow down the requests with each attempt. + * + *

+ * Must be greater than zero. + *

*/ - private Double backoffMultiplier; + private double backoffMultiplier; + /** * The set of status codes which may be retried. Status codes are specified in the integer form or the - * case-insensitive string form (eg. [14], ["UNAVAILABLE"] or ["unavailable"]) This field is required and must be - * non-empty. + * case-insensitive string form (e.g. {@code 14}, {@code "UNAVAILABLE"} or {@code "unavailable"}). This field is + * required and must be non-empty. */ private Set retryableStatusCodes; + /** + * Creates a copy of this instance. + * + * @return The newly created copy. + */ + public RetryPolicyConfig copy() { + final RetryPolicyConfig copy = new RetryPolicyConfig(); + copy.maxAttempts = this.maxAttempts; + copy.initialBackoff = this.initialBackoff; + copy.maxBackoff = this.maxBackoff; + copy.backoffMultiplier = this.backoffMultiplier; + copy.retryableStatusCodes = + new LinkedHashSet<>(requireNonNull(this.retryableStatusCodes, "retryableStatusCodes")); + return copy; + } + + /** + * Builds a json like map from this instance. + * + * @return The json like map representation of this instance. + */ public Map buildMap() { - Map map = new HashMap<>(); - - map.put("maxAttempts", this.getMaxAttempts()); - map.put("initialBackoff", String.format("%ss", this.getInitialBackoff().getSeconds())); - map.put("maxBackoff", String.format("%ss", this.getMaxBackoff().getSeconds())); - map.put("backoffMultiplier", Double.valueOf(this.getBackoffMultiplier())); - List statusCodesList = new ArrayList<>(); - if (!CollectionUtils.isEmpty(this.getRetryableStatusCodes())) { - this.getRetryableStatusCodes().forEach(code -> statusCodesList.add(code.name())); - } - map.put("retryableStatusCodes", statusCodesList); + final Map map = new LinkedHashMap<>(); + map.put("maxAttempts", (double) this.maxAttempts); + map.put("initialBackoff", formatDuration(requireNonNull(this.initialBackoff, "initialBackoff"))); + map.put("maxBackoff", formatDuration(requireNonNull(this.maxBackoff, "maxBackoff"))); + map.put("backoffMultiplier", this.backoffMultiplier); + map.put("retryableStatusCodes", requireNonNull(this.retryableStatusCodes, "retryableStatusCodes").stream() + .map(Code::name) + .collect(Collectors.toList())); return map; } - public void copyDefaultsFrom(final RetryPolicyConfig config) { - if (this == config) { - return; - } - if (this.maxAttempts == null) { - this.maxAttempts = config.maxAttempts; - } - if (this.initialBackoff == null) { - this.initialBackoff = config.initialBackoff; - } - if (this.maxBackoff == null) { - this.maxBackoff = config.maxBackoff; - } - if (this.backoffMultiplier == null) { - this.backoffMultiplier = config.backoffMultiplier; - } - if (this.retryableStatusCodes == null || retryableStatusCodes.isEmpty()) { - this.retryableStatusCodes = new HashSet<>(); - if (config.retryableStatusCodes != null && !config.retryableStatusCodes.isEmpty()) { - this.retryableStatusCodes.addAll(config.retryableStatusCodes); - } + private static String formatDuration(final Duration duration) { + if (duration.getNano() == 0) { + // 1s + return duration.getSeconds() + "s"; + } else { + // 1.2s + return String.format("%d.%09ds", duration.getSeconds(), duration.getNano()).replaceAll("0+s", "s"); } } - public Double getMaxAttempts() { - return maxAttempts; - } - - public void setMaxAttempts(Double maxAttempts) { - this.maxAttempts = maxAttempts; - } - - public Duration getInitialBackoff() { - return initialBackoff; - } - - public void setInitialBackoff(Duration initialBackoff) { - this.initialBackoff = initialBackoff; - } - - public Duration getMaxBackoff() { - return maxBackoff; - } - - public void setMaxBackoff(Duration maxBackoff) { - this.maxBackoff = maxBackoff; - } - - public Double getBackoffMultiplier() { - return backoffMultiplier; - } - - public void setBackoffMultiplier(Double backoffMultiplier) { - this.backoffMultiplier = backoffMultiplier; - } - - public Set getRetryableStatusCodes() { - return retryableStatusCodes; - } - - public void setRetryableStatusCodes(Set retryableStatusCodes) { - this.retryableStatusCodes = retryableStatusCodes; - } } diff --git a/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGivenMethodConfigUnitTest.java b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesMethodConfigTest.java similarity index 51% rename from grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGivenMethodConfigUnitTest.java rename to grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesMethodConfigTest.java index 6872c8679..06c972e5f 100644 --- a/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGivenMethodConfigUnitTest.java +++ b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesMethodConfigTest.java @@ -26,26 +26,20 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; /** - * @Title Test - * @Description GrpcChannelPropertiesGivenMethodConfigUnitTest - * @Program grpc-spring-boot-starter - * @Author wushengju - * @Version 1.0 - * @Date 2021-08-12 16:46 - * @Copyright Copyright (c) 2021 TCL Inc. All rights reserved + * Test retry configuration using properties and inheritance. **/ @ExtendWith(SpringExtension.class) @SpringBootTest(properties = { - "grpc.client.test.retry-enabled=true", - "grpc.client.test.method-config[0].name[0].service=helloworld.Greeter", - "grpc.client.test.method-config[0].name[0].method=SayHello", - "grpc.client.test.method-config[0].retry-policy.max-attempts=2", - "grpc.client.test.method-config[0].retry-policy.initial-backoff=1", - "grpc.client.test.method-config[0].retry-policy.max-backoff=1", - "grpc.client.test.method-config[0].retry-policy.backoff-multiplier=2", - "grpc.client.test.method-config[0].retry-policy.retryable-status-codes=UNKNOWN,UNAVAILABLE" + "grpc.client.GLOBAL.retry-enabled=true", + "grpc.client.GLOBAL.method-config[0].name[0].service=helloworld.Greeter", + "grpc.client.GLOBAL.method-config[0].name[0].method=SayHello", + "grpc.client.GLOBAL.method-config[0].retry-policy.max-attempts=2", + "grpc.client.GLOBAL.method-config[0].retry-policy.initial-backoff=1", + "grpc.client.GLOBAL.method-config[0].retry-policy.max-backoff=1", + "grpc.client.GLOBAL.method-config[0].retry-policy.backoff-multiplier=2", + "grpc.client.GLOBAL.method-config[0].retry-policy.retryable-status-codes=UNKNOWN,UNAVAILABLE", }) -public class GrpcChannelPropertiesGivenMethodConfigUnitTest { +class GrpcChannelPropertiesMethodConfigTest { @Autowired private GrpcChannelsProperties grpcChannelsProperties; @@ -53,14 +47,17 @@ public class GrpcChannelPropertiesGivenMethodConfigUnitTest { @Test void test() { final GrpcChannelProperties properties = this.grpcChannelsProperties.getChannel("test"); - assertEquals(true, properties.getRetryEnabled()); - assertEquals("helloworld.Greeter", properties.getMethodConfig().get(0).getName().get(0).getService()); - assertEquals("SayHello", properties.getMethodConfig().get(0).getName().get(0).getMethod()); - assertEquals(2, properties.getMethodConfig().get(0).getRetryPolicy().getMaxAttempts()); - assertEquals(1, properties.getMethodConfig().get(0).getRetryPolicy().getInitialBackoff().getSeconds()); - assertEquals(1, properties.getMethodConfig().get(0).getRetryPolicy().getMaxBackoff().getSeconds()); - assertEquals(2, properties.getMethodConfig().get(0).getRetryPolicy().getBackoffMultiplier()); - - assertEquals(2, properties.getMethodConfig().get(0).getRetryPolicy().getRetryableStatusCodes().size()); + assertEquals(true, properties.isRetryEnabled()); + final MethodConfig methodConfig = properties.getMethodConfig().get(0); + final NameConfig nameConfig = methodConfig.getName().get(0); + assertEquals("helloworld.Greeter", nameConfig.getService()); + assertEquals("SayHello", nameConfig.getMethod()); + final RetryPolicyConfig retryPolicy = methodConfig.getRetryPolicy(); + assertEquals(2, retryPolicy.getMaxAttempts()); + assertEquals(1, retryPolicy.getInitialBackoff().getSeconds()); + assertEquals(1, retryPolicy.getMaxBackoff().getSeconds()); + assertEquals(2, retryPolicy.getBackoffMultiplier()); + assertEquals(2, retryPolicy.getRetryableStatusCodes().size()); } + } diff --git a/tests/src/test/java/net/devh/boot/grpc/test/config/DynamicTestServiceConfig.java b/tests/src/test/java/net/devh/boot/grpc/test/config/DynamicTestServiceConfig.java new file mode 100644 index 000000000..08133a61f --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/config/DynamicTestServiceConfig.java @@ -0,0 +1,69 @@ + +package net.devh.boot.grpc.test.config; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.google.protobuf.Empty; + +import io.grpc.Status; +import io.grpc.stub.StreamObserver; +import net.devh.boot.grpc.server.service.GrpcService; +import net.devh.boot.grpc.test.proto.SomeType; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceImplBase; + +@Configuration +public class DynamicTestServiceConfig { + + public static BiConsumer> UNIMPLEMENTED = + errorWith(Status.UNIMPLEMENTED.withDescription("responseFunction not configured")); + + public static BiConsumer> respondWith(final String data) { + return respondWith(SomeType.newBuilder() + .setVersion(data) + .build()); + } + + public static BiConsumer> respondWith(final SomeType data) { + return (request, responseObserver) -> { + responseObserver.onNext(data); + responseObserver.onCompleted(); + }; + } + + public static BiConsumer> errorWith(final Status status) { + return (request, responseObserver) -> { + responseObserver.onError(status.asException()); + }; + } + + public static BiConsumer> increment(final AtomicInteger integer) { + return (request, responseObserver) -> { + integer.incrementAndGet(); + }; + } + + @Bean + AtomicReference>> responseFunction() { + return new AtomicReference<>(UNIMPLEMENTED); + } + + @GrpcService + TestServiceImplBase testServiceImplBase( + final AtomicReference>> responseFunction) { + + return new TestServiceImplBase() { + + @Override + public void normal(final Empty request, final StreamObserver responseObserver) { + responseFunction.get().accept(request, responseObserver); + } + + }; + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/RetryServerClientTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/RetryServerClientTest.java new file mode 100644 index 000000000..54a6c6063 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/RetryServerClientTest.java @@ -0,0 +1,118 @@ + +package net.devh.boot.grpc.test.setup; + +import static net.devh.boot.grpc.test.config.DynamicTestServiceConfig.errorWith; +import static net.devh.boot.grpc.test.config.DynamicTestServiceConfig.increment; +import static net.devh.boot.grpc.test.config.DynamicTestServiceConfig.respondWith; +import static net.devh.boot.grpc.test.util.GrpcAssertions.assertThrowsStatus; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.google.protobuf.Empty; + +import io.grpc.Status; +import io.grpc.stub.StreamObserver; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.DynamicTestServiceConfig; +import net.devh.boot.grpc.test.proto.SomeType; +import net.devh.boot.grpc.test.proto.TestServiceGrpc; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceBlockingStub; + +@Slf4j +@SpringBootTest(properties = { + "grpc.server.inProcessName=test", + "grpc.server.port=-1", + "grpc.client.GLOBAL.negotiationType=PLAINTEXT", + "grpc.client.test.address=in-process:test", + "grpc.client.GLOBAL.retry-enabled=true", + "grpc.client.GLOBAL.method-config[0].name[0].service=" + TestServiceGrpc.SERVICE_NAME, + "grpc.client.GLOBAL.method-config[0].name[0].method=", // all methods within that service + "grpc.client.GLOBAL.method-config[0].retry-policy.max-attempts=2", + "grpc.client.GLOBAL.method-config[0].retry-policy.initial-backoff=1200ms", + "grpc.client.GLOBAL.method-config[0].retry-policy.max-backoff=1", + "grpc.client.GLOBAL.method-config[0].retry-policy.backoff-multiplier=2", + "grpc.client.GLOBAL.method-config[0].retry-policy.retryable-status-codes=UNKNOWN,UNAVAILABLE", +}) +@SpringJUnitConfig(classes = { + DynamicTestServiceConfig.class, + BaseAutoConfiguration.class, +}) +@DirtiesContext +class RetryServerClientTest { + + private static final Empty EMPTY = Empty.getDefaultInstance(); + + @GrpcClient("test") + TestServiceBlockingStub stub; + + @Autowired + AtomicReference>> responseFunction; + + @Test + void testRetryConfigSuccess() { + final AtomicInteger counter = new AtomicInteger(); + + this.responseFunction.set((request, observer) -> { + log.info("Failing first request"); + this.responseFunction.set(increment(counter).andThen(respondWith("OK"))); + counter.incrementAndGet(); + errorWith(Status.UNAVAILABLE.withDescription("expected")).accept(request, observer); + }); + + final SomeType response = assertDoesNotThrow(() -> this.stub.normal(EMPTY)); + assertEquals("OK", response.getVersion()); + + assertEquals(2, counter.get()); + } + + @Test + void testRetryConfigFailedAttempts() { + final AtomicInteger counter = new AtomicInteger(); + final Status expectedFailure = Status.UNAVAILABLE.withDescription("unexpected"); + + this.responseFunction.set((request, observer) -> { + log.info("Failing first request"); + this.responseFunction.set((request2, observer2) -> { + log.info("Failing second request"); + this.responseFunction.set(increment(counter).andThen(respondWith("OK"))); + counter.incrementAndGet(); + errorWith(expectedFailure).accept(request2, observer2); + }); + counter.incrementAndGet(); + errorWith(Status.UNAVAILABLE.withDescription("expected")).accept(request, observer); + }); + + assertThrowsStatus(expectedFailure, () -> this.stub.normal(EMPTY)); + + assertEquals(2, counter.get()); + } + + @Test + void testRetryConfigFailedStatus() { + final AtomicInteger counter = new AtomicInteger(); + final Status expectedFailure = Status.UNAUTHENTICATED.withDescription("unexpected"); + + this.responseFunction.set((request, observer) -> { + log.info("Failing request"); + counter.incrementAndGet(); + errorWith(expectedFailure).accept(request, observer); + }); + + assertThrowsStatus(expectedFailure, () -> this.stub.normal(EMPTY)); + + assertEquals(1, counter.get()); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/util/GrpcAssertions.java b/tests/src/test/java/net/devh/boot/grpc/test/util/GrpcAssertions.java index c5fce8885..9bb623492 100644 --- a/tests/src/test/java/net/devh/boot/grpc/test/util/GrpcAssertions.java +++ b/tests/src/test/java/net/devh/boot/grpc/test/util/GrpcAssertions.java @@ -19,7 +19,7 @@ import static net.devh.boot.grpc.test.util.FutureAssertions.assertFutureEquals; import static net.devh.boot.grpc.test.util.FutureAssertions.assertFutureThrows; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.concurrent.ExecutionException; @@ -70,6 +70,20 @@ public static void assertFutureFirstEquals(final T expected, final Stream assertFutureEquals(expected, responseObserver.firstValue(), unwrapper, timeout, timeoutUnit); } + /** + * Assert that the given {@link Executable} throws a {@link StatusRuntimeException} with the expected status code. + * + * @param expectedStatus The expected status. + * @param executable The executable to run. + * @return The status contained in the exception. + * @see Assertions#assertThrows(Class, Executable) + */ + public static Status assertThrowsStatus(final Status expectedStatus, final Executable executable) { + final Status actualStatus = assertThrowsStatus(expectedStatus.getCode(), executable); + assertEquals(expectedStatus.getDescription(), actualStatus.getDescription(), "Status description"); + return actualStatus; + } + /** * Assert that the given {@link Executable} throws a {@link StatusRuntimeException} with the expected status code. * @@ -125,7 +139,7 @@ public static Status assertFutureThrowsStatus(final Status.Code expectedCode, fi */ public static Status assertStatus(final Status.Code expectedCode, final StatusRuntimeException exception) { final Status status = exception.getStatus(); - assertEquals(expectedCode, status.getCode()); + assertEquals(expectedCode, status.getCode(), "Status code"); return status; } From e3ffb212a8e2bf97d13cbd61d54ca8e819aec8fb Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Fri, 3 Sep 2021 00:19:01 +0200 Subject: [PATCH 6/6] Add missing license headers --- .../test/config/DynamicTestServiceConfig.java | 16 ++++++++++++++++ .../grpc/test/setup/RetryServerClientTest.java | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/tests/src/test/java/net/devh/boot/grpc/test/config/DynamicTestServiceConfig.java b/tests/src/test/java/net/devh/boot/grpc/test/config/DynamicTestServiceConfig.java index 08133a61f..421fe3a9c 100644 --- a/tests/src/test/java/net/devh/boot/grpc/test/config/DynamicTestServiceConfig.java +++ b/tests/src/test/java/net/devh/boot/grpc/test/config/DynamicTestServiceConfig.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2016-2021 Michael Zhang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ package net.devh.boot.grpc.test.config; diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/RetryServerClientTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/RetryServerClientTest.java index 54a6c6063..ca5d7379a 100644 --- a/tests/src/test/java/net/devh/boot/grpc/test/setup/RetryServerClientTest.java +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/RetryServerClientTest.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2016-2021 Michael Zhang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ package net.devh.boot.grpc.test.setup;