diff --git a/CHANGELOG.md b/CHANGELOG.md index af0b0ee1e..8270665ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,3 +7,4 @@ - [Optimize: Maybe remove Chinese characters](https://github.com/Tencent/spring-cloud-tencent/pull/609) - [Bugfix: fix feign report call result error when using feign direct call](https://github.com/Tencent/spring-cloud-tencent/pull/623) - [optimize:optimize PolarisRouterContext and constants.](https://github.com/Tencent/spring-cloud-tencent/pull/628) +- [support spring-retry router](https://github.com/Tencent/spring-cloud-tencent/pull/630) diff --git a/spring-cloud-starter-tencent-polaris-router/pom.xml b/spring-cloud-starter-tencent-polaris-router/pom.xml index 104dd433e..0b3fed451 100644 --- a/spring-cloud-starter-tencent-polaris-router/pom.xml +++ b/spring-cloud-starter-tencent-polaris-router/pom.xml @@ -62,6 +62,12 @@ true + + org.springframework.retry + spring-retry + true + + org.springframework.cloud spring-cloud-netflix-zuul diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/PolarisLoadBalancerCompositeRule.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/PolarisLoadBalancerCompositeRule.java index eeb84afd3..7581ca469 100644 --- a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/PolarisLoadBalancerCompositeRule.java +++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/PolarisLoadBalancerCompositeRule.java @@ -32,8 +32,10 @@ import com.netflix.loadbalancer.RoundRobinRule; import com.netflix.loadbalancer.Server; import com.netflix.loadbalancer.WeightedResponseTimeRule; import com.netflix.loadbalancer.ZoneAvoidanceRule; +import com.tencent.cloud.common.constant.RouterConstant; import com.tencent.cloud.common.metadata.MetadataContext; import com.tencent.cloud.common.pojo.PolarisServer; +import com.tencent.cloud.common.util.JacksonUtils; import com.tencent.cloud.polaris.loadbalancer.LoadBalancerUtils; import com.tencent.cloud.polaris.loadbalancer.PolarisWeightedRule; import com.tencent.cloud.polaris.loadbalancer.config.PolarisLoadBalancerProperties; @@ -45,19 +47,20 @@ import com.tencent.polaris.api.pojo.ServiceInstances; import com.tencent.polaris.router.api.core.RouterAPI; import com.tencent.polaris.router.api.rpc.ProcessRoutersRequest; import com.tencent.polaris.router.api.rpc.ProcessRoutersResponse; +import org.yaml.snakeyaml.util.UriEncoder; +import org.springframework.http.HttpRequest; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** - * * Service routing entrance. - * + *

* Rule routing needs to rely on request parameters for server filtering, * and {@link com.netflix.loadbalancer.ServerListFilter#getFilteredListOfServers(List)} * The interface cannot obtain the context object of the request granularity, * so the routing capability cannot be achieved through ServerListFilter. - * + *

* And {@link com.netflix.loadbalancer.IRule#choose(Object)} provides the ability to pass in context parameters, * so routing capabilities are implemented through IRule. * @@ -117,6 +120,7 @@ public class PolarisLoadBalancerCompositeRule extends AbstractLoadBalancerRule { ILoadBalancer loadBalancer = new SimpleLoadBalancer(); // 2. filter by router if (key instanceof PolarisRouterContext) { + // router implement for Feign and scg PolarisRouterContext routerContext = (PolarisRouterContext) key; List serversAfterRouter = doRouter(allServers, routerContext); // 3. filter by load balance. @@ -124,9 +128,26 @@ public class PolarisLoadBalancerCompositeRule extends AbstractLoadBalancerRule { // because the list of servers may be different after filtered by router loadBalancer.addServers(serversAfterRouter); } + else if (key instanceof HttpRequest) { + // router implement for rest template + HttpRequest request = (HttpRequest) key; + + String routerContextStr = request.getHeaders().getFirst(RouterConstant.HEADER_ROUTER_CONTEXT); + + if (StringUtils.isEmpty(routerContextStr)) { + loadBalancer.addServers(allServers); + } + else { + PolarisRouterContext routerContext = JacksonUtils.deserialize(UriEncoder.decode(routerContextStr), + PolarisRouterContext.class); + List serversAfterRouter = doRouter(allServers, routerContext); + loadBalancer.addServers(serversAfterRouter); + } + } else { loadBalancer.addServers(allServers); } + delegateRule.setLoadBalancer(loadBalancer); return delegateRule.choose(key); diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/PolarisRouterContext.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/PolarisRouterContext.java index 38cda88ad..6d380f5bb 100644 --- a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/PolarisRouterContext.java +++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/PolarisRouterContext.java @@ -102,4 +102,12 @@ public class PolarisRouterContext { } labels.put(labelType, subLabels); } + + public Map> getLabels() { + return labels; + } + + public void setLabels(Map> labels) { + this.labels = labels; + } } diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/beanprocessor/LoadBalancerInterceptorBeanPostProcessor.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/beanprocessor/LoadBalancerInterceptorBeanPostProcessor.java index a65f6f06f..9912d1731 100644 --- a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/beanprocessor/LoadBalancerInterceptorBeanPostProcessor.java +++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/beanprocessor/LoadBalancerInterceptorBeanPostProcessor.java @@ -18,13 +18,8 @@ package com.tencent.cloud.polaris.router.beanprocessor; -import java.util.List; - -import com.tencent.cloud.common.metadata.StaticMetadataManager; -import com.tencent.cloud.common.util.BeanFactoryUtils; -import com.tencent.cloud.polaris.router.RouterRuleLabelResolver; import com.tencent.cloud.polaris.router.resttemplate.PolarisLoadBalancerInterceptor; -import com.tencent.cloud.polaris.router.spi.SpringWebRouterLabelResolver; +import com.tencent.cloud.polaris.router.resttemplate.RouterContextFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; @@ -36,7 +31,7 @@ import org.springframework.cloud.client.loadbalancer.LoadBalancerRequestFactory; import org.springframework.lang.NonNull; /** - * Replace LoadBalancerInterceptor with PolarisLoadBalancerInterceptor. + * Replace {@link LoadBalancerInterceptor} with {@link PolarisLoadBalancerInterceptor}. * PolarisLoadBalancerInterceptor can pass routing context information. * * @author lepdou 2022-05-18 @@ -57,12 +52,9 @@ public class LoadBalancerInterceptorBeanPostProcessor implements BeanPostProcess // Replaces the default LoadBalancerInterceptor implementation and returns a custom PolarisLoadBalancerInterceptor LoadBalancerRequestFactory requestFactory = this.factory.getBean(LoadBalancerRequestFactory.class); LoadBalancerClient loadBalancerClient = this.factory.getBean(LoadBalancerClient.class); - List routerLabelResolvers = BeanFactoryUtils.getBeans(factory, SpringWebRouterLabelResolver.class); - StaticMetadataManager staticMetadataManager = this.factory.getBean(StaticMetadataManager.class); - RouterRuleLabelResolver routerRuleLabelResolver = this.factory.getBean(RouterRuleLabelResolver.class); + RouterContextFactory routerContextFactory = this.factory.getBean(RouterContextFactory.class); - return new PolarisLoadBalancerInterceptor(loadBalancerClient, requestFactory, - routerLabelResolvers, staticMetadataManager, routerRuleLabelResolver); + return new PolarisLoadBalancerInterceptor(loadBalancerClient, requestFactory, routerContextFactory); } return bean; } diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/beanprocessor/RetryLoadBalancerInterceptorBeanPostProcessor.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/beanprocessor/RetryLoadBalancerInterceptorBeanPostProcessor.java new file mode 100644 index 000000000..6325b874a --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/beanprocessor/RetryLoadBalancerInterceptorBeanPostProcessor.java @@ -0,0 +1,68 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.polaris.router.beanprocessor; + +import com.tencent.cloud.polaris.router.resttemplate.PolarisRetryLoadBalancerInterceptor; +import com.tencent.cloud.polaris.router.resttemplate.RouterContextFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerRequestFactory; +import org.springframework.cloud.client.loadbalancer.LoadBalancerRetryProperties; +import org.springframework.cloud.client.loadbalancer.RetryLoadBalancerInterceptor; +import org.springframework.lang.NonNull; + +/** + * Replace {@link RetryLoadBalancerInterceptor}RetryLoadBalancerInterceptor with {@link PolarisRetryLoadBalancerInterceptor}. + * PolarisRetryLoadBalancerInterceptor can pass routing context information. + * + * @author lepdou 2022-10-09 + */ +public class RetryLoadBalancerInterceptorBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware { + + private BeanFactory factory; + + @Override + public void setBeanFactory(@NonNull BeanFactory beanFactory) throws BeansException { + this.factory = beanFactory; + } + + @Override + public Object postProcessBeforeInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException { + if (bean instanceof RetryLoadBalancerInterceptor) { + // Support rest template router. + // Replaces the default RetryLoadBalancerInterceptor implementation + // and returns a custom PolarisRetryLoadBalancerInterceptor + + LoadBalancerClient loadBalancerClient = this.factory.getBean(LoadBalancerClient.class); + LoadBalancerRetryProperties lbProperties = this.factory.getBean(LoadBalancerRetryProperties.class); + LoadBalancerRequestFactory requestFactory = this.factory.getBean(LoadBalancerRequestFactory.class); + LoadBalancedRetryFactory lbRetryFactory = this.factory.getBean(LoadBalancedRetryFactory.class); + RouterContextFactory routerContextFactory = this.factory.getBean(RouterContextFactory.class); + + return new PolarisRetryLoadBalancerInterceptor(loadBalancerClient, lbProperties, requestFactory, lbRetryFactory, + routerContextFactory); + } + return bean; + } +} diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/RouterAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/RouterAutoConfiguration.java index 551c3b956..ed9766109 100644 --- a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/RouterAutoConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/RouterAutoConfiguration.java @@ -25,13 +25,16 @@ import com.tencent.cloud.polaris.context.ServiceRuleManager; import com.tencent.cloud.polaris.router.RouterRuleLabelResolver; import com.tencent.cloud.polaris.router.beanprocessor.LoadBalancerClientFilterBeanPostProcessor; import com.tencent.cloud.polaris.router.beanprocessor.LoadBalancerInterceptorBeanPostProcessor; +import com.tencent.cloud.polaris.router.beanprocessor.RetryLoadBalancerInterceptorBeanPostProcessor; import com.tencent.cloud.polaris.router.config.properties.PolarisMetadataRouterProperties; import com.tencent.cloud.polaris.router.config.properties.PolarisNearByRouterProperties; import com.tencent.cloud.polaris.router.config.properties.PolarisRuleBasedRouterProperties; import com.tencent.cloud.polaris.router.interceptor.MetadataRouterRequestInterceptor; import com.tencent.cloud.polaris.router.interceptor.NearbyRouterRequestInterceptor; import com.tencent.cloud.polaris.router.interceptor.RuleBasedRouterRequestInterceptor; +import com.tencent.cloud.polaris.router.resttemplate.RouterContextFactory; import com.tencent.cloud.polaris.router.spi.ServletRouterLabelResolver; +import com.tencent.cloud.polaris.router.spi.SpringWebRouterLabelResolver; import com.tencent.cloud.polaris.router.zuul.PolarisRibbonRoutingFilter; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -59,13 +62,6 @@ import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE; @Import({PolarisNearByRouterProperties.class, PolarisMetadataRouterProperties.class, PolarisRuleBasedRouterProperties.class}) public class RouterAutoConfiguration { - @Bean - @Order(HIGHEST_PRECEDENCE) - @ConditionalOnClass(name = "org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor") - public LoadBalancerInterceptorBeanPostProcessor loadBalancerInterceptorBeanPostProcessor() { - return new LoadBalancerInterceptorBeanPostProcessor(); - } - @Bean @Order(HIGHEST_PRECEDENCE) @ConditionalOnClass(name = "org.springframework.cloud.gateway.filter.LoadBalancerClientFilter") @@ -96,6 +92,32 @@ public class RouterAutoConfiguration { return new RuleBasedRouterRequestInterceptor(polarisRuleBasedRouterProperties); } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = "org.springframework.web.client.RestTemplate") + public static class RestTemplateAutoConfiguration { + @Bean + @Order(HIGHEST_PRECEDENCE) + @ConditionalOnClass(name = "org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor") + public LoadBalancerInterceptorBeanPostProcessor loadBalancerInterceptorBeanPostProcessor() { + return new LoadBalancerInterceptorBeanPostProcessor(); + } + + @Bean + @Order(HIGHEST_PRECEDENCE) + @ConditionalOnClass(name = "org.springframework.cloud.client.loadbalancer.RetryLoadBalancerInterceptor") + public RetryLoadBalancerInterceptorBeanPostProcessor retryLoadBalancerInterceptorBeanPostProcessor() { + return new RetryLoadBalancerInterceptorBeanPostProcessor(); + } + + @Bean + @Order(HIGHEST_PRECEDENCE) + public RouterContextFactory routerContextFactory(List routerLabelResolvers, + StaticMetadataManager staticMetadataManager, + RouterRuleLabelResolver routerRuleLabelResolver) { + return new RouterContextFactory(routerLabelResolvers, staticMetadataManager, routerRuleLabelResolver); + } + } + /** * AutoConfiguration for router module integrate for zuul. */ diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisInterceptorRetryPolicy.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisInterceptorRetryPolicy.java new file mode 100644 index 000000000..7da66766a --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisInterceptorRetryPolicy.java @@ -0,0 +1,85 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.polaris.router.resttemplate; + +import com.tencent.cloud.polaris.router.PolarisRouterContext; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.InterceptorRetryPolicy; +import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext; +import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy; +import org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser; +import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient; +import org.springframework.http.HttpRequest; +import org.springframework.retry.RetryContext; + +/** + * Override InterceptorRetryPolicy for passing router context. + * + * @author lepdou 2022-10-09 + */ +public class PolarisInterceptorRetryPolicy extends InterceptorRetryPolicy { + + private final HttpRequest request; + + private final LoadBalancedRetryPolicy policy; + + private final ServiceInstanceChooser serviceInstanceChooser; + + private final String serviceName; + + private final PolarisRouterContext routerContext; + + + public PolarisInterceptorRetryPolicy(HttpRequest request, LoadBalancedRetryPolicy policy, + ServiceInstanceChooser serviceInstanceChooser, String serviceName, + PolarisRouterContext routerContext) { + super(request, policy, serviceInstanceChooser, serviceName); + + this.request = request; + this.policy = policy; + this.serviceInstanceChooser = serviceInstanceChooser; + this.serviceName = serviceName; + this.routerContext = routerContext; + } + + @Override + public boolean canRetry(RetryContext context) { + if (serviceInstanceChooser instanceof RibbonLoadBalancerClient) { + LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext) context; + if (lbContext.getRetryCount() == 0 && lbContext.getServiceInstance() == null) { + loadbalancerWithRouterContext(lbContext, routerContext); + return true; + } + return policy.canRetryNextServer(lbContext); + } + else { + return super.canRetry(context); + } + } + + private void loadbalancerWithRouterContext(LoadBalancedRetryContext lbContext, PolarisRouterContext routerContext) { + RibbonLoadBalancerClient ribbonLoadBalancerClient = (RibbonLoadBalancerClient) serviceInstanceChooser; + + // set router context to request + RouterContextHelper.setRouterContextToRequest(request, routerContext); + + ServiceInstance serviceInstance = ribbonLoadBalancerClient.choose(serviceName, request); + lbContext.setServiceInstance(serviceInstance); + } +} diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerInterceptor.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerInterceptor.java index cce678a79..c62569bfa 100644 --- a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerInterceptor.java +++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerInterceptor.java @@ -19,40 +19,18 @@ package com.tencent.cloud.polaris.router.resttemplate; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.URI; -import java.net.URLEncoder; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import com.tencent.cloud.common.constant.RouterConstant; -import com.tencent.cloud.common.metadata.MetadataContext; -import com.tencent.cloud.common.metadata.MetadataContextHolder; -import com.tencent.cloud.common.metadata.StaticMetadataManager; -import com.tencent.cloud.common.util.JacksonUtils; -import com.tencent.cloud.common.util.expresstion.SpringWebExpressionLabelUtils; import com.tencent.cloud.polaris.router.PolarisRouterContext; -import com.tencent.cloud.polaris.router.RouterRuleLabelResolver; -import com.tencent.cloud.polaris.router.spi.SpringWebRouterLabelResolver; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor; import org.springframework.cloud.client.loadbalancer.LoadBalancerRequestFactory; import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient; -import org.springframework.core.Ordered; import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpResponse; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; - -import static com.tencent.cloud.common.constant.ContextConstant.UTF_8; /** * PolarisLoadBalancerInterceptor extends LoadBalancerInterceptor capabilities. @@ -61,34 +39,19 @@ import static com.tencent.cloud.common.constant.ContextConstant.UTF_8; * @author lepdou 2022-05-18 */ public class PolarisLoadBalancerInterceptor extends LoadBalancerInterceptor { - private static final Logger LOGGER = LoggerFactory.getLogger(PolarisLoadBalancerInterceptor.class); private final LoadBalancerClient loadBalancer; private final LoadBalancerRequestFactory requestFactory; - private final List routerLabelResolvers; - private final StaticMetadataManager staticMetadataManager; - private final RouterRuleLabelResolver routerRuleLabelResolver; - + private final RouterContextFactory routerContextFactory; private final boolean isRibbonLoadBalanceClient; public PolarisLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory, - List routerLabelResolvers, - StaticMetadataManager staticMetadataManager, - RouterRuleLabelResolver routerRuleLabelResolver) { + RouterContextFactory routerContextFactory) { super(loadBalancer, requestFactory); this.loadBalancer = loadBalancer; this.requestFactory = requestFactory; - this.staticMetadataManager = staticMetadataManager; - this.routerRuleLabelResolver = routerRuleLabelResolver; - - if (!CollectionUtils.isEmpty(routerLabelResolvers)) { - routerLabelResolvers.sort(Comparator.comparingInt(Ordered::getOrder)); - this.routerLabelResolvers = routerLabelResolvers; - } - else { - this.routerLabelResolvers = null; - } + this.routerContextFactory = routerContextFactory; this.isRibbonLoadBalanceClient = loadBalancer instanceof RibbonLoadBalancerClient; } @@ -101,72 +64,21 @@ public class PolarisLoadBalancerInterceptor extends LoadBalancerInterceptor { "Request URI does not contain a valid hostname: " + originalUri); if (isRibbonLoadBalanceClient) { - PolarisRouterContext routerContext = genRouterContext(request, body, peerServiceName); + //1. create router context + PolarisRouterContext routerContext = routerContextFactory.create(request, body, peerServiceName); + + //2. set router context to request + RouterContextHelper.setRouterContextToRequest(request, routerContext); + + //3. do loadbalancer and execute request ClientHttpResponse response = ((RibbonLoadBalancerClient) loadBalancer).execute(peerServiceName, this.requestFactory.createRequest(request, body, execution), routerContext); - Map labels = routerContext.getLabels(RouterConstant.ROUTER_LABELS); - // put labels in header - String encodedLabelsContent; - try { - encodedLabelsContent = URLEncoder.encode(JacksonUtils.serialize2Json(labels), UTF_8); - } - catch (UnsupportedEncodingException e) { - throw new RuntimeException("unsupported charset exception " + UTF_8); - } - response.getHeaders().add(RouterConstant.ROUTER_LABELS, encodedLabelsContent); + //4. set router context to response + RouterContextHelper.setRouterContextToResponse(routerContext, response); return response; } - return this.loadBalancer.execute(peerServiceName, - this.requestFactory.createRequest(request, body, execution)); - } - - PolarisRouterContext genRouterContext(HttpRequest request, byte[] body, String peerServiceName) { - // local service labels - Map labels = new HashMap<>(staticMetadataManager.getMergedStaticMetadata()); - - // labels from rule expression - Set expressionLabelKeys = routerRuleLabelResolver.getExpressionLabelKeys(MetadataContext.LOCAL_NAMESPACE, - MetadataContext.LOCAL_SERVICE, peerServiceName); - Map ruleExpressionLabels = getExpressionLabels(request, expressionLabelKeys); - if (!CollectionUtils.isEmpty(ruleExpressionLabels)) { - labels.putAll(ruleExpressionLabels); - } - - // labels from request - if (!CollectionUtils.isEmpty(routerLabelResolvers)) { - routerLabelResolvers.forEach(resolver -> { - try { - Map customResolvedLabels = resolver.resolve(request, body, expressionLabelKeys); - if (!CollectionUtils.isEmpty(customResolvedLabels)) { - labels.putAll(customResolvedLabels); - } - } - catch (Throwable t) { - LOGGER.error("[SCT][Router] revoke RouterLabelResolver occur some exception. ", t); - } - }); - } - - // labels from downstream - Map transitiveLabels = MetadataContextHolder.get() - .getFragmentContext(MetadataContext.FRAGMENT_TRANSITIVE); - labels.putAll(transitiveLabels); - - PolarisRouterContext routerContext = new PolarisRouterContext(); - - routerContext.putLabels(RouterConstant.ROUTER_LABELS, labels); - routerContext.putLabels(RouterConstant.TRANSITIVE_LABELS, transitiveLabels); - - return routerContext; - } - - private Map getExpressionLabels(HttpRequest request, Set labelKeys) { - if (CollectionUtils.isEmpty(labelKeys)) { - return Collections.emptyMap(); - } - - return SpringWebExpressionLabelUtils.resolve(request, labelKeys); + return this.loadBalancer.execute(peerServiceName, this.requestFactory.createRequest(request, body, execution)); } } diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisRetryLoadBalancerInterceptor.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisRetryLoadBalancerInterceptor.java new file mode 100644 index 000000000..6b19966f3 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/PolarisRetryLoadBalancerInterceptor.java @@ -0,0 +1,158 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.polaris.router.resttemplate; + +import java.io.IOException; +import java.net.URI; + +import com.tencent.cloud.polaris.router.PolarisRouterContext; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.ClientHttpResponseStatusCodeException; +import org.springframework.cloud.client.loadbalancer.LoadBalancedRecoveryCallback; +import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext; +import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; +import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerRequestFactory; +import org.springframework.cloud.client.loadbalancer.LoadBalancerRetryProperties; +import org.springframework.cloud.client.loadbalancer.RetryLoadBalancerInterceptor; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.retry.RetryListener; +import org.springframework.retry.backoff.BackOffPolicy; +import org.springframework.retry.backoff.NoBackOffPolicy; +import org.springframework.retry.policy.NeverRetryPolicy; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; + +/** + * Override {@link RetryLoadBalancerInterceptor} for passing router context. + * + * @author lepdou 2022-10-09 + */ +public class PolarisRetryLoadBalancerInterceptor extends RetryLoadBalancerInterceptor { + + private static final Log LOG = LogFactory.getLog(PolarisRetryLoadBalancerInterceptor.class); + + private final LoadBalancerClient loadBalancer; + + private final LoadBalancerRetryProperties lbProperties; + + private final LoadBalancerRequestFactory requestFactory; + + private final LoadBalancedRetryFactory lbRetryFactory; + + private final RouterContextFactory routerContextFactory; + + public PolarisRetryLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRetryProperties lbProperties, + LoadBalancerRequestFactory requestFactory, LoadBalancedRetryFactory lbRetryFactory, RouterContextFactory routerContextFactory) { + super(loadBalancer, lbProperties, requestFactory, lbRetryFactory); + this.loadBalancer = loadBalancer; + this.lbProperties = lbProperties; + this.requestFactory = requestFactory; + this.lbRetryFactory = lbRetryFactory; + this.routerContextFactory = routerContextFactory; + } + + @Override + public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, + final ClientHttpRequestExecution execution) throws IOException { + + final URI originalUri = request.getURI(); + final String serviceName = originalUri.getHost(); + Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri); + + final LoadBalancedRetryPolicy retryPolicy = lbRetryFactory.createRetryPolicy(serviceName, loadBalancer); + + //1. create router context + PolarisRouterContext routerContext = routerContextFactory.create(request, body, serviceName); + + RetryTemplate template = createRetryTemplate(serviceName, request, routerContext, retryPolicy); + + //2. do loadbalancer is template.execute + return template.execute(context -> { + ServiceInstance serviceInstance = null; if (context instanceof LoadBalancedRetryContext) { + LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext) context; + serviceInstance = lbContext.getServiceInstance(); + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Retrieved service instance from LoadBalancedRetryContext: %s", serviceInstance)); + } + } + + //retry loadbalancer if LoadBalancedRetryContext does not has instance + if (serviceInstance == null) { + if (LOG.isDebugEnabled()) { + LOG.debug("Service instance retrieved from LoadBalancedRetryContext: was null. " + "Reattempting service instance selection"); + } + serviceInstance = loadBalancer.choose(serviceName); if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Selected service instance: %s", serviceInstance)); + } + } + + //3. execute request + ClientHttpResponse response = loadBalancer.execute(serviceName, serviceInstance, + requestFactory.createRequest(request, body, execution)); + + //4. set router context to response + RouterContextHelper.setRouterContextToResponse(routerContext, response); + + //5. handle response whether retry execute request + int statusCode = response.getRawStatusCode(); + if (retryPolicy != null && retryPolicy.retryableStatusCode(statusCode)) { + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Retrying on status code: %d", statusCode)); + } + byte[] bodyCopy = StreamUtils.copyToByteArray(response.getBody()); + response.close(); + throw new ClientHttpResponseStatusCodeException(serviceName, response, bodyCopy); + } + return response; + }, new LoadBalancedRecoveryCallback() { + // This is a special case, where both parameters to + // LoadBalancedRecoveryCallback are + // the same. In most cases they would be different. + @Override + protected ClientHttpResponse createResponse(ClientHttpResponse response, URI uri) { + return response; + } + }); + } + + private RetryTemplate createRetryTemplate(String serviceName, HttpRequest request, PolarisRouterContext routerContext, + LoadBalancedRetryPolicy retryPolicy) { + RetryTemplate template = new RetryTemplate(); + BackOffPolicy backOffPolicy = lbRetryFactory.createBackOffPolicy(serviceName); + template.setBackOffPolicy(backOffPolicy == null ? new NoBackOffPolicy() : backOffPolicy); + template.setThrowLastExceptionOnExhausted(true); + RetryListener[] retryListeners = lbRetryFactory.createRetryListeners(serviceName); + if (retryListeners != null && retryListeners.length != 0) { + template.setListeners(retryListeners); + } + //Override: use PolarisInterceptorRetryPolicy for passing router context + template.setRetryPolicy(!lbProperties.isEnabled() || retryPolicy == null ? new NeverRetryPolicy() : + new PolarisInterceptorRetryPolicy(request, retryPolicy, loadBalancer, serviceName, routerContext)); + return template; + } + +} diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/RouterContextFactory.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/RouterContextFactory.java new file mode 100644 index 000000000..76aa4c407 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/RouterContextFactory.java @@ -0,0 +1,117 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.polaris.router.resttemplate; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.tencent.cloud.common.constant.RouterConstant; +import com.tencent.cloud.common.metadata.MetadataContext; +import com.tencent.cloud.common.metadata.MetadataContextHolder; +import com.tencent.cloud.common.metadata.StaticMetadataManager; +import com.tencent.cloud.common.util.expresstion.SpringWebExpressionLabelUtils; +import com.tencent.cloud.polaris.router.PolarisRouterContext; +import com.tencent.cloud.polaris.router.RouterRuleLabelResolver; +import com.tencent.cloud.polaris.router.spi.SpringWebRouterLabelResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.core.Ordered; +import org.springframework.http.HttpRequest; +import org.springframework.util.CollectionUtils; + +/** + * Polaris router context factory. + * + * @author lepdou 2022-10-09 + */ +public class RouterContextFactory { + private static final Logger LOGGER = LoggerFactory.getLogger(RouterContextFactory.class); + + private final List routerLabelResolvers; + private final StaticMetadataManager staticMetadataManager; + private final RouterRuleLabelResolver routerRuleLabelResolver; + + public RouterContextFactory(List routerLabelResolvers, + StaticMetadataManager staticMetadataManager, + RouterRuleLabelResolver routerRuleLabelResolver) { + this.staticMetadataManager = staticMetadataManager; + this.routerRuleLabelResolver = routerRuleLabelResolver; + + if (!CollectionUtils.isEmpty(routerLabelResolvers)) { + routerLabelResolvers.sort(Comparator.comparingInt(Ordered::getOrder)); + this.routerLabelResolvers = routerLabelResolvers; + } + else { + this.routerLabelResolvers = null; + } + } + + + public PolarisRouterContext create(HttpRequest request, byte[] body, String peerServiceName) { + // local service labels + Map labels = new HashMap<>(staticMetadataManager.getMergedStaticMetadata()); + + // labels from rule expression + Set expressionLabelKeys = routerRuleLabelResolver.getExpressionLabelKeys(MetadataContext.LOCAL_NAMESPACE, + MetadataContext.LOCAL_SERVICE, peerServiceName); + Map ruleExpressionLabels = getExpressionLabels(request, expressionLabelKeys); + if (!CollectionUtils.isEmpty(ruleExpressionLabels)) { + labels.putAll(ruleExpressionLabels); + } + + // labels from request + if (!CollectionUtils.isEmpty(routerLabelResolvers)) { + routerLabelResolvers.forEach(resolver -> { + try { + Map customResolvedLabels = resolver.resolve(request, body, expressionLabelKeys); + if (!CollectionUtils.isEmpty(customResolvedLabels)) { + labels.putAll(customResolvedLabels); + } + } + catch (Throwable t) { + LOGGER.error("[SCT][Router] revoke RouterLabelResolver occur some exception. ", t); + } + }); + } + + // labels from downstream + Map transitiveLabels = MetadataContextHolder.get() + .getFragmentContext(MetadataContext.FRAGMENT_TRANSITIVE); + labels.putAll(transitiveLabels); + + PolarisRouterContext routerContext = new PolarisRouterContext(); + + routerContext.putLabels(RouterConstant.ROUTER_LABELS, labels); + routerContext.putLabels(RouterConstant.TRANSITIVE_LABELS, transitiveLabels); + + return routerContext; + } + + private Map getExpressionLabels(HttpRequest request, Set labelKeys) { + if (CollectionUtils.isEmpty(labelKeys)) { + return Collections.emptyMap(); + } + + return SpringWebExpressionLabelUtils.resolve(request, labelKeys); + } +} diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/RouterContextHelper.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/RouterContextHelper.java new file mode 100644 index 000000000..7a0f9eb8f --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/RouterContextHelper.java @@ -0,0 +1,68 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.polaris.router.resttemplate; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Map; + +import com.tencent.cloud.common.constant.RouterConstant; +import com.tencent.cloud.common.util.JacksonUtils; +import com.tencent.cloud.polaris.router.PolarisRouterContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpResponse; + +import static com.tencent.cloud.common.constant.ContextConstant.UTF_8; + +/** + * Set router context to request and response. + * + * @author lepdou 2022-10-09 + */ +public final class RouterContextHelper { + private static final Logger LOGGER = LoggerFactory.getLogger(RouterContextHelper.class); + + private RouterContextHelper() { + + } + + public static void setRouterContextToRequest(HttpRequest request, PolarisRouterContext routerContext) { + try { + request.getHeaders().add(RouterConstant.HEADER_ROUTER_CONTEXT, + URLEncoder.encode(JacksonUtils.serialize2Json(routerContext), UTF_8)); + } + catch (Exception e) { + LOGGER.error("[SCT] serialize router context error.", e); + } + } + + public static void setRouterContextToResponse(PolarisRouterContext routerContext, ClientHttpResponse response) { + Map labels = routerContext.getLabels(RouterConstant.ROUTER_LABELS); + + try { + response.getHeaders().add(RouterConstant.ROUTER_LABELS, + URLEncoder.encode(JacksonUtils.serialize2Json(labels), UTF_8)); + } + catch (UnsupportedEncodingException e) { + LOGGER.error("[SCT] add router label to response header error.", e); + } + } +} diff --git a/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerBeanPostProcessorTest.java b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerBeanPostProcessorTest.java index afbb71883..9efdbb6b3 100644 --- a/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerBeanPostProcessorTest.java +++ b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerBeanPostProcessorTest.java @@ -18,9 +18,7 @@ package com.tencent.cloud.polaris.router.resttemplate; -import com.tencent.cloud.common.metadata.StaticMetadataManager; import com.tencent.cloud.common.util.BeanFactoryUtils; -import com.tencent.cloud.polaris.router.RouterRuleLabelResolver; import com.tencent.cloud.polaris.router.beanprocessor.LoadBalancerInterceptorBeanPostProcessor; import com.tencent.cloud.polaris.router.spi.SpringWebRouterLabelResolver; import org.junit.Assert; @@ -51,18 +49,12 @@ public class PolarisLoadBalancerBeanPostProcessorTest { @Mock private LoadBalancerRequestFactory loadBalancerRequestFactory; @Mock - private StaticMetadataManager staticMetadataManager; - @Mock - private RouterRuleLabelResolver routerRuleLabelResolver; - @Mock private BeanFactory beanFactory; @Test public void testWrapperLoadBalancerInterceptor() { when(beanFactory.getBean(LoadBalancerRequestFactory.class)).thenReturn(loadBalancerRequestFactory); when(beanFactory.getBean(LoadBalancerClient.class)).thenReturn(loadBalancerClient); - when(beanFactory.getBean(StaticMetadataManager.class)).thenReturn(staticMetadataManager); - when(beanFactory.getBean(RouterRuleLabelResolver.class)).thenReturn(routerRuleLabelResolver); try (MockedStatic mockedBeanFactoryUtils = Mockito.mockStatic(BeanFactoryUtils.class)) { mockedBeanFactoryUtils.when(() -> BeanFactoryUtils.getBeans(beanFactory, SpringWebRouterLabelResolver.class)) diff --git a/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerInterceptorTest.java b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerInterceptorTest.java index 81984fe71..d853a0c5d 100644 --- a/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerInterceptorTest.java +++ b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/resttemplate/PolarisLoadBalancerInterceptorTest.java @@ -22,22 +22,15 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLEncoder; -import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import com.tencent.cloud.common.constant.RouterConstant; import com.tencent.cloud.common.metadata.MetadataContext; import com.tencent.cloud.common.metadata.MetadataContextHolder; -import com.tencent.cloud.common.metadata.StaticMetadataManager; import com.tencent.cloud.common.util.ApplicationContextAwareUtils; import com.tencent.cloud.common.util.JacksonUtils; -import com.tencent.cloud.common.util.expresstion.SpringWebExpressionLabelUtils; import com.tencent.cloud.polaris.router.PolarisRouterContext; -import com.tencent.cloud.polaris.router.RouterRuleLabelResolver; -import com.tencent.cloud.polaris.router.spi.SpringWebRouterLabelResolver; import org.assertj.core.api.Assertions; import org.junit.Assert; import org.junit.Test; @@ -79,11 +72,7 @@ public class PolarisLoadBalancerInterceptorTest { @Mock private LoadBalancerRequestFactory loadBalancerRequestFactory; @Mock - private SpringWebRouterLabelResolver routerLabelResolver; - @Mock - private StaticMetadataManager staticMetadataManager; - @Mock - private RouterRuleLabelResolver routerRuleLabelResolver; + private RouterContextFactory routerContextFactory; @Test public void testProxyRibbonLoadBalance() throws Exception { @@ -91,28 +80,13 @@ public class PolarisLoadBalancerInterceptorTest { String calleeService = "calleeService"; HttpRequest request = new MockedHttpRequest("http://" + calleeService + "/user/get"); + PolarisRouterContext routerContext = new PolarisRouterContext(); Map routerLabels = new HashMap<>(); - // mock local metadata - Map localMetadata = new HashMap<>(); - localMetadata.put("k1", "v1"); - localMetadata.put("k2", "v2"); - when(staticMetadataManager.getMergedStaticMetadata()).thenReturn(localMetadata); - routerLabels.putAll(localMetadata); - - // mock expression rule labels - Set expressionKeys = new HashSet<>(); - expressionKeys.add("${http.method}"); - expressionKeys.add("${http.uri}"); - when(routerRuleLabelResolver.getExpressionLabelKeys(callerService, callerService, calleeService)).thenReturn(expressionKeys); - routerLabels.putAll(SpringWebExpressionLabelUtils.resolve(request, expressionKeys)); - - // mock custom resolved from request - Map customResolvedLabels = new HashMap<>(); - customResolvedLabels.put("k3", "v3"); - customResolvedLabels.put("k4", "v4"); - when(routerLabelResolver.resolve(request, null, expressionKeys)).thenReturn(customResolvedLabels); - routerLabels.putAll(customResolvedLabels); + routerLabels.put("k1", "v1"); + routerLabels.put("k2", "v12"); + routerContext.putLabels(RouterConstant.ROUTER_LABELS, routerLabels); + when(routerContextFactory.create(request, null, calleeService)).thenReturn(routerContext); try (MockedStatic mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class)) { mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString())) @@ -120,30 +94,21 @@ public class PolarisLoadBalancerInterceptorTest { MetadataContext metadataContext = Mockito.mock(MetadataContext.class); - // mock transitive metadata - Map transitiveLabels = new HashMap<>(); - transitiveLabels.put("k1", "v1"); - transitiveLabels.put("k2", "v22"); - when(metadataContext.getFragmentContext(MetadataContext.FRAGMENT_TRANSITIVE)).thenReturn(transitiveLabels); - routerLabels.putAll(transitiveLabels); - try (MockedStatic mockedMetadataContextHolder = Mockito.mockStatic(MetadataContextHolder.class)) { mockedMetadataContextHolder.when(MetadataContextHolder::get).thenReturn(metadataContext); + LoadBalancerRequest loadBalancerRequest = new MockedLoadBalancerRequest<>(); when(loadBalancerRequestFactory.createRequest(request, null, null)).thenReturn(loadBalancerRequest); PolarisLoadBalancerInterceptor polarisLoadBalancerInterceptor = new PolarisLoadBalancerInterceptor(loadBalancerClient, - loadBalancerRequestFactory, Collections.singletonList(routerLabelResolver), staticMetadataManager, routerRuleLabelResolver); + loadBalancerRequestFactory, routerContextFactory); ClientHttpResponse mockedResponse = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK); when(loadBalancerClient.execute(eq(calleeService), eq(loadBalancerRequest), any(PolarisRouterContext.class))).thenReturn(mockedResponse); polarisLoadBalancerInterceptor.intercept(request, null, null); - verify(staticMetadataManager).getMergedStaticMetadata(); - verify(routerRuleLabelResolver).getExpressionLabelKeys(callerService, callerService, calleeService); - verify(routerLabelResolver).resolve(request, null, expressionKeys); String encodedLabelsContent; try { encodedLabelsContent = URLEncoder.encode(JacksonUtils.serialize2Json(routerLabels), UTF_8); @@ -170,9 +135,7 @@ public class PolarisLoadBalancerInterceptorTest { when(notRibbonLoadBalancerClient.execute(calleeService, loadBalancerRequest)).thenReturn(mockedResponse); PolarisLoadBalancerInterceptor polarisLoadBalancerInterceptor = new PolarisLoadBalancerInterceptor( - notRibbonLoadBalancerClient, loadBalancerRequestFactory, - Collections.singletonList(routerLabelResolver), staticMetadataManager, - routerRuleLabelResolver); + notRibbonLoadBalancerClient, loadBalancerRequestFactory, routerContextFactory); ClientHttpResponse response = polarisLoadBalancerInterceptor.intercept(request, null, null); @@ -182,69 +145,6 @@ public class PolarisLoadBalancerInterceptorTest { } - @Test - public void testRouterContext() { - String callerService = "callerService"; - String calleeService = "calleeService"; - HttpRequest request = new MockedHttpRequest("http://" + calleeService + "/user/get"); - - // mock local metadata - Map localMetadata = new HashMap<>(); - localMetadata.put("k1", "v1"); - localMetadata.put("k2", "v2"); - when(staticMetadataManager.getMergedStaticMetadata()).thenReturn(localMetadata); - - // mock expression rule labels - Set expressionKeys = new HashSet<>(); - expressionKeys.add("${http.method}"); - expressionKeys.add("${http.uri}"); - when(routerRuleLabelResolver.getExpressionLabelKeys(callerService, callerService, calleeService)).thenReturn(expressionKeys); - - // mock custom resolved from request - Map customResolvedLabels = new HashMap<>(); - customResolvedLabels.put("k2", "v22"); - customResolvedLabels.put("k4", "v4"); - when(routerLabelResolver.resolve(request, null, expressionKeys)).thenReturn(customResolvedLabels); - - try (MockedStatic mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class)) { - mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString())) - .thenReturn(callerService); - - MetadataContext metadataContext = Mockito.mock(MetadataContext.class); - - // mock transitive metadata - Map transitiveLabels = new HashMap<>(); - transitiveLabels.put("k1", "v1"); - transitiveLabels.put("k2", "v22"); - when(metadataContext.getFragmentContext(MetadataContext.FRAGMENT_TRANSITIVE)).thenReturn(transitiveLabels); - - try (MockedStatic mockedMetadataContextHolder = Mockito.mockStatic(MetadataContextHolder.class)) { - mockedMetadataContextHolder.when(MetadataContextHolder::get).thenReturn(metadataContext); - - PolarisLoadBalancerInterceptor polarisLoadBalancerInterceptor = - new PolarisLoadBalancerInterceptor(loadBalancerClient, loadBalancerRequestFactory, - Collections.singletonList(routerLabelResolver), staticMetadataManager, - routerRuleLabelResolver); - - PolarisRouterContext routerContext = polarisLoadBalancerInterceptor.genRouterContext(request, null, calleeService); - - verify(staticMetadataManager).getMergedStaticMetadata(); - verify(routerRuleLabelResolver).getExpressionLabelKeys(callerService, callerService, calleeService); - verify(routerLabelResolver).resolve(request, null, expressionKeys); - - Assert.assertEquals("v1", routerContext.getLabels(RouterConstant.TRANSITIVE_LABELS).get("k1")); - Assert.assertEquals("v22", routerContext.getLabels(RouterConstant.TRANSITIVE_LABELS).get("k2")); - Assert.assertEquals("v1", routerContext.getLabels(RouterConstant.ROUTER_LABELS).get("k1")); - Assert.assertEquals("v22", routerContext.getLabels(RouterConstant.ROUTER_LABELS).get("k2")); - Assert.assertEquals("v4", routerContext.getLabels(RouterConstant.ROUTER_LABELS).get("k4")); - Assert.assertEquals("GET", routerContext.getLabels(RouterConstant.ROUTER_LABELS) - .get("${http.method}")); - Assert.assertEquals("/user/get", routerContext.getLabels(RouterConstant.ROUTER_LABELS) - .get("${http.uri}")); - } - } - } - static class MockedLoadBalancerRequest implements LoadBalancerRequest { @Override @@ -273,7 +173,7 @@ public class PolarisLoadBalancerInterceptorTest { @Override public HttpHeaders getHeaders() { - return null; + return new HttpHeaders(); } } } diff --git a/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/resttemplate/RouterContextFactoryTest.java b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/resttemplate/RouterContextFactoryTest.java new file mode 100644 index 000000000..6152db7ee --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/resttemplate/RouterContextFactoryTest.java @@ -0,0 +1,150 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.polaris.router.resttemplate; + +import java.net.URI; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.tencent.cloud.common.constant.RouterConstant; +import com.tencent.cloud.common.metadata.MetadataContext; +import com.tencent.cloud.common.metadata.MetadataContextHolder; +import com.tencent.cloud.common.metadata.StaticMetadataManager; +import com.tencent.cloud.common.util.ApplicationContextAwareUtils; +import com.tencent.cloud.polaris.router.PolarisRouterContext; +import com.tencent.cloud.polaris.router.RouterRuleLabelResolver; +import com.tencent.cloud.polaris.router.spi.SpringWebRouterLabelResolver; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test for {@link RouterContextFactory} + * + * @author lepdou 2022-10-09 + */ +@RunWith(MockitoJUnitRunner.class) +public class RouterContextFactoryTest { + + @Mock + private SpringWebRouterLabelResolver springWebRouterLabelResolver; + @Mock + private StaticMetadataManager staticMetadataManager; + @Mock + private RouterRuleLabelResolver routerRuleLabelResolver; + + @Test + public void testRouterContext() { + String callerService = "callerService"; + String calleeService = "calleeService"; + HttpRequest request = new MockedHttpRequest("http://" + calleeService + "/user/get"); + + // mock local metadata + Map localMetadata = new HashMap<>(); + localMetadata.put("k1", "v1"); + localMetadata.put("k2", "v2"); + when(staticMetadataManager.getMergedStaticMetadata()).thenReturn(localMetadata); + + // mock expression rule labels + Set expressionKeys = new HashSet<>(); + expressionKeys.add("${http.method}"); + expressionKeys.add("${http.uri}"); + when(routerRuleLabelResolver.getExpressionLabelKeys(callerService, callerService, calleeService)).thenReturn(expressionKeys); + + // mock custom resolved from request + Map customResolvedLabels = new HashMap<>(); + customResolvedLabels.put("k2", "v22"); + customResolvedLabels.put("k4", "v4"); + when(springWebRouterLabelResolver.resolve(request, null, expressionKeys)).thenReturn(customResolvedLabels); + + try (MockedStatic mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class)) { + mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString())) + .thenReturn(callerService); + + MetadataContext metadataContext = Mockito.mock(MetadataContext.class); + + // mock transitive metadata + Map transitiveLabels = new HashMap<>(); + transitiveLabels.put("k1", "v1"); + transitiveLabels.put("k2", "v22"); + when(metadataContext.getFragmentContext(MetadataContext.FRAGMENT_TRANSITIVE)).thenReturn(transitiveLabels); + + try (MockedStatic mockedMetadataContextHolder = Mockito.mockStatic(MetadataContextHolder.class)) { + mockedMetadataContextHolder.when(MetadataContextHolder::get).thenReturn(metadataContext); + + RouterContextFactory routerContextFactory = new RouterContextFactory(Arrays.asList(springWebRouterLabelResolver), + staticMetadataManager, routerRuleLabelResolver); + + PolarisRouterContext routerContext = routerContextFactory.create(request, null, calleeService); + + verify(staticMetadataManager).getMergedStaticMetadata(); + verify(routerRuleLabelResolver).getExpressionLabelKeys(callerService, callerService, calleeService); + verify(springWebRouterLabelResolver).resolve(request, null, expressionKeys); + + Assert.assertEquals("v1", routerContext.getLabels(RouterConstant.TRANSITIVE_LABELS).get("k1")); + Assert.assertEquals("v22", routerContext.getLabels(RouterConstant.TRANSITIVE_LABELS).get("k2")); + Assert.assertEquals("v1", routerContext.getLabels(RouterConstant.ROUTER_LABELS).get("k1")); + Assert.assertEquals("v22", routerContext.getLabels(RouterConstant.ROUTER_LABELS).get("k2")); + Assert.assertEquals("v4", routerContext.getLabels(RouterConstant.ROUTER_LABELS).get("k4")); + Assert.assertEquals("GET", routerContext.getLabels(RouterConstant.ROUTER_LABELS) + .get("${http.method}")); + Assert.assertEquals("/user/get", routerContext.getLabels(RouterConstant.ROUTER_LABELS) + .get("${http.uri}")); + } + } + } + + static class MockedHttpRequest implements HttpRequest { + + private URI uri; + + MockedHttpRequest(String url) { + this.uri = URI.create(url); + } + + @Override + public String getMethodValue() { + return HttpMethod.GET.name(); + } + + @Override + public URI getURI() { + return uri; + } + + @Override + public HttpHeaders getHeaders() { + return new HttpHeaders(); + } + } +} diff --git a/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/constant/RouterConstant.java b/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/constant/RouterConstant.java index 68317b99e..00d9633a7 100644 --- a/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/constant/RouterConstant.java +++ b/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/constant/RouterConstant.java @@ -38,6 +38,11 @@ public final class RouterConstant { */ public static final String ROUTER_LABEL_HEADER = "internal-router-label"; + /** + * + */ + public static final String HEADER_ROUTER_CONTEXT = "routerContext"; + /** * Default Private Constructor. */ diff --git a/spring-cloud-tencent-examples/polaris-router-example/router-caller-service/pom.xml b/spring-cloud-tencent-examples/polaris-router-example/router-caller-service/pom.xml index 574fea7a1..2c4a25100 100644 --- a/spring-cloud-tencent-examples/polaris-router-example/router-caller-service/pom.xml +++ b/spring-cloud-tencent-examples/polaris-router-example/router-caller-service/pom.xml @@ -23,6 +23,12 @@ spring-cloud-starter-tencent-polaris-router + + org.springframework.retry + spring-retry + 1.3.1 + + org.springframework.boot spring-boot-starter-web