parent
3859cd7eef
commit
1dffb915af
@ -1,3 +1,14 @@
|
||||
# Change Log
|
||||
---
|
||||
|
||||
- [feature:add PolarisRateLimiterLimitedFallback spi.](https://github.com/Tencent/spring-cloud-tencent/commit/52689515f5dd7f1ddf22dbe9b62fcd9d19fc8dbe)
|
||||
- [fix:fix the error capture of rate limit exception.](https://github.com/Tencent/spring-cloud-tencent/commit/edeeaded922ee3837eb5a58582a62f1f4c77608e)
|
||||
- [feat:enable stat reporting as default.](https://github.com/Tencent/spring-cloud-tencent/commit/f97a1b2450bfba047d22e96a85e2ab39e849f4a0)
|
||||
- [refactor:update to junit 5.](https://github.com/Tencent/spring-cloud-tencent/commit/21953e17b6549e0ce52fc4df0a94f96a10dbe0c5)
|
||||
- [docs:support auto snapshot release in GitHub Action.](https://github.com/Tencent/spring-cloud-tencent/commit/da2422ae71f83d229e8c322ce2ccd5850ffa8653)
|
||||
- [feature:add User-Agent:polaris for healthyCheck api.](https://github.com/Tencent/spring-cloud-tencent/commit/b2cecba273bfd0eaf5160791c1efe3acb01d0bfe)
|
||||
- [optimize ServiceRuleManager](https://github.com/Tencent/spring-cloud-tencent/commit/07f04174ca487cf1db1d9c19bbe44853e2bcf117)
|
||||
- [refactor:refactor stat module.](https://github.com/Tencent/spring-cloud-tencent/commit/9408bfbb948446738c050b3b18d8ed3124da36c1)
|
||||
- [fix:fix NPE.](https://github.com/Tencent/spring-cloud-tencent/commit/17a9a8d75aef6ed39a0a1c7aa4b7d547f5d3d40e)
|
||||
- [refactor:optimize sct-all.](https://github.com/Tencent/spring-cloud-tencent/commit/3859cd7eefcd93752d05113597656e584d97c38d)
|
||||
- [feature:add polaris circuit breaker support]()
|
@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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.metadata.core;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Map;
|
||||
|
||||
import com.tencent.cloud.common.metadata.MetadataContext;
|
||||
import com.tencent.cloud.common.metadata.MetadataContextHolder;
|
||||
import com.tencent.cloud.common.util.JacksonUtils;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFunction;
|
||||
|
||||
import static com.tencent.cloud.common.constant.ContextConstant.UTF_8;
|
||||
import static com.tencent.cloud.common.constant.MetadataConstant.HeaderName.CUSTOM_DISPOSABLE_METADATA;
|
||||
import static com.tencent.cloud.common.constant.MetadataConstant.HeaderName.CUSTOM_METADATA;
|
||||
|
||||
/**
|
||||
* web client filter used for writing metadata in HTTP request header.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
public class EncodeTransferMedataWebClientFilter implements ExchangeFilterFunction {
|
||||
|
||||
@Override
|
||||
public Mono<ClientResponse> filter(ClientRequest clientRequest, ExchangeFunction next) {
|
||||
MetadataContext metadataContext = MetadataContextHolder.get();
|
||||
Map<String, String> customMetadata = metadataContext.getCustomMetadata();
|
||||
Map<String, String> disposableMetadata = metadataContext.getDisposableMetadata();
|
||||
Map<String, String> transHeaders = metadataContext.getTransHeadersKV();
|
||||
|
||||
ClientRequest.Builder requestBuilder = ClientRequest.from(clientRequest);
|
||||
|
||||
this.buildMetadataHeader(requestBuilder, customMetadata, CUSTOM_METADATA);
|
||||
this.buildMetadataHeader(requestBuilder, disposableMetadata, CUSTOM_DISPOSABLE_METADATA);
|
||||
this.buildTransmittedHeader(requestBuilder, transHeaders);
|
||||
|
||||
ClientRequest request = requestBuilder.build();
|
||||
|
||||
return next.exchange(request);
|
||||
}
|
||||
|
||||
private void buildTransmittedHeader(ClientRequest.Builder requestBuilder, Map<String, String> transHeaders) {
|
||||
if (!CollectionUtils.isEmpty(transHeaders)) {
|
||||
transHeaders.forEach(requestBuilder::header);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set metadata into the request header for {@link ClientRequest} .
|
||||
* @param requestBuilder instance of {@link ClientRequest.Builder}
|
||||
* @param metadata metadata map .
|
||||
* @param headerName target metadata http header name .
|
||||
*/
|
||||
private void buildMetadataHeader(ClientRequest.Builder requestBuilder, Map<String, String> metadata, String headerName) {
|
||||
if (!CollectionUtils.isEmpty(metadata)) {
|
||||
String encodedMetadata = JacksonUtils.serialize2Json(metadata);
|
||||
try {
|
||||
requestBuilder.header(headerName, URLEncoder.encode(encodedMetadata, UTF_8));
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
requestBuilder.header(headerName, encodedMetadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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.metadata.core;
|
||||
|
||||
import com.tencent.cloud.common.metadata.MetadataContext;
|
||||
import com.tencent.cloud.common.metadata.MetadataContextHolder;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
||||
|
||||
/**
|
||||
* Test for {@link EncodeTransferMedataWebClientFilter}.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@SpringBootTest(webEnvironment = RANDOM_PORT,
|
||||
classes = EncodeTransferMedataWebClientFilterTest.TestApplication.class,
|
||||
properties = {"spring.config.location = classpath:application-test.yml",
|
||||
"spring.main.web-application-type = reactive"})
|
||||
public class EncodeTransferMedataWebClientFilterTest {
|
||||
|
||||
@Autowired
|
||||
private WebClient.Builder webClientBuilder;
|
||||
@LocalServerPort
|
||||
private int localServerPort;
|
||||
|
||||
@Test
|
||||
public void testTransitiveMetadataFromApplicationConfig() {
|
||||
MetadataContext metadataContext = MetadataContextHolder.get();
|
||||
metadataContext.setTransHeadersKV("xxx", "xxx");
|
||||
String metadata = webClientBuilder.baseUrl("http://localhost:" + localServerPort).build()
|
||||
.get()
|
||||
.uri("/test")
|
||||
.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
.block();
|
||||
assertThat(metadata).isEqualTo("2");
|
||||
}
|
||||
|
||||
@SpringBootApplication
|
||||
@RestController
|
||||
protected static class TestApplication {
|
||||
|
||||
@Bean
|
||||
public WebClient.Builder webClientBuilder() {
|
||||
return WebClient.builder();
|
||||
}
|
||||
|
||||
@RequestMapping("/test")
|
||||
public String test() {
|
||||
return MetadataContextHolder.get().getContext(MetadataContext.FRAGMENT_TRANSITIVE, "b");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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.circuitbreaker;
|
||||
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisResultToErrorCode;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.util.PolarisCircuitBreakerUtils;
|
||||
import com.tencent.polaris.api.core.ConsumerAPI;
|
||||
import com.tencent.polaris.api.pojo.ServiceKey;
|
||||
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
|
||||
import com.tencent.polaris.circuitbreak.api.FunctionalDecorator;
|
||||
import com.tencent.polaris.circuitbreak.api.InvokeHandler;
|
||||
import com.tencent.polaris.circuitbreak.api.pojo.FunctionalDecoratorRequest;
|
||||
import com.tencent.polaris.circuitbreak.api.pojo.InvokeContext;
|
||||
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
|
||||
/**
|
||||
* PolarisCircuitBreaker.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
public class PolarisCircuitBreaker implements CircuitBreaker, InvokeHandler {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PolarisCircuitBreaker.class);
|
||||
private final PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf;
|
||||
private final ConsumerAPI consumerAPI;
|
||||
private final FunctionalDecorator decorator;
|
||||
private final InvokeHandler invokeHandler;
|
||||
|
||||
public PolarisCircuitBreaker(PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf,
|
||||
ConsumerAPI consumerAPI,
|
||||
CircuitBreakAPI circuitBreakAPI) {
|
||||
FunctionalDecoratorRequest makeDecoratorRequest = new FunctionalDecoratorRequest(new ServiceKey(conf.getNamespace(), conf.getService()), conf.getMethod());
|
||||
makeDecoratorRequest.setSourceService(new ServiceKey(conf.getSourceNamespace(), conf.getSourceService()));
|
||||
makeDecoratorRequest.setResultToErrorCode(new PolarisResultToErrorCode());
|
||||
this.consumerAPI = consumerAPI;
|
||||
this.conf = conf;
|
||||
this.decorator = circuitBreakAPI.makeFunctionalDecorator(makeDecoratorRequest);
|
||||
this.invokeHandler = circuitBreakAPI.makeInvokeHandler(makeDecoratorRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback) {
|
||||
Supplier<T> toRunDecorator = decorator.decorateSupplier(toRun);
|
||||
try {
|
||||
return toRunDecorator.get();
|
||||
}
|
||||
catch (CallAbortedException e) {
|
||||
LOGGER.debug("PolarisCircuitBreaker CallAbortedException: {}", e.getMessage());
|
||||
PolarisCircuitBreakerUtils.reportStatus(consumerAPI, conf, e);
|
||||
return fallback.apply(e);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return fallback.apply(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void acquirePermission() {
|
||||
invokeHandler.acquirePermission();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(InvokeContext.ResponseContext responseContext) {
|
||||
invokeHandler.onSuccess(responseContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(InvokeContext.ResponseContext responseContext) {
|
||||
invokeHandler.onError(responseContext);
|
||||
}
|
||||
|
||||
public PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration getConf() {
|
||||
return conf;
|
||||
}
|
||||
|
||||
public ConsumerAPI getConsumerAPI() {
|
||||
return consumerAPI;
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.circuitbreaker;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.util.PolarisCircuitBreakerUtils;
|
||||
import com.tencent.polaris.api.core.ConsumerAPI;
|
||||
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
|
||||
/**
|
||||
* PolarisCircuitBreakerFactory.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
public class PolarisCircuitBreakerFactory
|
||||
extends CircuitBreakerFactory<PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration, PolarisCircuitBreakerConfigBuilder> {
|
||||
|
||||
private Function<String, PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration> defaultConfiguration =
|
||||
id -> {
|
||||
String[] metadata = PolarisCircuitBreakerUtils.resolveCircuitBreakerId(id);
|
||||
return new PolarisCircuitBreakerConfigBuilder()
|
||||
.namespace(metadata[0])
|
||||
.service(metadata[1])
|
||||
.method(metadata[2])
|
||||
.build();
|
||||
};
|
||||
|
||||
|
||||
private final CircuitBreakAPI circuitBreakAPI;
|
||||
|
||||
private final ConsumerAPI consumerAPI;
|
||||
|
||||
public PolarisCircuitBreakerFactory(CircuitBreakAPI circuitBreakAPI, ConsumerAPI consumerAPI) {
|
||||
this.circuitBreakAPI = circuitBreakAPI;
|
||||
this.consumerAPI = consumerAPI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CircuitBreaker create(String id) {
|
||||
PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf = getConfigurations()
|
||||
.computeIfAbsent(id, defaultConfiguration);
|
||||
return new PolarisCircuitBreaker(conf, consumerAPI, circuitBreakAPI);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PolarisCircuitBreakerConfigBuilder configBuilder(String id) {
|
||||
String[] metadata = PolarisCircuitBreakerUtils.resolveCircuitBreakerId(id);
|
||||
return new PolarisCircuitBreakerConfigBuilder(metadata[0], metadata[1], metadata[2]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureDefault(Function<String, PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration> defaultConfiguration) {
|
||||
this.defaultConfiguration = defaultConfiguration;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.circuitbreaker;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisResultToErrorCode;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.reactor.PolarisCircuitBreakerReactorTransformer;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.util.PolarisCircuitBreakerUtils;
|
||||
import com.tencent.polaris.api.core.ConsumerAPI;
|
||||
import com.tencent.polaris.api.pojo.ServiceKey;
|
||||
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
|
||||
import com.tencent.polaris.circuitbreak.api.InvokeHandler;
|
||||
import com.tencent.polaris.circuitbreak.api.pojo.FunctionalDecoratorRequest;
|
||||
import com.tencent.polaris.circuitbreak.api.pojo.InvokeContext;
|
||||
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
|
||||
|
||||
/**
|
||||
* ReactivePolarisCircuitBreaker.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
public class ReactivePolarisCircuitBreaker implements ReactiveCircuitBreaker {
|
||||
|
||||
private final InvokeHandler invokeHandler;
|
||||
|
||||
private final ConsumerAPI consumerAPI;
|
||||
|
||||
private final PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf;
|
||||
|
||||
public ReactivePolarisCircuitBreaker(PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf,
|
||||
ConsumerAPI consumerAPI,
|
||||
CircuitBreakAPI circuitBreakAPI) {
|
||||
InvokeContext.RequestContext requestContext = new FunctionalDecoratorRequest(new ServiceKey(conf.getNamespace(), conf.getService()), conf.getMethod());
|
||||
requestContext.setSourceService(new ServiceKey(conf.getSourceNamespace(), conf.getSourceService()));
|
||||
requestContext.setResultToErrorCode(new PolarisResultToErrorCode());
|
||||
this.consumerAPI = consumerAPI;
|
||||
this.conf = conf;
|
||||
this.invokeHandler = circuitBreakAPI.makeInvokeHandler(requestContext);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> run(Mono<T> toRun, Function<Throwable, Mono<T>> fallback) {
|
||||
Mono<T> toReturn = toRun.transform(new PolarisCircuitBreakerReactorTransformer<>(invokeHandler));
|
||||
if (fallback != null) {
|
||||
toReturn = toReturn.onErrorResume(throwable -> {
|
||||
if (throwable instanceof CallAbortedException) {
|
||||
PolarisCircuitBreakerUtils.reportStatus(consumerAPI, conf, (CallAbortedException) throwable);
|
||||
}
|
||||
return fallback.apply(throwable);
|
||||
});
|
||||
}
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> run(Flux<T> toRun, Function<Throwable, Flux<T>> fallback) {
|
||||
Flux<T> toReturn = toRun.transform(new PolarisCircuitBreakerReactorTransformer<>(invokeHandler));
|
||||
if (fallback != null) {
|
||||
toReturn = toReturn.onErrorResume(throwable -> {
|
||||
if (throwable instanceof CallAbortedException) {
|
||||
PolarisCircuitBreakerUtils.reportStatus(consumerAPI, conf, (CallAbortedException) throwable);
|
||||
}
|
||||
return fallback.apply(throwable);
|
||||
});
|
||||
}
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.circuitbreaker;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.util.PolarisCircuitBreakerUtils;
|
||||
import com.tencent.polaris.api.core.ConsumerAPI;
|
||||
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory;
|
||||
|
||||
/**
|
||||
* ReactivePolarisCircuitBreakerFactory.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
public class ReactivePolarisCircuitBreakerFactory extends
|
||||
ReactiveCircuitBreakerFactory<PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration, PolarisCircuitBreakerConfigBuilder> {
|
||||
|
||||
private Function<String, PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration> defaultConfiguration =
|
||||
id -> {
|
||||
String[] metadata = PolarisCircuitBreakerUtils.resolveCircuitBreakerId(id);
|
||||
return new PolarisCircuitBreakerConfigBuilder()
|
||||
.namespace(metadata[0])
|
||||
.service(metadata[1])
|
||||
.method(metadata[2])
|
||||
.build();
|
||||
};
|
||||
|
||||
private final CircuitBreakAPI circuitBreakAPI;
|
||||
|
||||
private final ConsumerAPI consumerAPI;
|
||||
|
||||
public ReactivePolarisCircuitBreakerFactory(CircuitBreakAPI circuitBreakAPI, ConsumerAPI consumerAPI) {
|
||||
this.circuitBreakAPI = circuitBreakAPI;
|
||||
this.consumerAPI = consumerAPI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveCircuitBreaker create(String id) {
|
||||
PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf = getConfigurations()
|
||||
.computeIfAbsent(id, defaultConfiguration);
|
||||
return new ReactivePolarisCircuitBreaker(conf, consumerAPI, circuitBreakAPI);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PolarisCircuitBreakerConfigBuilder configBuilder(String id) {
|
||||
String[] metadata = PolarisCircuitBreakerUtils.resolveCircuitBreakerId(id);
|
||||
return new PolarisCircuitBreakerConfigBuilder(metadata[0], metadata[1], metadata[2]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureDefault(
|
||||
Function<String, PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration> defaultConfiguration) {
|
||||
this.defaultConfiguration = defaultConfiguration;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.circuitbreaker.common;
|
||||
|
||||
import com.tencent.cloud.common.constant.ContextConstant;
|
||||
import com.tencent.cloud.polaris.context.PolarisConfigModifier;
|
||||
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
|
||||
import com.tencent.polaris.api.config.consumer.ServiceRouterConfig;
|
||||
import com.tencent.polaris.factory.config.ConfigurationImpl;
|
||||
import com.tencent.polaris.plugins.router.healthy.RecoverRouterConfig;
|
||||
|
||||
/**
|
||||
* CircuitBreakerConfigModifier.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
public class CircuitBreakerConfigModifier implements PolarisConfigModifier {
|
||||
|
||||
private final RpcEnhancementReporterProperties properties;
|
||||
|
||||
public CircuitBreakerConfigModifier(RpcEnhancementReporterProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modify(ConfigurationImpl configuration) {
|
||||
properties.setEnabled(true);
|
||||
|
||||
// Turn on circuitbreaker configuration
|
||||
configuration.getConsumer().getCircuitBreaker().setEnable(true);
|
||||
|
||||
// Set excludeCircuitBreakInstances to true
|
||||
RecoverRouterConfig recoverRouterConfig = configuration.getConsumer().getServiceRouter()
|
||||
.getPluginConfig(ServiceRouterConfig.DEFAULT_ROUTER_RECOVER, RecoverRouterConfig.class);
|
||||
|
||||
recoverRouterConfig.setExcludeCircuitBreakInstances(true);
|
||||
|
||||
// Update modified config to source properties
|
||||
configuration.getConsumer().getServiceRouter()
|
||||
.setPluginConfig(ServiceRouterConfig.DEFAULT_ROUTER_RECOVER, recoverRouterConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return ContextConstant.ModifierOrder.CIRCUIT_BREAKER_ORDER;
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.circuitbreaker.common;
|
||||
|
||||
import com.tencent.cloud.common.metadata.MetadataContext;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.ConfigBuilder;
|
||||
|
||||
/**
|
||||
* PolarisCircuitBreakerConfigBuilder.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
public class PolarisCircuitBreakerConfigBuilder implements ConfigBuilder<PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration> {
|
||||
|
||||
private String namespace = MetadataContext.LOCAL_NAMESPACE;
|
||||
|
||||
private String service;
|
||||
|
||||
private String method;
|
||||
|
||||
public PolarisCircuitBreakerConfigBuilder() {
|
||||
|
||||
}
|
||||
|
||||
public PolarisCircuitBreakerConfigBuilder(String namespace, String service, String method) {
|
||||
this.namespace = namespace;
|
||||
this.service = service;
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
public PolarisCircuitBreakerConfigBuilder namespace(String namespace) {
|
||||
this.namespace = namespace;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PolarisCircuitBreakerConfigBuilder service(String service) {
|
||||
this.service = service;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PolarisCircuitBreakerConfigBuilder method(String method) {
|
||||
this.method = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PolarisCircuitBreakerConfiguration build() {
|
||||
PolarisCircuitBreakerConfiguration conf = new PolarisCircuitBreakerConfiguration();
|
||||
conf.setNamespace(namespace);
|
||||
conf.setService(service);
|
||||
conf.setMethod(method);
|
||||
return conf;
|
||||
}
|
||||
|
||||
public static class PolarisCircuitBreakerConfiguration {
|
||||
|
||||
private final String sourceNamespace = MetadataContext.LOCAL_NAMESPACE;
|
||||
|
||||
private final String sourceService = MetadataContext.LOCAL_SERVICE;
|
||||
|
||||
private String namespace;
|
||||
|
||||
private String service;
|
||||
|
||||
private String method;
|
||||
|
||||
public String getNamespace() {
|
||||
return namespace;
|
||||
}
|
||||
|
||||
public void setNamespace(String namespace) {
|
||||
this.namespace = namespace;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public String getSourceNamespace() {
|
||||
return sourceNamespace;
|
||||
}
|
||||
|
||||
public String getSourceService() {
|
||||
return sourceService;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.circuitbreaker.common;
|
||||
|
||||
import com.tencent.cloud.polaris.circuitbreaker.zuul.PolarisCircuitBreakerPostZuulFilter;
|
||||
import com.tencent.polaris.circuitbreak.api.pojo.ResultToErrorCode;
|
||||
import feign.FeignException;
|
||||
|
||||
import org.springframework.web.client.RestClientResponseException;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
|
||||
/**
|
||||
* PolarisResultToErrorCode.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
public class PolarisResultToErrorCode implements ResultToErrorCode {
|
||||
|
||||
@Override
|
||||
public int onSuccess(Object value) {
|
||||
return 200;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onError(Throwable e) {
|
||||
if (checkClassExist("org.springframework.web.client.RestClientResponseException")
|
||||
&& e instanceof RestClientResponseException) {
|
||||
return ((RestClientResponseException) e).getRawStatusCode();
|
||||
}
|
||||
else if (checkClassExist("feign.FeignException")
|
||||
&& e instanceof FeignException) {
|
||||
return ((FeignException) e).status();
|
||||
}
|
||||
else if (checkClassExist("org.springframework.web.reactive.function.client.WebClientResponseException")
|
||||
&& e instanceof WebClientResponseException) {
|
||||
return ((WebClientResponseException) e).getRawStatusCode();
|
||||
}
|
||||
else if (e instanceof PolarisCircuitBreakerPostZuulFilter.CircuitBreakerStatusCodeException) {
|
||||
return ((PolarisCircuitBreakerPostZuulFilter.CircuitBreakerStatusCodeException) e).getRawStatusCode();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private boolean checkClassExist(String clazzName) {
|
||||
try {
|
||||
Class.forName(clazzName, false, getClass().getClassLoader());
|
||||
}
|
||||
catch (ClassNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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.circuitbreaker.config;
|
||||
|
||||
import com.tencent.cloud.polaris.circuitbreaker.ReactivePolarisCircuitBreakerFactory;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.gateway.PolarisCircuitBreakerFilterFactory;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory;
|
||||
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
|
||||
import org.springframework.cloud.gateway.config.GatewayAutoConfiguration;
|
||||
import org.springframework.cloud.gateway.config.conditional.ConditionalOnEnabledFilter;
|
||||
import org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties;
|
||||
import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.reactive.DispatcherHandler;
|
||||
|
||||
/**
|
||||
* GatewayPolarisCircuitBreakerAutoConfiguration.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
|
||||
@AutoConfigureAfter({ReactivePolarisCircuitBreakerAutoConfiguration.class })
|
||||
@ConditionalOnClass({ DispatcherHandler.class, ReactivePolarisCircuitBreakerAutoConfiguration.class,
|
||||
ReactiveCircuitBreakerFactory.class, ReactivePolarisCircuitBreakerFactory.class, GatewayAutoConfiguration.class})
|
||||
public class GatewayPolarisCircuitBreakerAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(ReactiveCircuitBreakerFactory.class)
|
||||
@ConditionalOnEnabledFilter
|
||||
public PolarisCircuitBreakerFilterFactory polarisCircuitBreakerFilterFactory(
|
||||
ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory,
|
||||
ObjectProvider<DispatcherHandler> dispatcherHandler,
|
||||
@Autowired(required = false) ReactiveDiscoveryClient discoveryClient,
|
||||
@Autowired(required = false) DiscoveryLocatorProperties properties
|
||||
) {
|
||||
return new PolarisCircuitBreakerFilterFactory(reactiveCircuitBreakerFactory, dispatcherHandler, discoveryClient, properties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnEnabledFilter
|
||||
public FallbackHeadersGatewayFilterFactory fallbackHeadersGatewayFilterFactory() {
|
||||
return new FallbackHeadersGatewayFilterFactory();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.circuitbreaker.config;
|
||||
|
||||
import com.tencent.cloud.polaris.circuitbreaker.feign.PolarisCircuitBreakerNameResolver;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.feign.PolarisFeignCircuitBreaker;
|
||||
import feign.Feign;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.cloud.openfeign.FeignClientFactoryBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
|
||||
/**
|
||||
* PolarisCircuitBreakerFeignClientAutoConfiguration.
|
||||
*
|
||||
* @author seansyyu 2023-02-28
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass({Feign.class, FeignClientFactoryBean.class})
|
||||
@ConditionalOnPolarisCircuitBreakerEnabled
|
||||
public class PolarisCircuitBreakerFeignClientAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public PolarisCircuitBreakerNameResolver polarisCircuitBreakerNameResolver() {
|
||||
return new PolarisCircuitBreakerNameResolver();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
@ConditionalOnBean(CircuitBreakerFactory.class)
|
||||
@ConditionalOnMissingBean(Feign.Builder.class)
|
||||
public Feign.Builder circuitBreakerFeignBuilder() {
|
||||
return PolarisFeignCircuitBreaker.builder();
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.circuitbreaker.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.tencent.cloud.polaris.circuitbreaker.ReactivePolarisCircuitBreakerFactory;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.common.CircuitBreakerConfigModifier;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.reporter.ExceptionCircuitBreakerReporter;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.reporter.SuccessCircuitBreakerReporter;
|
||||
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementAutoConfiguration;
|
||||
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
|
||||
import com.tencent.polaris.api.core.ConsumerAPI;
|
||||
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
|
||||
import com.tencent.polaris.circuitbreak.factory.CircuitBreakAPIFactory;
|
||||
import com.tencent.polaris.client.api.SDKContext;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.cloud.client.circuitbreaker.Customizer;
|
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* AutoConfiguration for ReactivePolarisCircuitBreaker.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(name = { "reactor.core.publisher.Mono", "reactor.core.publisher.Flux" })
|
||||
@ConditionalOnPolarisCircuitBreakerEnabled
|
||||
@AutoConfigureAfter(RpcEnhancementAutoConfiguration.class)
|
||||
public class ReactivePolarisCircuitBreakerAutoConfiguration {
|
||||
|
||||
@Autowired(required = false)
|
||||
private List<Customizer<ReactivePolarisCircuitBreakerFactory>> customizers = new ArrayList<>();
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(CircuitBreakAPI.class)
|
||||
public CircuitBreakAPI circuitBreakAPI(SDKContext polarisContext) {
|
||||
return CircuitBreakAPIFactory.createCircuitBreakAPIByContext(polarisContext);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(SuccessCircuitBreakerReporter.class)
|
||||
public SuccessCircuitBreakerReporter successCircuitBreakerReporter(RpcEnhancementReporterProperties properties,
|
||||
SDKContext polarisContext, CircuitBreakAPI circuitBreakAPI) {
|
||||
return new SuccessCircuitBreakerReporter(properties, polarisContext, circuitBreakAPI);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ExceptionCircuitBreakerReporter.class)
|
||||
public ExceptionCircuitBreakerReporter exceptionCircuitBreakerReporter(RpcEnhancementReporterProperties properties,
|
||||
SDKContext polarisContext, CircuitBreakAPI circuitBreakAPI) {
|
||||
return new ExceptionCircuitBreakerReporter(properties, polarisContext, circuitBreakAPI);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ReactiveCircuitBreakerFactory.class)
|
||||
public ReactiveCircuitBreakerFactory polarisReactiveCircuitBreakerFactory(CircuitBreakAPI circuitBreakAPI, ConsumerAPI consumerAPI) {
|
||||
ReactivePolarisCircuitBreakerFactory factory = new ReactivePolarisCircuitBreakerFactory(circuitBreakAPI, consumerAPI);
|
||||
customizers.forEach(customizer -> customizer.customize(factory));
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(RpcEnhancementReporterProperties.class)
|
||||
@ConditionalOnMissingBean(CircuitBreakerConfigModifier.class)
|
||||
public CircuitBreakerConfigModifier reactiveCircuitBreakerConfigModifier(RpcEnhancementReporterProperties properties) {
|
||||
return new CircuitBreakerConfigModifier(properties);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.circuitbreaker.feign;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.tencent.polaris.api.pojo.CircuitBreakerStatus;
|
||||
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
|
||||
import feign.Request;
|
||||
import feign.RequestTemplate;
|
||||
import feign.Response;
|
||||
import feign.codec.Decoder;
|
||||
|
||||
import org.springframework.cloud.openfeign.FallbackFactory;
|
||||
|
||||
/**
|
||||
* PolarisCircuitBreakerFallbackFactory.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
public class PolarisCircuitBreakerFallbackFactory implements FallbackFactory {
|
||||
|
||||
private final Decoder decoder;
|
||||
|
||||
public PolarisCircuitBreakerFallbackFactory(Decoder decoder) {
|
||||
this.decoder = decoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object create(Throwable t) {
|
||||
return new DefaultFallback(t, decoder);
|
||||
}
|
||||
|
||||
public class DefaultFallback {
|
||||
|
||||
private final Throwable t;
|
||||
|
||||
private final Decoder decoder;
|
||||
|
||||
public DefaultFallback(Throwable t, Decoder decoder) {
|
||||
this.t = t;
|
||||
this.decoder = decoder;
|
||||
}
|
||||
|
||||
public Object fallback(Method method) {
|
||||
if (t instanceof CallAbortedException) {
|
||||
CircuitBreakerStatus.FallbackInfo fallbackInfo = ((CallAbortedException) t).getFallbackInfo();
|
||||
if (fallbackInfo != null) {
|
||||
Response.Builder responseBuilder = Response.builder()
|
||||
.status(fallbackInfo.getCode());
|
||||
if (fallbackInfo.getHeaders() != null) {
|
||||
Map<String, Collection<String>> headers = new HashMap<>();
|
||||
fallbackInfo.getHeaders().forEach((k, v) -> headers.put(k, Collections.singleton(v)));
|
||||
responseBuilder.headers(headers);
|
||||
}
|
||||
if (fallbackInfo.getBody() != null) {
|
||||
responseBuilder.body(fallbackInfo.getBody(), StandardCharsets.UTF_8);
|
||||
}
|
||||
// Feign Response need a nonnull Request,
|
||||
// which is not important in fallback response (no real request),
|
||||
// so we create a fake one
|
||||
Request fakeRequest = Request.create(Request.HttpMethod.GET, "/", new HashMap<>(), Request.Body.empty(), new RequestTemplate());
|
||||
responseBuilder.request(fakeRequest);
|
||||
|
||||
try (Response response = responseBuilder.build()) {
|
||||
return decoder.decode(response, method.getGenericReturnType());
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException(t);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.circuitbreaker.feign;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import com.tencent.cloud.common.metadata.MetadataContext;
|
||||
import feign.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation;
|
||||
|
||||
/**
|
||||
* PolarisCircuitBreakerNameResolver.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
public class PolarisCircuitBreakerNameResolver {
|
||||
|
||||
public String resolveCircuitBreakerName(String feignClientName, Target<?> target, Method method) {
|
||||
String serviceName = target.name();
|
||||
RequestMapping requestMapping = findMergedAnnotation(method, RequestMapping.class);
|
||||
String path = "";
|
||||
if (requestMapping != null) {
|
||||
path = requestMapping.path().length == 0 ?
|
||||
requestMapping.value().length == 0 ? "" : requestMapping.value()[0] :
|
||||
requestMapping.path()[0];
|
||||
}
|
||||
return "".equals(path) ?
|
||||
MetadataContext.LOCAL_NAMESPACE + "#" + serviceName :
|
||||
MetadataContext.LOCAL_NAMESPACE + "#" + serviceName + "#" + path;
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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.circuitbreaker.feign;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import feign.Feign;
|
||||
import feign.Target;
|
||||
import feign.codec.Decoder;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.cloud.openfeign.FallbackFactory;
|
||||
import org.springframework.cloud.openfeign.FeignCircuitBreaker;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* PolarisFeignCircuitBreaker, mostly copy from {@link FeignCircuitBreaker}, but giving Polaris modification.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
public final class PolarisFeignCircuitBreaker {
|
||||
|
||||
private PolarisFeignCircuitBreaker() {
|
||||
throw new IllegalStateException("Don't instantiate a utility class");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return builder for Feign CircuitBreaker integration
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for Feign CircuitBreaker integration.
|
||||
*/
|
||||
public static final class Builder extends Feign.Builder {
|
||||
|
||||
private CircuitBreakerFactory circuitBreakerFactory;
|
||||
private String feignClientName;
|
||||
private PolarisCircuitBreakerNameResolver circuitBreakerNameResolver;
|
||||
|
||||
public Builder() {
|
||||
}
|
||||
|
||||
public Builder circuitBreakerFactory(CircuitBreakerFactory circuitBreakerFactory) {
|
||||
this.circuitBreakerFactory = circuitBreakerFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder feignClientName(String feignClientName) {
|
||||
this.feignClientName = feignClientName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder circuitBreakerNameResolver(PolarisCircuitBreakerNameResolver circuitBreakerNameResolver) {
|
||||
this.circuitBreakerNameResolver = circuitBreakerNameResolver;
|
||||
return this;
|
||||
}
|
||||
|
||||
public <T> T target(Target<T> target, T fallback) {
|
||||
return build(fallback != null ? new FallbackFactory.Default<T>(fallback) : null).newInstance(target);
|
||||
}
|
||||
|
||||
public <T> T target(Target<T> target, FallbackFactory<? extends T> fallbackFactory) {
|
||||
return build(fallbackFactory).newInstance(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T target(Target<T> target) {
|
||||
return build(null).newInstance(target);
|
||||
}
|
||||
|
||||
public Feign build(final FallbackFactory<?> nullableFallbackFactory) {
|
||||
Field field = ReflectionUtils.findField(Feign.Builder.class, "decoder");
|
||||
field.setAccessible(true);
|
||||
Decoder decoder = (Decoder) ReflectionUtils.getField(field, this);
|
||||
this.invocationHandlerFactory((target, dispatch) -> new PolarisFeignCircuitBreakerInvocationHandler(
|
||||
circuitBreakerFactory, feignClientName, target, dispatch, nullableFallbackFactory, circuitBreakerNameResolver, decoder));
|
||||
return this.build();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,199 @@
|
||||
/*
|
||||
* 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.circuitbreaker.feign;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import feign.InvocationHandlerFactory;
|
||||
import feign.Target;
|
||||
import feign.codec.Decoder;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
|
||||
import org.springframework.cloud.openfeign.FallbackFactory;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
|
||||
import static feign.Util.checkNotNull;
|
||||
|
||||
/**
|
||||
* PolarisFeignCircuitBreakerInvocationHandler, mostly copy from {@link org.springframework.cloud.openfeign.FeignCircuitBreakerInvocationHandler}, but giving Polaris modification.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
public class PolarisFeignCircuitBreakerInvocationHandler implements InvocationHandler {
|
||||
|
||||
private final CircuitBreakerFactory factory;
|
||||
|
||||
private final String feignClientName;
|
||||
|
||||
private final Target<?> target;
|
||||
|
||||
private final Map<Method, InvocationHandlerFactory.MethodHandler> dispatch;
|
||||
|
||||
private final FallbackFactory<?> nullableFallbackFactory;
|
||||
|
||||
private final Map<Method, Method> fallbackMethodMap;
|
||||
|
||||
private final PolarisCircuitBreakerNameResolver circuitBreakerNameResolver;
|
||||
|
||||
private final Decoder decoder;
|
||||
|
||||
public PolarisFeignCircuitBreakerInvocationHandler(CircuitBreakerFactory factory, String feignClientName, Target<?> target,
|
||||
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch, FallbackFactory<?> nullableFallbackFactory,
|
||||
PolarisCircuitBreakerNameResolver circuitBreakerNameResolver, Decoder decoder) {
|
||||
this.factory = factory;
|
||||
this.feignClientName = feignClientName;
|
||||
this.target = checkNotNull(target, "target");
|
||||
this.dispatch = checkNotNull(dispatch, "dispatch");
|
||||
this.fallbackMethodMap = toFallbackMethod(dispatch);
|
||||
this.nullableFallbackFactory = nullableFallbackFactory;
|
||||
this.circuitBreakerNameResolver = circuitBreakerNameResolver;
|
||||
this.decoder = decoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the method param of {@link InvocationHandler#invoke(Object, Method, Object[])}
|
||||
* is not accessible, i.e in a package-private interface, the fallback call will cause
|
||||
* of access restrictions. But methods in dispatch are copied methods. So setting
|
||||
* access to dispatch method doesn't take effect to the method in
|
||||
* InvocationHandler.invoke. Use map to store a copy of method to invoke the fallback
|
||||
* to bypass this and reducing the count of reflection calls.
|
||||
* @return cached methods map for fallback invoking
|
||||
*/
|
||||
static Map<Method, Method> toFallbackMethod(Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) {
|
||||
Map<Method, Method> result = new LinkedHashMap<>();
|
||||
for (Method method : dispatch.keySet()) {
|
||||
method.setAccessible(true);
|
||||
result.put(method, method);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
|
||||
// early exit if the invoked method is from java.lang.Object
|
||||
// code is the same as ReflectiveFeign.FeignInvocationHandler
|
||||
if ("equals".equals(method.getName())) {
|
||||
try {
|
||||
Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
|
||||
return equals(otherHandler);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if ("hashCode".equals(method.getName())) {
|
||||
return hashCode();
|
||||
}
|
||||
else if ("toString".equals(method.getName())) {
|
||||
return toString();
|
||||
}
|
||||
|
||||
String circuitName = circuitBreakerNameResolver.resolveCircuitBreakerName(feignClientName, target, method);
|
||||
CircuitBreaker circuitBreaker = factory.create(circuitName);
|
||||
Supplier<Object> supplier = asSupplier(method, args);
|
||||
Function<Throwable, Object> fallbackFunction;
|
||||
if (this.nullableFallbackFactory != null) {
|
||||
fallbackFunction = throwable -> {
|
||||
Object fallback = this.nullableFallbackFactory.create(throwable);
|
||||
try {
|
||||
return this.fallbackMethodMap.get(method).invoke(fallback, args);
|
||||
}
|
||||
catch (Exception exception) {
|
||||
unwrapAndRethrow(exception);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
else {
|
||||
fallbackFunction = throwable -> {
|
||||
PolarisCircuitBreakerFallbackFactory.DefaultFallback fallback =
|
||||
(PolarisCircuitBreakerFallbackFactory.DefaultFallback) new PolarisCircuitBreakerFallbackFactory(this.decoder).create(throwable);
|
||||
return fallback.fallback(method);
|
||||
};
|
||||
}
|
||||
return circuitBreaker.run(supplier, fallbackFunction);
|
||||
}
|
||||
|
||||
private void unwrapAndRethrow(Exception exception) {
|
||||
if (exception instanceof InvocationTargetException || exception instanceof NoFallbackAvailableException) {
|
||||
Throwable underlyingException = exception.getCause();
|
||||
if (underlyingException instanceof RuntimeException) {
|
||||
throw (RuntimeException) underlyingException;
|
||||
}
|
||||
if (underlyingException != null) {
|
||||
throw new IllegalStateException(underlyingException);
|
||||
}
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private Supplier<Object> asSupplier(final Method method, final Object[] args) {
|
||||
final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
final Thread caller = Thread.currentThread();
|
||||
return () -> {
|
||||
boolean isAsync = caller != Thread.currentThread();
|
||||
try {
|
||||
if (isAsync) {
|
||||
RequestContextHolder.setRequestAttributes(requestAttributes);
|
||||
}
|
||||
return dispatch.get(method).invoke(args);
|
||||
}
|
||||
catch (RuntimeException throwable) {
|
||||
throw throwable;
|
||||
}
|
||||
catch (Throwable throwable) {
|
||||
throw new RuntimeException(throwable);
|
||||
}
|
||||
finally {
|
||||
if (isAsync) {
|
||||
RequestContextHolder.resetRequestAttributes();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof PolarisFeignCircuitBreakerInvocationHandler) {
|
||||
PolarisFeignCircuitBreakerInvocationHandler other = (PolarisFeignCircuitBreakerInvocationHandler) obj;
|
||||
return this.target.equals(other.target);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.target.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.target.toString();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,278 @@
|
||||
/*
|
||||
* 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.circuitbreaker.gateway;
|
||||
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.tencent.polaris.api.pojo.CircuitBreakerStatus;
|
||||
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.InvalidPropertyException;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory;
|
||||
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
|
||||
import org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerFilterFactory;
|
||||
import org.springframework.cloud.gateway.route.Route;
|
||||
import org.springframework.cloud.gateway.support.HttpStatusHolder;
|
||||
import org.springframework.cloud.gateway.support.ServiceUnavailableException;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.DispatcherHandler;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.containsEncodedParts;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.reset;
|
||||
|
||||
/**
|
||||
* PolarisCircuitBreakerFilterFactory.
|
||||
* mostly copy from SpringCloudCircuitBreakerFilterFactory, but create ReactiveCircuitBreaker per request to build method level CircuitBreaker.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
public class PolarisCircuitBreakerFilterFactory extends SpringCloudCircuitBreakerFilterFactory {
|
||||
|
||||
private final ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory;
|
||||
private final ObjectProvider<DispatcherHandler> dispatcherHandlerProvider;
|
||||
private String routeIdPrefix;
|
||||
// do not use this dispatcherHandler directly, use getDispatcherHandler() instead.
|
||||
private volatile DispatcherHandler dispatcherHandler;
|
||||
|
||||
public PolarisCircuitBreakerFilterFactory(
|
||||
ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory,
|
||||
ObjectProvider<DispatcherHandler> dispatcherHandlerProvider,
|
||||
ReactiveDiscoveryClient discoveryClient,
|
||||
DiscoveryLocatorProperties properties
|
||||
) {
|
||||
super(reactiveCircuitBreakerFactory, dispatcherHandlerProvider);
|
||||
this.reactiveCircuitBreakerFactory = reactiveCircuitBreakerFactory;
|
||||
this.dispatcherHandlerProvider = dispatcherHandlerProvider;
|
||||
if (discoveryClient != null && properties != null) {
|
||||
if (StringUtils.hasText(properties.getRouteIdPrefix())) {
|
||||
routeIdPrefix = properties.getRouteIdPrefix();
|
||||
}
|
||||
else {
|
||||
routeIdPrefix = discoveryClient.getClass().getSimpleName() + "_";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addExceptionDetails(Throwable t, ServerWebExchange exchange) {
|
||||
ofNullable(t).ifPresent(
|
||||
exception -> exchange.getAttributes().put(CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR, exception));
|
||||
}
|
||||
|
||||
private DispatcherHandler getDispatcherHandler() {
|
||||
if (dispatcherHandler == null) {
|
||||
dispatcherHandler = dispatcherHandlerProvider.getIfAvailable();
|
||||
}
|
||||
return dispatcherHandler;
|
||||
}
|
||||
|
||||
private String getCircuitBreakerId(Config config) {
|
||||
if (!StringUtils.hasText(config.getName()) && StringUtils.hasText(config.getRouteId())) {
|
||||
if (routeIdPrefix != null && config.getRouteId().startsWith(routeIdPrefix)) {
|
||||
return config.getRouteId().replace(routeIdPrefix, "");
|
||||
}
|
||||
return config.getRouteId();
|
||||
}
|
||||
return config.getName();
|
||||
}
|
||||
|
||||
private boolean isNumeric(String statusString) {
|
||||
try {
|
||||
Integer.parseInt(statusString);
|
||||
return true;
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<HttpStatus> getSeriesStatus(String series) {
|
||||
if (!Arrays.asList("1**", "2**", "3**", "4**", "5**").contains(series)) {
|
||||
throw new InvalidPropertyException(Config.class, "statusCodes", "polaris circuit breaker status code can only be a numeric http status, or a http series pattern, e.g. [\"1**\",\"2**\",\"3**\",\"4**\",\"5**\"]");
|
||||
}
|
||||
HttpStatus[] allHttpStatus = HttpStatus.values();
|
||||
if (series.startsWith("1")) {
|
||||
return Arrays.stream(allHttpStatus).filter(HttpStatus::is1xxInformational).collect(Collectors.toList());
|
||||
}
|
||||
else if (series.startsWith("2")) {
|
||||
return Arrays.stream(allHttpStatus).filter(HttpStatus::is2xxSuccessful).collect(Collectors.toList());
|
||||
}
|
||||
else if (series.startsWith("3")) {
|
||||
return Arrays.stream(allHttpStatus).filter(HttpStatus::is3xxRedirection).collect(Collectors.toList());
|
||||
}
|
||||
else if (series.startsWith("4")) {
|
||||
return Arrays.stream(allHttpStatus).filter(HttpStatus::is4xxClientError).collect(Collectors.toList());
|
||||
}
|
||||
else if (series.startsWith("5")) {
|
||||
return Arrays.stream(allHttpStatus).filter(HttpStatus::is5xxServerError).collect(Collectors.toList());
|
||||
}
|
||||
return Arrays.asList(allHttpStatus);
|
||||
}
|
||||
|
||||
private Set<HttpStatus> getDefaultStatus() {
|
||||
return Arrays.stream(HttpStatus.values())
|
||||
.filter(HttpStatus::is5xxServerError)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public GatewayFilter apply(Config config) {
|
||||
Set<HttpStatus> statuses = config.getStatusCodes().stream()
|
||||
.flatMap(statusCode -> {
|
||||
List<HttpStatus> httpStatuses = new ArrayList<>();
|
||||
if (isNumeric(statusCode)) {
|
||||
httpStatuses.add(HttpStatusHolder.parse(statusCode).getHttpStatus());
|
||||
}
|
||||
else {
|
||||
httpStatuses.addAll(getSeriesStatus(statusCode));
|
||||
}
|
||||
return httpStatuses.stream();
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
if (CollectionUtils.isEmpty(statuses)) {
|
||||
statuses.addAll(getDefaultStatus());
|
||||
}
|
||||
String circuitBreakerId = getCircuitBreakerId(config);
|
||||
return new GatewayFilter() {
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
|
||||
String serviceName = circuitBreakerId;
|
||||
if (route != null) {
|
||||
serviceName = route.getUri().getHost();
|
||||
}
|
||||
String path = exchange.getRequest().getPath().value();
|
||||
ReactiveCircuitBreaker cb = reactiveCircuitBreakerFactory.create(serviceName + "#" + path);
|
||||
return cb.run(
|
||||
chain.filter(exchange)
|
||||
.doOnSuccess(v -> {
|
||||
// throw CircuitBreakerStatusCodeException by default for all need checking status
|
||||
// so polaris can report right error status
|
||||
Set<HttpStatus> statusNeedToCheck = new HashSet<>();
|
||||
statusNeedToCheck.addAll(statuses);
|
||||
statusNeedToCheck.addAll(getDefaultStatus());
|
||||
HttpStatus status = exchange.getResponse().getStatusCode();
|
||||
if (statusNeedToCheck.contains(status)) {
|
||||
throw new CircuitBreakerStatusCodeException(status);
|
||||
}
|
||||
}),
|
||||
t -> {
|
||||
// pre-check CircuitBreakerStatusCodeException's status matches input status
|
||||
if (t instanceof CircuitBreakerStatusCodeException) {
|
||||
HttpStatus status = ((CircuitBreakerStatusCodeException) t).getStatusCode();
|
||||
// no need to fallback
|
||||
if (!statuses.contains(status)) {
|
||||
return Mono.error(t);
|
||||
}
|
||||
}
|
||||
// do fallback
|
||||
if (config.getFallbackUri() == null) {
|
||||
// polaris checking
|
||||
if (t instanceof CallAbortedException) {
|
||||
CircuitBreakerStatus.FallbackInfo fallbackInfo = ((CallAbortedException) t).getFallbackInfo();
|
||||
if (fallbackInfo != null) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
response.setRawStatusCode(fallbackInfo.getCode());
|
||||
if (fallbackInfo.getHeaders() != null) {
|
||||
fallbackInfo.getHeaders()
|
||||
.forEach((k, v) -> response.getHeaders().add(k, v));
|
||||
}
|
||||
DataBuffer bodyBuffer = null;
|
||||
if (fallbackInfo.getBody() != null) {
|
||||
byte[] bytes = fallbackInfo.getBody().getBytes(StandardCharsets.UTF_8);
|
||||
bodyBuffer = response.bufferFactory().wrap(bytes);
|
||||
}
|
||||
return bodyBuffer != null ? response.writeWith(Flux.just(bodyBuffer)) : response.setComplete();
|
||||
}
|
||||
}
|
||||
return Mono.error(t);
|
||||
}
|
||||
exchange.getResponse().setStatusCode(null);
|
||||
reset(exchange);
|
||||
|
||||
// TODO: copied from RouteToRequestUrlFilter
|
||||
URI uri = exchange.getRequest().getURI();
|
||||
// TODO: assume always?
|
||||
boolean encoded = containsEncodedParts(uri);
|
||||
URI requestUrl = UriComponentsBuilder.fromUri(uri).host(null).port(null)
|
||||
.uri(config.getFallbackUri()).scheme(null).build(encoded).toUri();
|
||||
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
|
||||
addExceptionDetails(t, exchange);
|
||||
|
||||
// Reset the exchange
|
||||
reset(exchange);
|
||||
|
||||
ServerHttpRequest request = exchange.getRequest().mutate().uri(requestUrl).build();
|
||||
return getDispatcherHandler().handle(exchange.mutate().request(request).build());
|
||||
})
|
||||
.onErrorResume(t -> handleErrorWithoutFallback(t));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return filterToStringCreator(PolarisCircuitBreakerFilterFactory.this)
|
||||
.append("name", config.getName()).append("fallback", config.getFallbackUri()).toString();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<Void> handleErrorWithoutFallback(Throwable t) {
|
||||
if (t instanceof java.util.concurrent.TimeoutException) {
|
||||
return Mono.error(new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, t.getMessage(), t));
|
||||
}
|
||||
if (t instanceof CallAbortedException) {
|
||||
return Mono.error(new ServiceUnavailableException());
|
||||
}
|
||||
if (t instanceof CircuitBreakerStatusCodeException) {
|
||||
return Mono.empty();
|
||||
}
|
||||
return Mono.error(t);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.circuitbreaker.reactor;
|
||||
|
||||
import com.tencent.polaris.circuitbreak.api.InvokeHandler;
|
||||
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.CoreSubscriber;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.FluxOperator;
|
||||
import reactor.core.publisher.Operators;
|
||||
|
||||
/**
|
||||
* FluxOperator for PolarisCircuitBreaker.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
public class PolarisCircuitBreakerFluxOperator<T> extends FluxOperator<T, T> {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PolarisCircuitBreakerFluxOperator.class);
|
||||
|
||||
private final InvokeHandler invokeHandler;
|
||||
|
||||
protected PolarisCircuitBreakerFluxOperator(Flux<? extends T> source, InvokeHandler invokeHandler) {
|
||||
super(source);
|
||||
this.invokeHandler = invokeHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(CoreSubscriber<? super T> actual) {
|
||||
try {
|
||||
invokeHandler.acquirePermission();
|
||||
source.subscribe(new PolarisCircuitBreakerReactorSubscriber<>(invokeHandler, actual, false));
|
||||
}
|
||||
catch (CallAbortedException e) {
|
||||
LOGGER.debug("ReactivePolarisCircuitBreaker CallAbortedException: {}", e.getMessage());
|
||||
Operators.error(actual, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.circuitbreaker.reactor;
|
||||
|
||||
import com.tencent.polaris.circuitbreak.api.InvokeHandler;
|
||||
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.CoreSubscriber;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.MonoOperator;
|
||||
import reactor.core.publisher.Operators;
|
||||
|
||||
/**
|
||||
* MonoOperator for PolarisCircuitBreaker.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
public class PolarisCircuitBreakerMonoOperator<T> extends MonoOperator<T, T> {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PolarisCircuitBreakerMonoOperator.class);
|
||||
|
||||
private final InvokeHandler invokeHandler;
|
||||
|
||||
protected PolarisCircuitBreakerMonoOperator(Mono<? extends T> source, InvokeHandler invokeHandler) {
|
||||
super(source);
|
||||
this.invokeHandler = invokeHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(CoreSubscriber<? super T> actual) {
|
||||
try {
|
||||
invokeHandler.acquirePermission();
|
||||
source.subscribe(new PolarisCircuitBreakerReactorSubscriber<>(invokeHandler, actual, true));
|
||||
}
|
||||
catch (CallAbortedException e) {
|
||||
LOGGER.debug("ReactivePolarisCircuitBreaker CallAbortedException: {}", e.getMessage());
|
||||
Operators.error(actual, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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.circuitbreaker.reactor;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import com.tencent.polaris.circuitbreak.api.InvokeHandler;
|
||||
import com.tencent.polaris.circuitbreak.api.pojo.InvokeContext;
|
||||
import org.reactivestreams.Subscription;
|
||||
import reactor.core.CoreSubscriber;
|
||||
import reactor.core.publisher.BaseSubscriber;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
/**
|
||||
* Reactor Subscriber for PolarisCircuitBreaker.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
public class PolarisCircuitBreakerReactorSubscriber<T> extends BaseSubscriber<T> {
|
||||
|
||||
private final InvokeHandler invokeHandler;
|
||||
|
||||
|
||||
private final CoreSubscriber<? super T> downstreamSubscriber;
|
||||
|
||||
private final long startTimeMilli;
|
||||
private final boolean singleProducer;
|
||||
|
||||
private final AtomicBoolean successSignaled = new AtomicBoolean(false);
|
||||
private final AtomicBoolean eventWasEmitted = new AtomicBoolean(false);
|
||||
|
||||
public PolarisCircuitBreakerReactorSubscriber(InvokeHandler invokeHandler, CoreSubscriber<? super T> downstreamSubscriber, boolean singleProducer) {
|
||||
this.invokeHandler = invokeHandler;
|
||||
this.downstreamSubscriber = downstreamSubscriber;
|
||||
this.singleProducer = singleProducer;
|
||||
this.startTimeMilli = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context currentContext() {
|
||||
return downstreamSubscriber.currentContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void hookOnSubscribe(Subscription subscription) {
|
||||
downstreamSubscriber.onSubscribe(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void hookOnNext(T value) {
|
||||
if (!isDisposed()) {
|
||||
if (singleProducer && successSignaled.compareAndSet(false, true)) {
|
||||
long delay = System.currentTimeMillis() - startTimeMilli;
|
||||
InvokeContext.ResponseContext responseContext = new InvokeContext.ResponseContext();
|
||||
responseContext.setDuration(delay);
|
||||
responseContext.setDurationUnit(TimeUnit.MILLISECONDS);
|
||||
responseContext.setResult(value);
|
||||
invokeHandler.onSuccess(responseContext);
|
||||
}
|
||||
eventWasEmitted.set(true);
|
||||
|
||||
downstreamSubscriber.onNext(value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void hookOnComplete() {
|
||||
if (successSignaled.compareAndSet(false, true)) {
|
||||
long delay = System.currentTimeMillis() - startTimeMilli;
|
||||
InvokeContext.ResponseContext responseContext = new InvokeContext.ResponseContext();
|
||||
responseContext.setDuration(delay);
|
||||
responseContext.setDurationUnit(TimeUnit.MILLISECONDS);
|
||||
invokeHandler.onSuccess(responseContext);
|
||||
}
|
||||
|
||||
downstreamSubscriber.onComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hookOnCancel() {
|
||||
if (!successSignaled.get()) {
|
||||
if (eventWasEmitted.get()) {
|
||||
long delay = System.currentTimeMillis() - startTimeMilli;
|
||||
InvokeContext.ResponseContext responseContext = new InvokeContext.ResponseContext();
|
||||
responseContext.setDuration(delay);
|
||||
responseContext.setDurationUnit(TimeUnit.MILLISECONDS);
|
||||
invokeHandler.onSuccess(responseContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void hookOnError(Throwable e) {
|
||||
long delay = System.currentTimeMillis() - startTimeMilli;
|
||||
InvokeContext.ResponseContext responseContext = new InvokeContext.ResponseContext();
|
||||
responseContext.setDuration(delay);
|
||||
responseContext.setDurationUnit(TimeUnit.MILLISECONDS);
|
||||
responseContext.setError(e);
|
||||
invokeHandler.onError(responseContext);
|
||||
downstreamSubscriber.onError(e);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.circuitbreaker.reactor;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.tencent.polaris.circuitbreak.api.InvokeHandler;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Reactor Transformer for PolarisCircuitBreaker.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
public class PolarisCircuitBreakerReactorTransformer<T> implements Function<Publisher<T>, Publisher<T>> {
|
||||
|
||||
private final InvokeHandler invokeHandler;
|
||||
|
||||
public PolarisCircuitBreakerReactorTransformer(InvokeHandler invokeHandler) {
|
||||
this.invokeHandler = invokeHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<T> apply(Publisher<T> publisher) {
|
||||
if (publisher instanceof Mono) {
|
||||
return new PolarisCircuitBreakerMonoOperator<>((Mono<? extends T>) publisher, invokeHandler);
|
||||
}
|
||||
else if (publisher instanceof Flux) {
|
||||
return new PolarisCircuitBreakerFluxOperator<>((Flux<? extends T>) publisher, invokeHandler);
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException("Publisher type is not supported: " + publisher.getClass().getCanonicalName());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.circuitbreaker.reporter;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import com.tencent.cloud.rpc.enhancement.AbstractPolarisReporterAdapter;
|
||||
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPlugin;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext;
|
||||
import com.tencent.polaris.api.plugin.circuitbreaker.ResourceStat;
|
||||
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
|
||||
import com.tencent.polaris.client.api.SDKContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.core.Ordered;
|
||||
|
||||
public class ExceptionCircuitBreakerReporter extends AbstractPolarisReporterAdapter implements EnhancedPlugin {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ExceptionCircuitBreakerReporter.class);
|
||||
|
||||
private final CircuitBreakAPI circuitBreakAPI;
|
||||
|
||||
public ExceptionCircuitBreakerReporter(RpcEnhancementReporterProperties reportProperties,
|
||||
SDKContext context,
|
||||
CircuitBreakAPI circuitBreakAPI) {
|
||||
super(reportProperties, context);
|
||||
this.circuitBreakAPI = circuitBreakAPI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return ExceptionCircuitBreakerReporter.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EnhancedPluginType getType() {
|
||||
return EnhancedPluginType.EXCEPTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(EnhancedPluginContext context) throws Throwable {
|
||||
if (!super.reportProperties.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
EnhancedRequestContext request = context.getRequest();
|
||||
ServiceInstance serviceInstance = Optional.ofNullable(context.getServiceInstance()).orElse(new DefaultServiceInstance());
|
||||
|
||||
ResourceStat resourceStat = createInstanceResourceStat(
|
||||
serviceInstance.getServiceId(),
|
||||
serviceInstance.getHost(),
|
||||
serviceInstance.getPort(),
|
||||
request.getUrl(),
|
||||
null,
|
||||
context.getDelay(),
|
||||
context.getThrowable()
|
||||
);
|
||||
|
||||
LOG.debug("Will report CircuitBreaker ResourceStat of {}. Request=[{} {}]. Response=[{}]. Delay=[{}]ms.",
|
||||
resourceStat.getRetStatus().name(), request.getHttpMethod().name(), request.getUrl().getPath(), context.getThrowable().getMessage(), context.getDelay());
|
||||
|
||||
circuitBreakAPI.report(resourceStat);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerThrowable(EnhancedPluginContext context, Throwable throwable) {
|
||||
LOG.error("ExceptionCircuitBreakerReporter runs failed. context=[{}].",
|
||||
context, throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.HIGHEST_PRECEDENCE + 2;
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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.circuitbreaker.reporter;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import com.tencent.cloud.rpc.enhancement.AbstractPolarisReporterAdapter;
|
||||
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPlugin;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.reporter.SuccessPolarisReporter;
|
||||
import com.tencent.polaris.api.plugin.circuitbreaker.ResourceStat;
|
||||
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
|
||||
import com.tencent.polaris.client.api.SDKContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.core.Ordered;
|
||||
|
||||
|
||||
public class SuccessCircuitBreakerReporter extends AbstractPolarisReporterAdapter implements EnhancedPlugin {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SuccessPolarisReporter.class);
|
||||
|
||||
private final CircuitBreakAPI circuitBreakAPI;
|
||||
|
||||
public SuccessCircuitBreakerReporter(RpcEnhancementReporterProperties reportProperties,
|
||||
SDKContext context,
|
||||
CircuitBreakAPI circuitBreakAPI) {
|
||||
super(reportProperties, context);
|
||||
this.circuitBreakAPI = circuitBreakAPI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return SuccessCircuitBreakerReporter.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EnhancedPluginType getType() {
|
||||
return EnhancedPluginType.POST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(EnhancedPluginContext context) throws Throwable {
|
||||
if (!super.reportProperties.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
EnhancedRequestContext request = context.getRequest();
|
||||
EnhancedResponseContext response = context.getResponse();
|
||||
ServiceInstance serviceInstance = Optional.ofNullable(context.getServiceInstance()).orElse(new DefaultServiceInstance());
|
||||
|
||||
ResourceStat resourceStat = createInstanceResourceStat(
|
||||
serviceInstance.getServiceId(),
|
||||
serviceInstance.getHost(),
|
||||
serviceInstance.getPort(),
|
||||
request.getUrl(),
|
||||
response.getHttpStatus(),
|
||||
context.getDelay(),
|
||||
null
|
||||
);
|
||||
|
||||
LOG.debug("Will report CircuitBreaker ResourceStat of {}. Request=[{} {}]. Response=[{}]. Delay=[{}]ms.",
|
||||
resourceStat.getRetStatus().name(), request.getHttpMethod().name(), request.getUrl().getPath(), response.getHttpStatus(), context.getDelay());
|
||||
|
||||
circuitBreakAPI.report(resourceStat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerThrowable(EnhancedPluginContext context, Throwable throwable) {
|
||||
LOG.error("SuccessCircuitBreakerReporter runs failed. context=[{}].",
|
||||
context, throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.HIGHEST_PRECEDENCE + 2;
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.circuitbreaker.resttemplate;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* PolarisCircuitBreaker annotation.
|
||||
* if coded fallback or fallbackClass provided, RestTemplate will always return fallback when any exception occurs,
|
||||
* if none coded fallback or fallbackClass provided, RestTemplate will return fallback response from Polaris server when fallback occurs.
|
||||
* fallback and fallbackClass cannot provide at same time.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
@Target({ ElementType.METHOD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface PolarisCircuitBreaker {
|
||||
|
||||
/**
|
||||
* a fallback string, will return a response { status: 200, body: fallback string} when any exception occurs.
|
||||
*
|
||||
* @return fallback string
|
||||
*/
|
||||
String fallback() default "";
|
||||
|
||||
/**
|
||||
* a fallback Class, will return a PolarisCircuitBreakerHttpResponse when any exception occurs.
|
||||
* fallback Class must be a spring bean.
|
||||
*
|
||||
* @return PolarisCircuitBreakerFallback
|
||||
*/
|
||||
Class<? extends PolarisCircuitBreakerFallback> fallbackClass() default PolarisCircuitBreakerFallback.class;
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.circuitbreaker.resttemplate;
|
||||
|
||||
/**
|
||||
* PolarisCircuitBreakerFallback.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
public interface PolarisCircuitBreakerFallback {
|
||||
|
||||
PolarisCircuitBreakerHttpResponse fallback();
|
||||
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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.circuitbreaker.resttemplate;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
|
||||
import com.tencent.polaris.api.pojo.CircuitBreakerStatus;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.client.AbstractClientHttpResponse;
|
||||
|
||||
/**
|
||||
* PolarisCircuitBreakerHttpResponse.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
public class PolarisCircuitBreakerHttpResponse extends AbstractClientHttpResponse {
|
||||
|
||||
private final CircuitBreakerStatus.FallbackInfo fallbackInfo;
|
||||
|
||||
private final HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
private InputStream body;
|
||||
|
||||
public PolarisCircuitBreakerHttpResponse(int code) {
|
||||
this(new CircuitBreakerStatus.FallbackInfo(code, null, null));
|
||||
}
|
||||
|
||||
public PolarisCircuitBreakerHttpResponse(int code, String body) {
|
||||
this(new CircuitBreakerStatus.FallbackInfo(code, null, body));
|
||||
}
|
||||
|
||||
public PolarisCircuitBreakerHttpResponse(int code, Map<String, String> headers, String body) {
|
||||
this(new CircuitBreakerStatus.FallbackInfo(code, headers, body));
|
||||
}
|
||||
|
||||
public PolarisCircuitBreakerHttpResponse(CircuitBreakerStatus.FallbackInfo fallbackInfo) {
|
||||
this.fallbackInfo = fallbackInfo;
|
||||
if (fallbackInfo.getHeaders() != null) {
|
||||
fallbackInfo.getHeaders().forEach(headers::add);
|
||||
}
|
||||
if (fallbackInfo.getBody() != null) {
|
||||
body = new ByteArrayInputStream(fallbackInfo.getBody().getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int getRawStatusCode() {
|
||||
return fallbackInfo.getCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getStatusText() {
|
||||
HttpStatus status = HttpStatus.resolve(getRawStatusCode());
|
||||
return (status != null ? status.getReasonPhrase() : "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void close() {
|
||||
if (this.body != null) {
|
||||
try {
|
||||
this.body.close();
|
||||
}
|
||||
catch (IOException e) {
|
||||
// Ignore exception on close...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final InputStream getBody() {
|
||||
return this.body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final HttpHeaders getHeaders() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
public CircuitBreakerStatus.FallbackInfo getFallbackInfo() {
|
||||
return this.fallbackInfo;
|
||||
}
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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.circuitbreaker.resttemplate;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.type.MethodMetadata;
|
||||
import org.springframework.core.type.StandardMethodMetadata;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* PolarisCircuitBreakerRestTemplateBeanPostProcessor.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
public class PolarisCircuitBreakerRestTemplateBeanPostProcessor implements MergedBeanDefinitionPostProcessor {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
public PolarisCircuitBreakerRestTemplateBeanPostProcessor(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
private final ConcurrentHashMap<String, PolarisCircuitBreaker> cache = new ConcurrentHashMap<>();
|
||||
|
||||
private void checkPolarisCircuitBreakerRestTemplate(PolarisCircuitBreaker polarisCircuitBreaker) {
|
||||
if (
|
||||
StringUtils.hasText(polarisCircuitBreaker.fallback()) &&
|
||||
!PolarisCircuitBreakerFallback.class.toGenericString().equals(polarisCircuitBreaker.fallbackClass().toGenericString())
|
||||
) {
|
||||
throw new IllegalArgumentException("PolarisCircuitBreaker's fallback and fallbackClass could not set at sametime !");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
|
||||
if (checkAnnotated(beanDefinition, beanType, beanName)) {
|
||||
PolarisCircuitBreaker polarisCircuitBreaker;
|
||||
if (beanDefinition.getSource() instanceof StandardMethodMetadata) {
|
||||
polarisCircuitBreaker = ((StandardMethodMetadata) beanDefinition.getSource()).getIntrospectedMethod()
|
||||
.getAnnotation(PolarisCircuitBreaker.class);
|
||||
}
|
||||
else {
|
||||
polarisCircuitBreaker = beanDefinition.getResolvedFactoryMethod()
|
||||
.getAnnotation(PolarisCircuitBreaker.class);
|
||||
}
|
||||
checkPolarisCircuitBreakerRestTemplate(polarisCircuitBreaker);
|
||||
cache.put(beanName, polarisCircuitBreaker);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (cache.containsKey(beanName)) {
|
||||
// add interceptor for each RestTemplate with @PolarisCircuitBreaker annotation
|
||||
StringBuilder interceptorBeanNamePrefix = new StringBuilder();
|
||||
PolarisCircuitBreaker polarisCircuitBreaker = cache.get(beanName);
|
||||
interceptorBeanNamePrefix
|
||||
.append(StringUtils.uncapitalize(
|
||||
PolarisCircuitBreaker.class.getSimpleName()))
|
||||
.append("_")
|
||||
.append(polarisCircuitBreaker.fallback())
|
||||
.append("_")
|
||||
.append(polarisCircuitBreaker.fallbackClass().getSimpleName());
|
||||
RestTemplate restTemplate = (RestTemplate) bean;
|
||||
String interceptorBeanName = interceptorBeanNamePrefix + "@" + bean;
|
||||
CircuitBreakerFactory circuitBreakerFactory = this.applicationContext.getBean(CircuitBreakerFactory.class);
|
||||
registerBean(interceptorBeanName, polarisCircuitBreaker, applicationContext, circuitBreakerFactory, restTemplate);
|
||||
PolarisCircuitBreakerRestTemplateInterceptor polarisCircuitBreakerRestTemplateInterceptor = applicationContext
|
||||
.getBean(interceptorBeanName, PolarisCircuitBreakerRestTemplateInterceptor.class);
|
||||
restTemplate.getInterceptors().add(0, polarisCircuitBreakerRestTemplateInterceptor);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
private boolean checkAnnotated(RootBeanDefinition beanDefinition,
|
||||
Class<?> beanType, String beanName) {
|
||||
return beanName != null && beanType == RestTemplate.class
|
||||
&& beanDefinition.getSource() instanceof MethodMetadata
|
||||
&& ((MethodMetadata) beanDefinition.getSource())
|
||||
.isAnnotated(PolarisCircuitBreaker.class.getName());
|
||||
}
|
||||
|
||||
private void registerBean(String interceptorBeanName, PolarisCircuitBreaker polarisCircuitBreaker,
|
||||
ApplicationContext applicationContext, CircuitBreakerFactory circuitBreakerFactory, RestTemplate restTemplate) {
|
||||
// register PolarisCircuitBreakerRestTemplateInterceptor bean
|
||||
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext
|
||||
.getAutowireCapableBeanFactory();
|
||||
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
|
||||
.genericBeanDefinition(PolarisCircuitBreakerRestTemplateInterceptor.class);
|
||||
beanDefinitionBuilder.addConstructorArgValue(polarisCircuitBreaker);
|
||||
beanDefinitionBuilder.addConstructorArgValue(applicationContext);
|
||||
beanDefinitionBuilder.addConstructorArgValue(circuitBreakerFactory);
|
||||
beanDefinitionBuilder.addConstructorArgValue(restTemplate);
|
||||
BeanDefinition interceptorBeanDefinition = beanDefinitionBuilder
|
||||
.getRawBeanDefinition();
|
||||
beanFactory.registerBeanDefinition(interceptorBeanName,
|
||||
interceptorBeanDefinition);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.circuitbreaker.resttemplate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import com.tencent.polaris.api.pojo.CircuitBreakerStatus;
|
||||
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.ResponseErrorHandler;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* PolarisCircuitBreakerRestTemplateInterceptor.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
public class PolarisCircuitBreakerRestTemplateInterceptor implements ClientHttpRequestInterceptor {
|
||||
|
||||
private final PolarisCircuitBreaker polarisCircuitBreaker;
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
private final CircuitBreakerFactory circuitBreakerFactory;
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
public PolarisCircuitBreakerRestTemplateInterceptor(
|
||||
PolarisCircuitBreaker polarisCircuitBreaker,
|
||||
ApplicationContext applicationContext,
|
||||
CircuitBreakerFactory circuitBreakerFactory,
|
||||
RestTemplate restTemplate
|
||||
) {
|
||||
this.polarisCircuitBreaker = polarisCircuitBreaker;
|
||||
this.applicationContext = applicationContext;
|
||||
this.circuitBreakerFactory = circuitBreakerFactory;
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
|
||||
return circuitBreakerFactory.create(request.getURI().getHost() + "#" + request.getURI().getPath()).run(
|
||||
() -> {
|
||||
try {
|
||||
ClientHttpResponse response = execution.execute(request, body);
|
||||
ResponseErrorHandler errorHandler = restTemplate.getErrorHandler();
|
||||
if (errorHandler.hasError(response)) {
|
||||
errorHandler.handleError(request.getURI(), request.getMethod(), response);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
},
|
||||
t -> {
|
||||
if (StringUtils.hasText(polarisCircuitBreaker.fallback())) {
|
||||
CircuitBreakerStatus.FallbackInfo fallbackInfo = new CircuitBreakerStatus.FallbackInfo(200, null, polarisCircuitBreaker.fallback());
|
||||
return new PolarisCircuitBreakerHttpResponse(fallbackInfo);
|
||||
}
|
||||
if (!PolarisCircuitBreakerFallback.class.toGenericString().equals(polarisCircuitBreaker.fallbackClass().toGenericString())) {
|
||||
Method method = ReflectionUtils.findMethod(PolarisCircuitBreakerFallback.class, "fallback");
|
||||
PolarisCircuitBreakerFallback polarisCircuitBreakerFallback = applicationContext.getBean(polarisCircuitBreaker.fallbackClass());
|
||||
return (PolarisCircuitBreakerHttpResponse) ReflectionUtils.invokeMethod(method, polarisCircuitBreakerFallback);
|
||||
}
|
||||
if (t instanceof CallAbortedException) {
|
||||
CircuitBreakerStatus.FallbackInfo fallbackInfo = ((CallAbortedException) t).getFallbackInfo();
|
||||
if (fallbackInfo != null) {
|
||||
return new PolarisCircuitBreakerHttpResponse(fallbackInfo);
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException(t);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.circuitbreaker.util;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import com.tencent.cloud.common.metadata.MetadataContext;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
|
||||
import com.tencent.polaris.api.core.ConsumerAPI;
|
||||
import com.tencent.polaris.api.pojo.RetStatus;
|
||||
import com.tencent.polaris.api.pojo.ServiceKey;
|
||||
import com.tencent.polaris.api.rpc.ServiceCallResult;
|
||||
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* PolarisCircuitBreakerUtils.
|
||||
*
|
||||
* @author seanyu 2023-02-27
|
||||
*/
|
||||
public final class PolarisCircuitBreakerUtils {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PolarisCircuitBreakerUtils.class);
|
||||
|
||||
private PolarisCircuitBreakerUtils() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param id CircuitBreakerId
|
||||
* Format: namespace#service#method or service#method or service ,
|
||||
* namespace set as default spring.cloud.polaris.namespace if absent
|
||||
* @return String[]{namespace, service, method}
|
||||
*/
|
||||
public static String[] resolveCircuitBreakerId(String id) {
|
||||
Assert.hasText(id, "A CircuitBreaker must have an id. Id could be : namespace#service#method or service#method or service");
|
||||
String[] polarisCircuitBreakerMetaData = id.split("#");
|
||||
if (polarisCircuitBreakerMetaData.length == 2) {
|
||||
return new String[]{MetadataContext.LOCAL_NAMESPACE, polarisCircuitBreakerMetaData[0], polarisCircuitBreakerMetaData[1]};
|
||||
}
|
||||
if (polarisCircuitBreakerMetaData.length == 3) {
|
||||
return new String[]{polarisCircuitBreakerMetaData[0], polarisCircuitBreakerMetaData[1], polarisCircuitBreakerMetaData[2]};
|
||||
}
|
||||
return new String[]{MetadataContext.LOCAL_NAMESPACE, id, ""};
|
||||
}
|
||||
|
||||
public static void reportStatus(ConsumerAPI consumerAPI,
|
||||
PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf, CallAbortedException e) {
|
||||
try {
|
||||
ServiceCallResult result = new ServiceCallResult();
|
||||
result.setMethod(conf.getMethod());
|
||||
result.setNamespace(conf.getNamespace());
|
||||
result.setService(conf.getService());
|
||||
result.setRuleName(e.getRuleName());
|
||||
result.setRetStatus(RetStatus.RetReject);
|
||||
result.setCallerService(new ServiceKey(conf.getSourceNamespace(), conf.getSourceService()));
|
||||
|
||||
if (Objects.nonNull(e.getFallbackInfo())) {
|
||||
result.setRetCode(e.getFallbackInfo().getCode());
|
||||
}
|
||||
consumerAPI.updateServiceCallResult(result);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
LOG.error("[CircuitBreaker] report circuitbreaker call result fail ", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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.circuitbreaker.zuul;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.netflix.zuul.ZuulFilter;
|
||||
import com.netflix.zuul.context.RequestContext;
|
||||
import com.netflix.zuul.exception.ZuulException;
|
||||
import com.tencent.cloud.common.util.ZuulFilterUtils;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreaker;
|
||||
import com.tencent.polaris.circuitbreak.api.pojo.InvokeContext;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.HttpStatusCodeException;
|
||||
|
||||
import static com.tencent.cloud.common.constant.ContextConstant.Zuul.POLARIS_CIRCUIT_BREAKER;
|
||||
import static com.tencent.cloud.common.constant.ContextConstant.Zuul.POLARIS_PRE_ROUTE_TIME;
|
||||
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.POST_TYPE;
|
||||
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SEND_RESPONSE_FILTER_ORDER;
|
||||
|
||||
/**
|
||||
* Polaris circuit breaker post-processing. Including reporting.
|
||||
*
|
||||
* @author Haotian Zhang
|
||||
*/
|
||||
public class PolarisCircuitBreakerPostZuulFilter extends ZuulFilter {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PolarisCircuitBreakerPostZuulFilter.class);
|
||||
|
||||
private final PolarisZuulFallbackFactory polarisZuulFallbackFactory;
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
public PolarisCircuitBreakerPostZuulFilter(PolarisZuulFallbackFactory polarisZuulFallbackFactory,
|
||||
Environment environment) {
|
||||
this.polarisZuulFallbackFactory = polarisZuulFallbackFactory;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String filterType() {
|
||||
return POST_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int filterOrder() {
|
||||
return SEND_RESPONSE_FILTER_ORDER - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldFilter() {
|
||||
String enabled = environment.getProperty("spring.cloud.polaris.circuitbreaker.enabled");
|
||||
return StringUtils.isEmpty(enabled) || enabled.equals("true");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object run() throws ZuulException {
|
||||
RequestContext context = RequestContext.getCurrentContext();
|
||||
|
||||
HttpServletResponse response = context.getResponse();
|
||||
HttpStatus status = HttpStatus.resolve(response.getStatus());
|
||||
|
||||
Object polarisCircuitBreakerObject = context.get(POLARIS_CIRCUIT_BREAKER);
|
||||
Object startTimeMilliObject = context.get(POLARIS_PRE_ROUTE_TIME);
|
||||
if (polarisCircuitBreakerObject != null && polarisCircuitBreakerObject instanceof PolarisCircuitBreaker
|
||||
&& startTimeMilliObject != null && startTimeMilliObject instanceof Long) {
|
||||
PolarisCircuitBreaker polarisCircuitBreaker = (PolarisCircuitBreaker) polarisCircuitBreakerObject;
|
||||
Long startTimeMilli = (Long) startTimeMilliObject;
|
||||
long delay = System.currentTimeMillis() - startTimeMilli;
|
||||
InvokeContext.ResponseContext responseContext = new InvokeContext.ResponseContext();
|
||||
responseContext.setDuration(delay);
|
||||
responseContext.setDurationUnit(TimeUnit.MILLISECONDS);
|
||||
|
||||
if (status != null && status.is5xxServerError()) {
|
||||
Throwable throwable = new CircuitBreakerStatusCodeException(status);
|
||||
responseContext.setError(throwable);
|
||||
|
||||
// fallback if FallbackProvider is implemented.
|
||||
String serviceId = ZuulFilterUtils.getServiceId(context);
|
||||
FallbackProvider fallbackProvider = polarisZuulFallbackFactory.getFallbackProvider(serviceId);
|
||||
if (fallbackProvider != null) {
|
||||
ClientHttpResponse clientHttpResponse = fallbackProvider.fallbackResponse(serviceId, throwable);
|
||||
try {
|
||||
// set status code
|
||||
context.setResponseStatusCode(clientHttpResponse.getRawStatusCode());
|
||||
// set headers
|
||||
HttpHeaders headers = clientHttpResponse.getHeaders();
|
||||
for (String key : headers.keySet()) {
|
||||
List<String> values = headers.get(key);
|
||||
if (!CollectionUtils.isEmpty(values)) {
|
||||
for (String value : values) {
|
||||
context.addZuulResponseHeader(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
// set body
|
||||
context.getResponse().setCharacterEncoding("UTF-8");
|
||||
context.setResponseBody(IOUtils.toString(clientHttpResponse.getBody(), StandardCharsets.UTF_8));
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOGGER.error("error filter exception", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (responseContext.getError() == null) {
|
||||
polarisCircuitBreaker.onSuccess(responseContext);
|
||||
}
|
||||
else {
|
||||
polarisCircuitBreaker.onError(responseContext);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public class CircuitBreakerStatusCodeException extends HttpStatusCodeException {
|
||||
|
||||
public CircuitBreakerStatusCodeException(HttpStatus statusCode) {
|
||||
super(statusCode);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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.circuitbreaker.zuul;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import com.netflix.zuul.ZuulFilter;
|
||||
import com.netflix.zuul.context.RequestContext;
|
||||
import com.netflix.zuul.exception.ZuulException;
|
||||
import com.tencent.cloud.common.metadata.MetadataContext;
|
||||
import com.tencent.cloud.common.util.ZuulFilterUtils;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreaker;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreakerHttpResponse;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.util.PolarisCircuitBreakerUtils;
|
||||
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static com.tencent.cloud.common.constant.ContextConstant.Zuul.POLARIS_CIRCUIT_BREAKER;
|
||||
import static com.tencent.cloud.common.constant.ContextConstant.Zuul.POLARIS_IS_IN_ROUTING_STATE;
|
||||
import static com.tencent.cloud.common.constant.ContextConstant.Zuul.POLARIS_PRE_ROUTE_TIME;
|
||||
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_DECORATION_FILTER_ORDER;
|
||||
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
|
||||
|
||||
/**
|
||||
* Polaris circuit breaker implement in Zuul.
|
||||
*
|
||||
* @author Haotian Zhang
|
||||
*/
|
||||
public class PolarisCircuitBreakerZuulFilter extends ZuulFilter {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PolarisCircuitBreakerZuulFilter.class);
|
||||
|
||||
private final CircuitBreakerFactory circuitBreakerFactory;
|
||||
|
||||
private final PolarisZuulFallbackFactory polarisZuulFallbackFactory;
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
public PolarisCircuitBreakerZuulFilter(
|
||||
CircuitBreakerFactory circuitBreakerFactory,
|
||||
PolarisZuulFallbackFactory polarisZuulFallbackFactory,
|
||||
Environment environment) {
|
||||
this.circuitBreakerFactory = circuitBreakerFactory;
|
||||
this.polarisZuulFallbackFactory = polarisZuulFallbackFactory;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String filterType() {
|
||||
return PRE_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceId is set after PreDecorationFilter.
|
||||
*
|
||||
* @return filter order
|
||||
*/
|
||||
@Override
|
||||
public int filterOrder() {
|
||||
return PRE_DECORATION_FILTER_ORDER + 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldFilter() {
|
||||
String enabled = environment.getProperty("spring.cloud.polaris.circuitbreaker.enabled");
|
||||
return StringUtils.isEmpty(enabled) || enabled.equals("true");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object run() throws ZuulException {
|
||||
RequestContext context = RequestContext.getCurrentContext();
|
||||
String serviceId = ZuulFilterUtils.getServiceId(context);
|
||||
String path = ZuulFilterUtils.getPath(context);
|
||||
String circuitName = "".equals(path) ?
|
||||
MetadataContext.LOCAL_NAMESPACE + "#" + serviceId :
|
||||
MetadataContext.LOCAL_NAMESPACE + "#" + serviceId + "#" + path;
|
||||
CircuitBreaker circuitBreaker = circuitBreakerFactory.create(circuitName);
|
||||
if (circuitBreaker instanceof PolarisCircuitBreaker) {
|
||||
PolarisCircuitBreaker polarisCircuitBreaker = (PolarisCircuitBreaker) circuitBreaker;
|
||||
context.set(POLARIS_CIRCUIT_BREAKER, circuitBreaker);
|
||||
try {
|
||||
polarisCircuitBreaker.acquirePermission();
|
||||
}
|
||||
catch (CallAbortedException exception) {
|
||||
FallbackProvider fallbackProvider = polarisZuulFallbackFactory.getFallbackProvider(serviceId);
|
||||
ClientHttpResponse clientHttpResponse;
|
||||
if (fallbackProvider != null) {
|
||||
clientHttpResponse = fallbackProvider.fallbackResponse(serviceId, exception);
|
||||
}
|
||||
else if (exception.getFallbackInfo() != null) {
|
||||
clientHttpResponse = new PolarisCircuitBreakerHttpResponse(exception.getFallbackInfo());
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
try {
|
||||
context.setSendZuulResponse(false);
|
||||
// set status code
|
||||
context.setResponseStatusCode(clientHttpResponse.getRawStatusCode());
|
||||
// set headers
|
||||
HttpHeaders headers = clientHttpResponse.getHeaders();
|
||||
for (String key : headers.keySet()) {
|
||||
List<String> values = headers.get(key);
|
||||
if (!CollectionUtils.isEmpty(values)) {
|
||||
for (String value : values) {
|
||||
context.addZuulResponseHeader(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
// set body
|
||||
context.getResponse().setCharacterEncoding("UTF-8");
|
||||
context.setResponseBody(IOUtils.toString(clientHttpResponse.getBody(), StandardCharsets.UTF_8));
|
||||
LOGGER.debug("PolarisCircuitBreaker CallAbortedException: {}", exception.getMessage());
|
||||
PolarisCircuitBreakerUtils.reportStatus(polarisCircuitBreaker.getConsumerAPI(), polarisCircuitBreaker.getConf(), exception);
|
||||
}
|
||||
catch (IOException e) {
|
||||
LOGGER.error("Return circuit breaker fallback info failed: {}", e.getMessage());
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
context.set(POLARIS_PRE_ROUTE_TIME, Long.valueOf(System.currentTimeMillis()));
|
||||
context.set(POLARIS_IS_IN_ROUTING_STATE, true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.circuitbreaker.zuul;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
|
||||
|
||||
/**
|
||||
* Zuul fallback factory.
|
||||
*
|
||||
* @author Haotian Zhang
|
||||
*/
|
||||
public class PolarisZuulFallbackFactory {
|
||||
private final Map<String, FallbackProvider> fallbackProviderCache;
|
||||
|
||||
private FallbackProvider defaultFallbackProvider = null;
|
||||
|
||||
public PolarisZuulFallbackFactory(Set<FallbackProvider> fallbackProviders) {
|
||||
this.fallbackProviderCache = new HashMap<>();
|
||||
for (FallbackProvider provider : fallbackProviders) {
|
||||
String route = provider.getRoute();
|
||||
if ("*".equals(route) || route == null) {
|
||||
defaultFallbackProvider = provider;
|
||||
}
|
||||
else {
|
||||
fallbackProviderCache.put(route, provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FallbackProvider getFallbackProvider(String route) {
|
||||
FallbackProvider provider = fallbackProviderCache.get(route);
|
||||
if (provider == null) {
|
||||
provider = defaultFallbackProvider;
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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 org.springframework.cloud.openfeign;
|
||||
|
||||
import com.tencent.cloud.polaris.circuitbreaker.feign.PolarisCircuitBreakerNameResolver;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.feign.PolarisFeignCircuitBreaker;
|
||||
import feign.Feign;
|
||||
import feign.Target;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* PolarisFeignCircuitBreakerTargeter, mostly copy from {@link org.springframework.cloud.openfeign.FeignCircuitBreakerTargeter}, but giving Polaris modification.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
public class PolarisFeignCircuitBreakerTargeter implements Targeter {
|
||||
|
||||
private final CircuitBreakerFactory circuitBreakerFactory;
|
||||
|
||||
private final PolarisCircuitBreakerNameResolver circuitBreakerNameResolver;
|
||||
|
||||
public PolarisFeignCircuitBreakerTargeter(CircuitBreakerFactory circuitBreakerFactory, PolarisCircuitBreakerNameResolver circuitBreakerNameResolver) {
|
||||
this.circuitBreakerFactory = circuitBreakerFactory;
|
||||
this.circuitBreakerNameResolver = circuitBreakerNameResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
|
||||
Target.HardCodedTarget<T> target) {
|
||||
if (!(feign instanceof PolarisFeignCircuitBreaker.Builder)) {
|
||||
return feign.target(target);
|
||||
}
|
||||
PolarisFeignCircuitBreaker.Builder builder = (PolarisFeignCircuitBreaker.Builder) feign;
|
||||
String name = !StringUtils.hasText(factory.getContextId()) ? factory.getName() : factory.getContextId();
|
||||
Class<?> fallback = factory.getFallback();
|
||||
if (fallback != void.class) {
|
||||
return targetWithFallback(name, context, target, builder, fallback);
|
||||
}
|
||||
Class<?> fallbackFactory = factory.getFallbackFactory();
|
||||
if (fallbackFactory != void.class) {
|
||||
return targetWithFallbackFactory(name, context, target, builder, fallbackFactory);
|
||||
}
|
||||
return builder(name, builder).target(target);
|
||||
}
|
||||
|
||||
private <T> T targetWithFallbackFactory(String feignClientName, FeignContext context,
|
||||
Target.HardCodedTarget<T> target, PolarisFeignCircuitBreaker.Builder builder, Class<?> fallbackFactoryClass) {
|
||||
FallbackFactory<? extends T> fallbackFactory = (FallbackFactory<? extends T>) getFromContext("fallbackFactory",
|
||||
feignClientName, context, fallbackFactoryClass, FallbackFactory.class);
|
||||
return builder(feignClientName, builder).target(target, fallbackFactory);
|
||||
}
|
||||
|
||||
private <T> T targetWithFallback(String feignClientName, FeignContext context, Target.HardCodedTarget<T> target,
|
||||
PolarisFeignCircuitBreaker.Builder builder, Class<?> fallback) {
|
||||
T fallbackInstance = getFromContext("fallback", feignClientName, context, fallback, target.type());
|
||||
return builder(feignClientName, builder).target(target, fallbackInstance);
|
||||
}
|
||||
|
||||
private <T> T getFromContext(String fallbackMechanism, String feignClientName, FeignContext context,
|
||||
Class<?> beanType, Class<T> targetType) {
|
||||
Object fallbackInstance = context.getInstance(feignClientName, beanType);
|
||||
if (fallbackInstance == null) {
|
||||
throw new IllegalStateException(
|
||||
String.format("No " + fallbackMechanism + " instance of type %s found for feign client %s",
|
||||
beanType, feignClientName));
|
||||
}
|
||||
|
||||
if (!targetType.isAssignableFrom(beanType)) {
|
||||
throw new IllegalStateException(String.format("Incompatible " + fallbackMechanism
|
||||
+ " instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s",
|
||||
beanType, targetType, feignClientName));
|
||||
}
|
||||
return (T) fallbackInstance;
|
||||
}
|
||||
|
||||
private PolarisFeignCircuitBreaker.Builder builder(String feignClientName, PolarisFeignCircuitBreaker.Builder builder) {
|
||||
return builder
|
||||
.circuitBreakerFactory(circuitBreakerFactory)
|
||||
.feignClientName(feignClientName)
|
||||
.circuitBreakerNameResolver(circuitBreakerNameResolver);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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 org.springframework.cloud.openfeign;
|
||||
|
||||
import com.tencent.cloud.polaris.circuitbreaker.config.ConditionalOnPolarisCircuitBreakerEnabled;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.feign.PolarisCircuitBreakerNameResolver;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
/**
|
||||
* Auto configuration for PolarisFeignCircuitBreakerTargeter.
|
||||
*
|
||||
* @author Haotian Zhang
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass({Targeter.class})
|
||||
@ConditionalOnProperty(value = "feign.circuitbreaker.enabled", havingValue = "true")
|
||||
@AutoConfigureBefore(FeignAutoConfiguration.class)
|
||||
@ConditionalOnPolarisCircuitBreakerEnabled
|
||||
public class PolarisFeignCircuitBreakerTargeterAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
@ConditionalOnBean(CircuitBreakerFactory.class)
|
||||
public Targeter polarisFeignCircuitBreakerTargeter(CircuitBreakerFactory circuitBreakerFactory, PolarisCircuitBreakerNameResolver circuitBreakerNameResolver) {
|
||||
return new PolarisFeignCircuitBreakerTargeter(circuitBreakerFactory, circuitBreakerNameResolver);
|
||||
}
|
||||
}
|
@ -1,4 +1,8 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerAutoConfiguration
|
||||
com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerAutoConfiguration,\
|
||||
com.tencent.cloud.polaris.circuitbreaker.config.ReactivePolarisCircuitBreakerAutoConfiguration,\
|
||||
com.tencent.cloud.polaris.circuitbreaker.config.GatewayPolarisCircuitBreakerAutoConfiguration,\
|
||||
com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerFeignClientAutoConfiguration,\
|
||||
org.springframework.cloud.openfeign.PolarisFeignCircuitBreakerTargeterAutoConfiguration
|
||||
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
|
||||
com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerBootstrapConfiguration
|
||||
|
38
spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerAutoConfigurationTest.java → spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/CircuitBreakerPolarisAutoConfigurationTest.java
38
spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerAutoConfigurationTest.java → spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/CircuitBreakerPolarisAutoConfigurationTest.java
15
spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerBootstrapConfigurationTest.java → spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerBootstrapConfigurationTest.java
15
spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerBootstrapConfigurationTest.java → spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/PolarisCircuitBreakerBootstrapConfigurationTest.java
@ -0,0 +1,234 @@
|
||||
/*
|
||||
* 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.circuitbreaker;
|
||||
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import com.google.protobuf.util.JsonFormat;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerFeignClientAutoConfiguration;
|
||||
import com.tencent.polaris.api.pojo.ServiceKey;
|
||||
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
|
||||
import com.tencent.polaris.circuitbreak.factory.CircuitBreakAPIFactory;
|
||||
import com.tencent.polaris.client.util.Utils;
|
||||
import com.tencent.polaris.specification.api.v1.fault.tolerance.CircuitBreakerProto;
|
||||
import com.tencent.polaris.test.common.TestUtils;
|
||||
import com.tencent.polaris.test.mock.discovery.NamingServer;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FallbackFactory;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import static com.tencent.polaris.test.common.TestUtils.SERVER_ADDRESS_ENV;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
||||
|
||||
/**
|
||||
* @author sean yu
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@SpringBootTest(webEnvironment = RANDOM_PORT,
|
||||
classes = PolarisCircuitBreakerFeignIntegrationTest.TestConfig.class,
|
||||
properties = {
|
||||
"spring.cloud.gateway.enabled=false",
|
||||
"feign.circuitbreaker.enabled=true",
|
||||
"spring.cloud.polaris.namespace=default",
|
||||
"spring.cloud.polaris.service=Test",
|
||||
"spring.cloud.polaris.address=grpc://127.0.0.1:10081"
|
||||
})
|
||||
@DirtiesContext
|
||||
public class PolarisCircuitBreakerFeignIntegrationTest {
|
||||
|
||||
private static final String TEST_SERVICE_NAME = "test-service-callee";
|
||||
private static NamingServer namingServer;
|
||||
@Autowired
|
||||
private EchoService echoService;
|
||||
@Autowired
|
||||
private FooService fooService;
|
||||
@Autowired
|
||||
private BarService barService;
|
||||
@Autowired
|
||||
private BazService bazService;
|
||||
|
||||
@AfterAll
|
||||
public static void afterAll() {
|
||||
if (null != namingServer) {
|
||||
namingServer.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contextLoads() throws Exception {
|
||||
assertThat(echoService).isNotNull();
|
||||
assertThat(fooService).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFeignClient() throws InvocationTargetException {
|
||||
assertThat(echoService.echo("test")).isEqualTo("echo fallback");
|
||||
Utils.sleepUninterrupted(2000);
|
||||
assertThatThrownBy(() -> {
|
||||
echoService.echo(null);
|
||||
}).isInstanceOf(Exception.class);
|
||||
assertThatThrownBy(() -> {
|
||||
fooService.echo("test");
|
||||
}).isInstanceOf(NoFallbackAvailableException.class);
|
||||
Utils.sleepUninterrupted(2000);
|
||||
assertThat(barService.bar()).isEqualTo("\"fallback from polaris server\"");
|
||||
Utils.sleepUninterrupted(2000);
|
||||
assertThat(bazService.baz()).isEqualTo("\"fallback from polaris server\"");
|
||||
assertThat(fooService.toString()).isNotEqualTo(echoService.toString());
|
||||
assertThat(fooService.hashCode()).isNotEqualTo(echoService.hashCode());
|
||||
assertThat(echoService.equals(fooService)).isEqualTo(Boolean.FALSE);
|
||||
}
|
||||
|
||||
@FeignClient(value = TEST_SERVICE_NAME, contextId = "1", fallback = EchoServiceFallback.class)
|
||||
public interface EchoService {
|
||||
|
||||
@RequestMapping(path = "echo/{str}")
|
||||
String echo(@RequestParam("str") String param) throws InvocationTargetException;
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(value = TEST_SERVICE_NAME, contextId = "2", fallbackFactory = CustomFallbackFactory.class)
|
||||
public interface FooService {
|
||||
|
||||
@RequestMapping("echo/{str}")
|
||||
String echo(@RequestParam("str") String param);
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(value = TEST_SERVICE_NAME, contextId = "3")
|
||||
public interface BarService {
|
||||
|
||||
@RequestMapping(path = "bar")
|
||||
String bar();
|
||||
|
||||
}
|
||||
|
||||
public interface BazService {
|
||||
|
||||
@RequestMapping(path = "baz")
|
||||
String baz();
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(value = TEST_SERVICE_NAME, contextId = "4")
|
||||
public interface BazClient extends BazService {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
@ImportAutoConfiguration({PolarisCircuitBreakerFeignClientAutoConfiguration.class})
|
||||
@EnableFeignClients
|
||||
public static class TestConfig {
|
||||
|
||||
@Bean
|
||||
public EchoServiceFallback echoServiceFallback() {
|
||||
return new EchoServiceFallback();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CustomFallbackFactory customFallbackFactory() {
|
||||
return new CustomFallbackFactory();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CircuitBreakAPI circuitBreakAPI() throws InvalidProtocolBufferException {
|
||||
try {
|
||||
namingServer = NamingServer.startNamingServer(10081);
|
||||
System.setProperty(SERVER_ADDRESS_ENV, String.format("127.0.0.1:%d", namingServer.getPort()));
|
||||
}
|
||||
catch (IOException e) {
|
||||
|
||||
}
|
||||
ServiceKey serviceKey = new ServiceKey("default", TEST_SERVICE_NAME);
|
||||
|
||||
CircuitBreakerProto.CircuitBreakerRule.Builder circuitBreakerRuleBuilder = CircuitBreakerProto.CircuitBreakerRule.newBuilder();
|
||||
InputStream inputStream = PolarisCircuitBreakerMockServerTest.class.getClassLoader()
|
||||
.getResourceAsStream("circuitBreakerRule.json");
|
||||
String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines()
|
||||
.collect(Collectors.joining(""));
|
||||
JsonFormat.parser().ignoringUnknownFields().merge(json, circuitBreakerRuleBuilder);
|
||||
CircuitBreakerProto.CircuitBreakerRule circuitBreakerRule = circuitBreakerRuleBuilder.build();
|
||||
CircuitBreakerProto.CircuitBreaker circuitBreaker = CircuitBreakerProto.CircuitBreaker.newBuilder()
|
||||
.addRules(circuitBreakerRule).build();
|
||||
namingServer.getNamingService().setCircuitBreaker(serviceKey, circuitBreaker);
|
||||
com.tencent.polaris.api.config.Configuration configuration = TestUtils.configWithEnvAddress();
|
||||
return CircuitBreakAPIFactory.createCircuitBreakAPIByConfig(configuration);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class EchoServiceFallback implements EchoService {
|
||||
|
||||
@Override
|
||||
public String echo(@RequestParam("str") String param) throws InvocationTargetException {
|
||||
if (param == null) {
|
||||
throw new InvocationTargetException(new Exception());
|
||||
}
|
||||
return "echo fallback";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class FooServiceFallback implements FooService {
|
||||
|
||||
@Override
|
||||
public String echo(@RequestParam("str") String param) {
|
||||
throw new NoFallbackAvailableException("fallback", new RuntimeException());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class CustomFallbackFactory
|
||||
implements FallbackFactory<FooService> {
|
||||
|
||||
private final FooService fooService = new FooServiceFallback();
|
||||
|
||||
@Override
|
||||
public FooService create(Throwable throwable) {
|
||||
return fooService;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,231 @@
|
||||
/*
|
||||
* 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.circuitbreaker;
|
||||
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import com.google.protobuf.util.JsonFormat;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.gateway.PolarisCircuitBreakerFilterFactory;
|
||||
import com.tencent.polaris.api.pojo.ServiceKey;
|
||||
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
|
||||
import com.tencent.polaris.circuitbreak.factory.CircuitBreakAPIFactory;
|
||||
import com.tencent.polaris.client.util.Utils;
|
||||
import com.tencent.polaris.specification.api.v1.fault.tolerance.CircuitBreakerProto;
|
||||
import com.tencent.polaris.test.common.TestUtils;
|
||||
import com.tencent.polaris.test.mock.discovery.NamingServer;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
|
||||
import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerFilterFactory;
|
||||
import org.springframework.cloud.gateway.route.RouteLocator;
|
||||
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static com.tencent.polaris.test.common.TestUtils.SERVER_ADDRESS_ENV;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/**
|
||||
* @author sean yu
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
properties = {
|
||||
"spring.cloud.gateway.enabled=true",
|
||||
"spring.cloud.polaris.namespace=default",
|
||||
"spring.cloud.polaris.service=Test",
|
||||
"spring.main.web-application-type=reactive",
|
||||
"httpbin=http://localhost:${wiremock.server.port}",
|
||||
"spring.cloud.polaris.address=grpc://127.0.0.1:10081"
|
||||
},
|
||||
classes = PolarisCircuitBreakerGatewayIntegrationTest.TestApplication.class
|
||||
)
|
||||
@AutoConfigureWireMock(port = 0)
|
||||
@ActiveProfiles("test-gateway")
|
||||
@AutoConfigureWebTestClient(timeout = "1000000")
|
||||
public class PolarisCircuitBreakerGatewayIntegrationTest {
|
||||
|
||||
private static final String TEST_SERVICE_NAME = "test-service-callee";
|
||||
private static NamingServer namingServer;
|
||||
@Autowired
|
||||
private WebTestClient webClient;
|
||||
@Autowired
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@AfterAll
|
||||
public static void afterAll() {
|
||||
if (null != namingServer) {
|
||||
namingServer.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fallback() throws Exception {
|
||||
SpringCloudCircuitBreakerFilterFactory.Config config = new SpringCloudCircuitBreakerFilterFactory.Config();
|
||||
applicationContext.getBean(PolarisCircuitBreakerFilterFactory.class).apply(config).toString();
|
||||
|
||||
webClient
|
||||
.get().uri("/err")
|
||||
.header("Host", "www.circuitbreaker.com")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.consumeWith(
|
||||
response -> assertThat(response.getResponseBody()).isEqualTo("fallback".getBytes()));
|
||||
|
||||
Utils.sleepUninterrupted(2000);
|
||||
|
||||
webClient
|
||||
.get().uri("/err-skip-fallback")
|
||||
.header("Host", "www.circuitbreaker-skip-fallback.com")
|
||||
.exchange()
|
||||
.expectStatus();
|
||||
|
||||
Utils.sleepUninterrupted(2000);
|
||||
|
||||
// this should be 200, but for some unknown reason, GitHub action run failed in windows, so we skip this check
|
||||
webClient
|
||||
.get().uri("/err-skip-fallback")
|
||||
.header("Host", "www.circuitbreaker-skip-fallback.com")
|
||||
.exchange()
|
||||
.expectStatus();
|
||||
|
||||
Utils.sleepUninterrupted(2000);
|
||||
|
||||
webClient
|
||||
.get().uri("/err-no-fallback")
|
||||
.header("Host", "www.circuitbreaker-no-fallback.com")
|
||||
.exchange()
|
||||
.expectStatus();
|
||||
|
||||
Utils.sleepUninterrupted(2000);
|
||||
|
||||
webClient
|
||||
.get().uri("/err-no-fallback")
|
||||
.header("Host", "www.circuitbreaker-no-fallback.com")
|
||||
.exchange()
|
||||
.expectStatus();
|
||||
|
||||
Utils.sleepUninterrupted(2000);
|
||||
|
||||
webClient
|
||||
.get().uri("/err-no-fallback")
|
||||
.header("Host", "www.circuitbreaker-no-fallback.com")
|
||||
.exchange()
|
||||
.expectStatus();
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
public static class TestApplication {
|
||||
|
||||
@Bean
|
||||
public CircuitBreakAPI circuitBreakAPI() throws InvalidProtocolBufferException {
|
||||
try {
|
||||
namingServer = NamingServer.startNamingServer(10081);
|
||||
System.setProperty(SERVER_ADDRESS_ENV, String.format("127.0.0.1:%d", namingServer.getPort()));
|
||||
}
|
||||
catch (IOException e) {
|
||||
|
||||
}
|
||||
ServiceKey serviceKey = new ServiceKey("default", TEST_SERVICE_NAME);
|
||||
|
||||
CircuitBreakerProto.CircuitBreakerRule.Builder circuitBreakerRuleBuilder = CircuitBreakerProto.CircuitBreakerRule.newBuilder();
|
||||
InputStream inputStream = PolarisCircuitBreakerMockServerTest.class.getClassLoader()
|
||||
.getResourceAsStream("circuitBreakerRule.json");
|
||||
String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines()
|
||||
.collect(Collectors.joining(""));
|
||||
JsonFormat.parser().ignoringUnknownFields().merge(json, circuitBreakerRuleBuilder);
|
||||
CircuitBreakerProto.CircuitBreakerRule circuitBreakerRule = circuitBreakerRuleBuilder.build();
|
||||
CircuitBreakerProto.CircuitBreaker circuitBreaker = CircuitBreakerProto.CircuitBreaker.newBuilder()
|
||||
.addRules(circuitBreakerRule).build();
|
||||
namingServer.getNamingService().setCircuitBreaker(serviceKey, circuitBreaker);
|
||||
com.tencent.polaris.api.config.Configuration configuration = TestUtils.configWithEnvAddress();
|
||||
return CircuitBreakAPIFactory.createCircuitBreakAPIByConfig(configuration);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
|
||||
Set<String> codeSets = new HashSet<>();
|
||||
codeSets.add("4**");
|
||||
codeSets.add("5**");
|
||||
return builder.routes()
|
||||
.route(p -> p
|
||||
.host("*.circuitbreaker.com")
|
||||
.filters(f -> f
|
||||
.circuitBreaker(config -> config
|
||||
.setStatusCodes(codeSets)
|
||||
.setFallbackUri("forward:/fallback")
|
||||
.setName(TEST_SERVICE_NAME)
|
||||
))
|
||||
.uri("http://httpbin.org:80"))
|
||||
.route(p -> p
|
||||
.host("*.circuitbreaker-skip-fallback.com")
|
||||
.filters(f -> f
|
||||
.circuitBreaker(config -> config
|
||||
.setStatusCodes(Collections.singleton("5**"))
|
||||
.setName(TEST_SERVICE_NAME)
|
||||
))
|
||||
.uri("http://httpbin.org:80"))
|
||||
.route(p -> p
|
||||
.host("*.circuitbreaker-no-fallback.com")
|
||||
.filters(f -> f
|
||||
.circuitBreaker(config -> config
|
||||
.setName(TEST_SERVICE_NAME)
|
||||
))
|
||||
.uri("lb://" + TEST_SERVICE_NAME))
|
||||
.build();
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class Controller {
|
||||
@RequestMapping("/fallback")
|
||||
public Mono<String> fallback() {
|
||||
return Mono.just("fallback");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,309 @@
|
||||
/*
|
||||
* 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.circuitbreaker;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import com.google.protobuf.util.JsonFormat;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerFeignClientAutoConfiguration;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreaker;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreakerFallback;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.resttemplate.PolarisCircuitBreakerHttpResponse;
|
||||
import com.tencent.polaris.api.pojo.ServiceKey;
|
||||
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
|
||||
import com.tencent.polaris.circuitbreak.factory.CircuitBreakAPIFactory;
|
||||
import com.tencent.polaris.client.util.Utils;
|
||||
import com.tencent.polaris.specification.api.v1.fault.tolerance.CircuitBreakerProto;
|
||||
import com.tencent.polaris.test.common.TestUtils;
|
||||
import com.tencent.polaris.test.mock.discovery.NamingServer;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.client.ExpectedCount;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
|
||||
import static com.tencent.polaris.test.common.TestUtils.SERVER_ADDRESS_ENV;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
|
||||
|
||||
/**
|
||||
* @author sean yu
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@SpringBootTest(webEnvironment = RANDOM_PORT,
|
||||
classes = PolarisCircuitBreakerIntegrationTest.TestConfig.class,
|
||||
properties = {
|
||||
"spring.cloud.gateway.enabled=false",
|
||||
"feign.circuitbreaker.enabled=true",
|
||||
"spring.cloud.polaris.namespace=default",
|
||||
"spring.cloud.polaris.service=test",
|
||||
"spring.cloud.polaris.address=grpc://127.0.0.1:10081"
|
||||
})
|
||||
@DirtiesContext
|
||||
public class PolarisCircuitBreakerIntegrationTest {
|
||||
|
||||
private static final String TEST_SERVICE_NAME = "test-service-callee";
|
||||
|
||||
private static NamingServer namingServer;
|
||||
@Autowired
|
||||
@Qualifier("defaultRestTemplate")
|
||||
private RestTemplate defaultRestTemplate;
|
||||
@Autowired
|
||||
@Qualifier("restTemplateFallbackFromPolaris")
|
||||
private RestTemplate restTemplateFallbackFromPolaris;
|
||||
@Autowired
|
||||
@Qualifier("restTemplateFallbackFromCode")
|
||||
private RestTemplate restTemplateFallbackFromCode;
|
||||
@Autowired
|
||||
@Qualifier("restTemplateFallbackFromCode2")
|
||||
private RestTemplate restTemplateFallbackFromCode2;
|
||||
@Autowired
|
||||
@Qualifier("restTemplateFallbackFromCode3")
|
||||
private RestTemplate restTemplateFallbackFromCode3;
|
||||
@Autowired
|
||||
@Qualifier("restTemplateFallbackFromCode4")
|
||||
private RestTemplate restTemplateFallbackFromCode4;
|
||||
@Autowired
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@AfterAll
|
||||
public static void afterAll() {
|
||||
if (null != namingServer) {
|
||||
namingServer.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestTemplate() throws URISyntaxException {
|
||||
MockRestServiceServer mockServer = MockRestServiceServer.createServer(defaultRestTemplate);
|
||||
mockServer
|
||||
.expect(ExpectedCount.once(), requestTo(new URI("http://localhost:18001/example/service/b/info")))
|
||||
.andExpect(method(HttpMethod.GET))
|
||||
.andRespond(withStatus(HttpStatus.OK).body("OK"));
|
||||
assertThat(defaultRestTemplate.getForObject("http://localhost:18001/example/service/b/info", String.class)).isEqualTo("OK");
|
||||
mockServer.verify();
|
||||
mockServer.reset();
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
mockServer
|
||||
.expect(ExpectedCount.once(), requestTo(new URI("http://localhost:18001/example/service/b/info")))
|
||||
.andExpect(method(HttpMethod.GET))
|
||||
.andRespond(withStatus(HttpStatus.BAD_GATEWAY).headers(headers).body("BAD_GATEWAY"));
|
||||
assertThat(defaultRestTemplate.getForObject("http://localhost:18001/example/service/b/info", String.class)).isEqualTo("fallback");
|
||||
mockServer.verify();
|
||||
mockServer.reset();
|
||||
assertThat(restTemplateFallbackFromCode.getForObject("/example/service/b/info", String.class)).isEqualTo("\"this is a fallback class\"");
|
||||
Utils.sleepUninterrupted(2000);
|
||||
assertThat(restTemplateFallbackFromCode2.getForObject("/example/service/b/info", String.class)).isEqualTo("\"this is a fallback class\"");
|
||||
Utils.sleepUninterrupted(2000);
|
||||
assertThat(restTemplateFallbackFromCode3.getForEntity("/example/service/b/info", String.class)
|
||||
.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
Utils.sleepUninterrupted(2000);
|
||||
assertThat(restTemplateFallbackFromCode4.getForObject("/example/service/b/info", String.class)).isEqualTo("fallback");
|
||||
Utils.sleepUninterrupted(2000);
|
||||
assertThat(restTemplateFallbackFromPolaris.getForObject("/example/service/b/info", String.class)).isEqualTo("\"fallback from polaris server\"");
|
||||
// just for code coverage
|
||||
PolarisCircuitBreakerHttpResponse response = ((CustomPolarisCircuitBreakerFallback) applicationContext.getBean("customPolarisCircuitBreakerFallback")).fallback();
|
||||
assertThat(response.getStatusText()).isEqualTo("OK");
|
||||
assertThat(response.getFallbackInfo().getCode()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
@ImportAutoConfiguration({PolarisCircuitBreakerFeignClientAutoConfiguration.class})
|
||||
@EnableFeignClients
|
||||
public static class TestConfig {
|
||||
|
||||
@Bean
|
||||
@PolarisCircuitBreaker(fallback = "fallback")
|
||||
public RestTemplate defaultRestTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@LoadBalanced
|
||||
@PolarisCircuitBreaker
|
||||
public RestTemplate restTemplateFallbackFromPolaris() {
|
||||
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(uriBuilderFactory);
|
||||
return restTemplate;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@LoadBalanced
|
||||
@PolarisCircuitBreaker(fallbackClass = CustomPolarisCircuitBreakerFallback.class)
|
||||
public RestTemplate restTemplateFallbackFromCode() {
|
||||
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(uriBuilderFactory);
|
||||
return restTemplate;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@LoadBalanced
|
||||
@PolarisCircuitBreaker(fallbackClass = CustomPolarisCircuitBreakerFallback2.class)
|
||||
public RestTemplate restTemplateFallbackFromCode2() {
|
||||
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(uriBuilderFactory);
|
||||
return restTemplate;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@LoadBalanced
|
||||
@PolarisCircuitBreaker(fallbackClass = CustomPolarisCircuitBreakerFallback3.class)
|
||||
public RestTemplate restTemplateFallbackFromCode3() {
|
||||
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(uriBuilderFactory);
|
||||
return restTemplate;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@LoadBalanced
|
||||
@PolarisCircuitBreaker(fallback = "fallback")
|
||||
public RestTemplate restTemplateFallbackFromCode4() {
|
||||
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://" + TEST_SERVICE_NAME);
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(uriBuilderFactory);
|
||||
return restTemplate;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CustomPolarisCircuitBreakerFallback customPolarisCircuitBreakerFallback() {
|
||||
return new CustomPolarisCircuitBreakerFallback();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CustomPolarisCircuitBreakerFallback2 customPolarisCircuitBreakerFallback2() {
|
||||
return new CustomPolarisCircuitBreakerFallback2();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CustomPolarisCircuitBreakerFallback3 customPolarisCircuitBreakerFallback3() {
|
||||
return new CustomPolarisCircuitBreakerFallback3();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CircuitBreakAPI circuitBreakAPI() throws InvalidProtocolBufferException {
|
||||
try {
|
||||
namingServer = NamingServer.startNamingServer(10081);
|
||||
System.setProperty(SERVER_ADDRESS_ENV, String.format("127.0.0.1:%d", namingServer.getPort()));
|
||||
}
|
||||
catch (IOException e) {
|
||||
|
||||
}
|
||||
ServiceKey serviceKey = new ServiceKey("default", TEST_SERVICE_NAME);
|
||||
|
||||
CircuitBreakerProto.CircuitBreakerRule.Builder circuitBreakerRuleBuilder = CircuitBreakerProto.CircuitBreakerRule.newBuilder();
|
||||
InputStream inputStream = PolarisCircuitBreakerMockServerTest.class.getClassLoader()
|
||||
.getResourceAsStream("circuitBreakerRule.json");
|
||||
String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines()
|
||||
.collect(Collectors.joining(""));
|
||||
JsonFormat.parser().ignoringUnknownFields().merge(json, circuitBreakerRuleBuilder);
|
||||
CircuitBreakerProto.CircuitBreakerRule circuitBreakerRule = circuitBreakerRuleBuilder.build();
|
||||
CircuitBreakerProto.CircuitBreaker circuitBreaker = CircuitBreakerProto.CircuitBreaker.newBuilder()
|
||||
.addRules(circuitBreakerRule).build();
|
||||
namingServer.getNamingService().setCircuitBreaker(serviceKey, circuitBreaker);
|
||||
com.tencent.polaris.api.config.Configuration configuration = TestUtils.configWithEnvAddress();
|
||||
return CircuitBreakAPIFactory.createCircuitBreakAPIByConfig(configuration);
|
||||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/example/service/b")
|
||||
public class ServiceBController {
|
||||
|
||||
/**
|
||||
* Get service information.
|
||||
*
|
||||
* @return service information
|
||||
*/
|
||||
@GetMapping("/info")
|
||||
public String info() {
|
||||
return "hello world ! I'm a service B1";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class CustomPolarisCircuitBreakerFallback implements PolarisCircuitBreakerFallback {
|
||||
@Override
|
||||
public PolarisCircuitBreakerHttpResponse fallback() {
|
||||
return new PolarisCircuitBreakerHttpResponse(
|
||||
200,
|
||||
new HashMap<String, String>() {{
|
||||
put("xxx", "xxx");
|
||||
}},
|
||||
"\"this is a fallback class\"");
|
||||
}
|
||||
}
|
||||
|
||||
public static class CustomPolarisCircuitBreakerFallback2 implements PolarisCircuitBreakerFallback {
|
||||
@Override
|
||||
public PolarisCircuitBreakerHttpResponse fallback() {
|
||||
return new PolarisCircuitBreakerHttpResponse(
|
||||
200,
|
||||
"\"this is a fallback class\""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static class CustomPolarisCircuitBreakerFallback3 implements PolarisCircuitBreakerFallback {
|
||||
@Override
|
||||
public PolarisCircuitBreakerHttpResponse fallback() {
|
||||
return new PolarisCircuitBreakerHttpResponse(
|
||||
200
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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.circuitbreaker;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.protobuf.util.JsonFormat;
|
||||
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
|
||||
import com.tencent.polaris.api.config.Configuration;
|
||||
import com.tencent.polaris.api.core.ConsumerAPI;
|
||||
import com.tencent.polaris.api.pojo.ServiceKey;
|
||||
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
|
||||
import com.tencent.polaris.circuitbreak.factory.CircuitBreakAPIFactory;
|
||||
import com.tencent.polaris.client.util.Utils;
|
||||
import com.tencent.polaris.factory.api.DiscoveryAPIFactory;
|
||||
import com.tencent.polaris.specification.api.v1.fault.tolerance.CircuitBreakerProto;
|
||||
import com.tencent.polaris.test.common.TestUtils;
|
||||
import com.tencent.polaris.test.mock.discovery.NamingServer;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
|
||||
|
||||
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
|
||||
import static com.tencent.polaris.test.common.Consts.SERVICE_CIRCUIT_BREAKER;
|
||||
import static com.tencent.polaris.test.common.TestUtils.SERVER_ADDRESS_ENV;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Test for {@link PolarisCircuitBreaker} and {@link ReactivePolarisCircuitBreaker} using real mock server.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class PolarisCircuitBreakerMockServerTest {
|
||||
|
||||
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
|
||||
private static NamingServer namingServer;
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeAll() throws IOException {
|
||||
mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class);
|
||||
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties("spring.cloud.polaris.namespace"))
|
||||
.thenReturn(NAMESPACE_TEST);
|
||||
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties("spring.cloud.polaris.service"))
|
||||
.thenReturn(SERVICE_CIRCUIT_BREAKER);
|
||||
|
||||
try {
|
||||
namingServer = NamingServer.startNamingServer(-1);
|
||||
System.setProperty(SERVER_ADDRESS_ENV, String.format("127.0.0.1:%d", namingServer.getPort()));
|
||||
}
|
||||
catch (IOException e) {
|
||||
|
||||
}
|
||||
ServiceKey serviceKey = new ServiceKey(NAMESPACE_TEST, SERVICE_CIRCUIT_BREAKER);
|
||||
|
||||
CircuitBreakerProto.CircuitBreakerRule.Builder circuitBreakerRuleBuilder = CircuitBreakerProto.CircuitBreakerRule.newBuilder();
|
||||
InputStream inputStream = PolarisCircuitBreakerMockServerTest.class.getClassLoader().getResourceAsStream("circuitBreakerRule.json");
|
||||
String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines().collect(Collectors.joining(""));
|
||||
JsonFormat.parser().ignoringUnknownFields().merge(json, circuitBreakerRuleBuilder);
|
||||
CircuitBreakerProto.CircuitBreakerRule circuitBreakerRule = circuitBreakerRuleBuilder.build();
|
||||
CircuitBreakerProto.CircuitBreaker circuitBreaker = CircuitBreakerProto.CircuitBreaker.newBuilder().addRules(circuitBreakerRule).build();
|
||||
namingServer.getNamingService().setCircuitBreaker(serviceKey, circuitBreaker);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterAll() {
|
||||
if (null != namingServer) {
|
||||
namingServer.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCircuitBreaker() {
|
||||
Configuration configuration = TestUtils.configWithEnvAddress();
|
||||
CircuitBreakAPI circuitBreakAPI = CircuitBreakAPIFactory.createCircuitBreakAPIByConfig(configuration);
|
||||
|
||||
ConsumerAPI consumerAPI = DiscoveryAPIFactory.createConsumerAPIByConfig(configuration);
|
||||
PolarisCircuitBreakerFactory polarisCircuitBreakerFactory = new PolarisCircuitBreakerFactory(circuitBreakAPI, consumerAPI);
|
||||
CircuitBreaker cb = polarisCircuitBreakerFactory.create(SERVICE_CIRCUIT_BREAKER);
|
||||
|
||||
// trigger fallback for 5 times
|
||||
List<String> resList = new ArrayList<>();
|
||||
for (int i = 0; i < 5; i++) {
|
||||
int finalI = i;
|
||||
String res = cb.run(() -> {
|
||||
if (finalI % 2 == 1) {
|
||||
throw new IllegalArgumentException("invoke failed");
|
||||
}
|
||||
else {
|
||||
return "invoke success";
|
||||
}
|
||||
}, t -> "fallback");
|
||||
resList.add(res);
|
||||
Utils.sleepUninterrupted(2000);
|
||||
}
|
||||
assertThat(resList).isEqualTo(Arrays.asList("invoke success", "fallback", "fallback", "fallback", "fallback"));
|
||||
|
||||
// always fallback
|
||||
ReactivePolarisCircuitBreakerFactory reactivePolarisCircuitBreakerFactory = new ReactivePolarisCircuitBreakerFactory(circuitBreakAPI, consumerAPI);
|
||||
ReactiveCircuitBreaker rcb = reactivePolarisCircuitBreakerFactory.create(SERVICE_CIRCUIT_BREAKER);
|
||||
|
||||
assertThat(Mono.just("foobar").transform(it -> rcb.run(it, t -> Mono.just("fallback")))
|
||||
.block()).isEqualTo("fallback");
|
||||
|
||||
assertThat(Mono.error(new RuntimeException("boom")).transform(it -> rcb.run(it, t -> Mono.just("fallback")))
|
||||
.block()).isEqualTo("fallback");
|
||||
|
||||
assertThat(Flux.just("foobar", "hello world").transform(it -> rcb.run(it, t -> Flux.just("fallback", "fallback")))
|
||||
.collectList().block())
|
||||
.isEqualTo(Arrays.asList("fallback", "fallback"));
|
||||
|
||||
assertThat(Flux.error(new RuntimeException("boom")).transform(it -> rcb.run(it, t -> Flux.just("fallback")))
|
||||
.collectList().block())
|
||||
.isEqualTo(Collections.singletonList("fallback"));
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.circuitbreaker;
|
||||
|
||||
|
||||
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerAutoConfiguration;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.config.PolarisCircuitBreakerFeignClientAutoConfiguration;
|
||||
import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration;
|
||||
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementAutoConfiguration;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration;
|
||||
|
||||
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
|
||||
import static com.tencent.polaris.test.common.Consts.SERVICE_CIRCUIT_BREAKER;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Test for {@link PolarisCircuitBreaker}.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class PolarisCircuitBreakerTest {
|
||||
|
||||
private static final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(
|
||||
PolarisContextAutoConfiguration.class,
|
||||
RpcEnhancementAutoConfiguration.class,
|
||||
LoadBalancerAutoConfiguration.class,
|
||||
PolarisCircuitBreakerFeignClientAutoConfiguration.class,
|
||||
PolarisCircuitBreakerAutoConfiguration.class))
|
||||
.withPropertyValues("spring.cloud.polaris.circuitbreaker.enabled=true");
|
||||
|
||||
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeClass() {
|
||||
mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class);
|
||||
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties("spring.cloud.polaris.namespace"))
|
||||
.thenReturn(NAMESPACE_TEST);
|
||||
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties("spring.cloud.polaris.service"))
|
||||
.thenReturn(SERVICE_CIRCUIT_BREAKER);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterAll() {
|
||||
mockedApplicationContextAwareUtils.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void run() {
|
||||
contextRunner.run(context -> {
|
||||
|
||||
PolarisCircuitBreakerFactory polarisCircuitBreakerFactory = context.getBean(PolarisCircuitBreakerFactory.class);
|
||||
CircuitBreaker cb = polarisCircuitBreakerFactory.create(SERVICE_CIRCUIT_BREAKER);
|
||||
|
||||
PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration configuration =
|
||||
polarisCircuitBreakerFactory.configBuilder(SERVICE_CIRCUIT_BREAKER).build();
|
||||
|
||||
polarisCircuitBreakerFactory.configureDefault(id -> configuration);
|
||||
|
||||
assertThat(cb.run(() -> "foobar")).isEqualTo("foobar");
|
||||
|
||||
assertThat((String) cb.run(() -> {
|
||||
throw new RuntimeException("boom");
|
||||
}, t -> "fallback")).isEqualTo("fallback");
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.circuitbreaker;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.config.ReactivePolarisCircuitBreakerAutoConfiguration;
|
||||
import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration;
|
||||
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementAutoConfiguration;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration;
|
||||
|
||||
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
|
||||
import static com.tencent.polaris.test.common.Consts.SERVICE_CIRCUIT_BREAKER;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Test for {@link ReactivePolarisCircuitBreaker}.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class ReactivePolarisCircuitBreakerTest {
|
||||
|
||||
private final ApplicationContextRunner reactiveContextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(
|
||||
PolarisContextAutoConfiguration.class,
|
||||
RpcEnhancementAutoConfiguration.class,
|
||||
LoadBalancerAutoConfiguration.class,
|
||||
ReactivePolarisCircuitBreakerAutoConfiguration.class))
|
||||
.withPropertyValues("spring.cloud.polaris.circuitbreaker.enabled=true");
|
||||
|
||||
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeClass() {
|
||||
mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class);
|
||||
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties("spring.cloud.polaris.namespace"))
|
||||
.thenReturn(NAMESPACE_TEST);
|
||||
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties("spring.cloud.polaris.service"))
|
||||
.thenReturn(SERVICE_CIRCUIT_BREAKER);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterAll() {
|
||||
mockedApplicationContextAwareUtils.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void run() {
|
||||
this.reactiveContextRunner.run(context -> {
|
||||
ReactivePolarisCircuitBreakerFactory polarisCircuitBreakerFactory = context.getBean(ReactivePolarisCircuitBreakerFactory.class);
|
||||
ReactiveCircuitBreaker cb = polarisCircuitBreakerFactory.create(SERVICE_CIRCUIT_BREAKER);
|
||||
|
||||
PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration configuration =
|
||||
polarisCircuitBreakerFactory.configBuilder(SERVICE_CIRCUIT_BREAKER).build();
|
||||
|
||||
polarisCircuitBreakerFactory.configureDefault(id -> configuration);
|
||||
|
||||
assertThat(Mono.just("foobar").transform(cb::run).block()).isEqualTo("foobar");
|
||||
|
||||
assertThat(Mono.error(new RuntimeException("boom")).transform(it -> cb.run(it, t -> Mono.just("fallback")))
|
||||
.block()).isEqualTo("fallback");
|
||||
|
||||
assertThat(Flux.just("foobar", "hello world").transform(cb::run).collectList().block())
|
||||
.isEqualTo(Arrays.asList("foobar", "hello world"));
|
||||
|
||||
assertThat(Flux.error(new RuntimeException("boom")).transform(it -> cb.run(it, t -> Flux.just("fallback")))
|
||||
.collectList().block()).isEqualTo(Collections.singletonList("fallback"));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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.circuitbreaker.reporter;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import com.tencent.cloud.common.metadata.MetadataContext;
|
||||
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
|
||||
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext;
|
||||
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
|
||||
import com.tencent.polaris.client.api.SDKContext;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
|
||||
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
|
||||
import static com.tencent.polaris.test.common.Consts.SERVICE_PROVIDER;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* ExceptionCircuitBreakerReporterTest.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class ExceptionCircuitBreakerReporterTest {
|
||||
|
||||
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
|
||||
@Mock
|
||||
private RpcEnhancementReporterProperties reporterProperties;
|
||||
@Mock
|
||||
private SDKContext sdkContext;
|
||||
@InjectMocks
|
||||
private ExceptionCircuitBreakerReporter exceptionCircuitBreakerReporter;
|
||||
@Mock
|
||||
private CircuitBreakAPI circuitBreakAPI;
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() {
|
||||
mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class);
|
||||
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString()))
|
||||
.thenReturn("unit-test");
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void afterAll() {
|
||||
mockedApplicationContextAwareUtils.close();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MetadataContext.LOCAL_NAMESPACE = NAMESPACE_TEST;
|
||||
MetadataContext.LOCAL_SERVICE = SERVICE_PROVIDER;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetName() {
|
||||
assertThat(exceptionCircuitBreakerReporter.getName()).isEqualTo(ExceptionCircuitBreakerReporter.class.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testType() {
|
||||
assertThat(exceptionCircuitBreakerReporter.getType()).isEqualTo(EnhancedPluginType.EXCEPTION);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRun() throws Throwable {
|
||||
EnhancedPluginContext context = mock(EnhancedPluginContext.class);
|
||||
// test not report
|
||||
exceptionCircuitBreakerReporter.run(context);
|
||||
verify(context, times(0)).getRequest();
|
||||
|
||||
doReturn(true).when(reporterProperties).isEnabled();
|
||||
|
||||
EnhancedPluginContext pluginContext = new EnhancedPluginContext();
|
||||
EnhancedRequestContext request = EnhancedRequestContext.builder()
|
||||
.httpMethod(HttpMethod.GET)
|
||||
.url(URI.create("http://0.0.0.0/"))
|
||||
.httpHeaders(new HttpHeaders())
|
||||
.build();
|
||||
EnhancedResponseContext response = EnhancedResponseContext.builder()
|
||||
.httpStatus(200)
|
||||
.build();
|
||||
DefaultServiceInstance serviceInstance = new DefaultServiceInstance();
|
||||
serviceInstance.setServiceId(SERVICE_PROVIDER);
|
||||
|
||||
pluginContext.setRequest(request);
|
||||
pluginContext.setResponse(response);
|
||||
pluginContext.setServiceInstance(serviceInstance);
|
||||
pluginContext.setThrowable(new RuntimeException());
|
||||
|
||||
exceptionCircuitBreakerReporter.run(pluginContext);
|
||||
exceptionCircuitBreakerReporter.getOrder();
|
||||
exceptionCircuitBreakerReporter.getName();
|
||||
exceptionCircuitBreakerReporter.getType();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlerThrowable() {
|
||||
// mock request
|
||||
EnhancedRequestContext request = mock(EnhancedRequestContext.class);
|
||||
// mock response
|
||||
EnhancedResponseContext response = mock(EnhancedResponseContext.class);
|
||||
|
||||
EnhancedPluginContext context = new EnhancedPluginContext();
|
||||
context.setRequest(request);
|
||||
context.setResponse(response);
|
||||
exceptionCircuitBreakerReporter.handlerThrowable(context, new RuntimeException("Mock exception."));
|
||||
}
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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.circuitbreaker.reporter;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import com.tencent.cloud.common.metadata.MetadataContext;
|
||||
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
|
||||
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext;
|
||||
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext;
|
||||
import com.tencent.polaris.circuitbreak.api.CircuitBreakAPI;
|
||||
import com.tencent.polaris.client.api.SDKContext;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.http.HttpMethod;
|
||||
|
||||
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
|
||||
import static com.tencent.polaris.test.common.Consts.SERVICE_PROVIDER;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* SuccessCircuitBreakerReporterTest.
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class SuccessCircuitBreakerReporterTest {
|
||||
|
||||
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
|
||||
@Mock
|
||||
private SDKContext sdkContext;
|
||||
@Mock
|
||||
private RpcEnhancementReporterProperties reporterProperties;
|
||||
@InjectMocks
|
||||
private SuccessCircuitBreakerReporter successCircuitBreakerReporter;
|
||||
@Mock
|
||||
private CircuitBreakAPI circuitBreakAPI;
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() {
|
||||
mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class);
|
||||
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString()))
|
||||
.thenReturn("unit-test");
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void afterAll() {
|
||||
mockedApplicationContextAwareUtils.close();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MetadataContext.LOCAL_NAMESPACE = NAMESPACE_TEST;
|
||||
MetadataContext.LOCAL_SERVICE = SERVICE_PROVIDER;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetName() {
|
||||
assertThat(successCircuitBreakerReporter.getName()).isEqualTo(SuccessCircuitBreakerReporter.class.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testType() {
|
||||
assertThat(successCircuitBreakerReporter.getType()).isEqualTo(EnhancedPluginType.POST);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRun() throws Throwable {
|
||||
|
||||
EnhancedPluginContext context = mock(EnhancedPluginContext.class);
|
||||
// test not report
|
||||
successCircuitBreakerReporter.run(context);
|
||||
verify(context, times(0)).getRequest();
|
||||
|
||||
doReturn(true).when(reporterProperties).isEnabled();
|
||||
|
||||
EnhancedPluginContext pluginContext = new EnhancedPluginContext();
|
||||
EnhancedRequestContext request = EnhancedRequestContext.builder()
|
||||
.httpMethod(HttpMethod.GET)
|
||||
.url(URI.create("http://0.0.0.0/"))
|
||||
.build();
|
||||
EnhancedResponseContext response = EnhancedResponseContext.builder()
|
||||
.httpStatus(200)
|
||||
.build();
|
||||
DefaultServiceInstance serviceInstance = new DefaultServiceInstance();
|
||||
serviceInstance.setServiceId(SERVICE_PROVIDER);
|
||||
|
||||
pluginContext.setRequest(request);
|
||||
pluginContext.setResponse(response);
|
||||
pluginContext.setServiceInstance(serviceInstance);
|
||||
|
||||
successCircuitBreakerReporter.run(pluginContext);
|
||||
successCircuitBreakerReporter.getOrder();
|
||||
successCircuitBreakerReporter.getName();
|
||||
successCircuitBreakerReporter.getType();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlerThrowable() {
|
||||
// mock request
|
||||
EnhancedRequestContext request = mock(EnhancedRequestContext.class);
|
||||
// mock response
|
||||
EnhancedResponseContext response = mock(EnhancedResponseContext.class);
|
||||
|
||||
EnhancedPluginContext context = new EnhancedPluginContext();
|
||||
context.setRequest(request);
|
||||
context.setResponse(response);
|
||||
successCircuitBreakerReporter.handlerThrowable(context, new RuntimeException("Mock exception."));
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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.circuitbreaker.util;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import com.tencent.cloud.common.metadata.MetadataContext;
|
||||
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
|
||||
import com.tencent.cloud.polaris.circuitbreaker.common.PolarisCircuitBreakerConfigBuilder;
|
||||
import com.tencent.polaris.api.core.ConsumerAPI;
|
||||
import com.tencent.polaris.api.pojo.CircuitBreakerStatus;
|
||||
import com.tencent.polaris.api.rpc.ServiceCallResult;
|
||||
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import static com.tencent.polaris.test.common.Consts.NAMESPACE_TEST;
|
||||
import static com.tencent.polaris.test.common.Consts.SERVICE_PROVIDER;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
|
||||
public class PolarisCircuitBreakerUtilsTests {
|
||||
|
||||
private static MockedStatic<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
|
||||
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() {
|
||||
mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class);
|
||||
mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString()))
|
||||
.thenReturn("unit-test");
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void afterAll() {
|
||||
mockedApplicationContextAwareUtils.close();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MetadataContext.LOCAL_NAMESPACE = NAMESPACE_TEST;
|
||||
MetadataContext.LOCAL_SERVICE = SERVICE_PROVIDER;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReportStatus() {
|
||||
ConsumerAPI consumerAPI = Mockito.mock(ConsumerAPI.class);
|
||||
Mockito.doNothing().when(consumerAPI).updateServiceCallResult(new ServiceCallResult());
|
||||
PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration conf = new PolarisCircuitBreakerConfigBuilder.PolarisCircuitBreakerConfiguration();
|
||||
PolarisCircuitBreakerUtils.reportStatus(consumerAPI, conf, new CallAbortedException("mock", new CircuitBreakerStatus.FallbackInfo(0, new HashMap<>(), "")));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
spring:
|
||||
application:
|
||||
name: GatewayScgService
|
||||
cloud:
|
||||
tencent:
|
||||
plugin:
|
||||
scg:
|
||||
staining:
|
||||
enabled: true
|
||||
rule-staining:
|
||||
enabled: true
|
||||
router:
|
||||
feature-env:
|
||||
enabled: true
|
||||
polaris:
|
||||
address: grpc://127.0.0.1:8091
|
||||
namespace: default
|
||||
enabled: true
|
||||
gateway:
|
||||
routes:
|
||||
- id: cb-test
|
||||
uri: http://localhost:${server.port}/hello/1
|
||||
predicates:
|
||||
- Path=/cb-test/**
|
||||
filters:
|
||||
- name: CircuitBreaker
|
||||
args:
|
||||
statusCodes: 5**,4**,3**,2**,1**,500,400
|
||||
fallbackUri: forward:/polaris-fallback
|
||||
logging:
|
||||
level:
|
||||
root: info
|
||||
com.tencent.polaris.discovery.client.flow.RegisterFlow: off
|
||||
com.tencent.polaris.plugins.registry.memory.CacheObject: off
|
||||
com.tencent.cloud.polaris.circuitbreaker: debug
|
||||
|
@ -0,0 +1,59 @@
|
||||
{
|
||||
"@type": "type.googleapis.com/v1.CircuitBreakerRule",
|
||||
"id": "5f1601f01823474d9be39c0bbb26ab87",
|
||||
"name": "test",
|
||||
"namespace": "TestCircuitBreakerRule",
|
||||
"enable": true,
|
||||
"revision": "10b120c08706429f8fdc3fb44a53224b",
|
||||
"ctime": "1754-08-31 06:49:24",
|
||||
"mtime": "2023-02-21 17:35:31",
|
||||
"etime": "",
|
||||
"description": "",
|
||||
"level": "METHOD",
|
||||
"ruleMatcher": {
|
||||
"source": {
|
||||
"service": "*",
|
||||
"namespace": "*"
|
||||
},
|
||||
"destination": {
|
||||
"service": "*",
|
||||
"namespace": "*",
|
||||
"method": {"type": "REGEX", "value": "*"}
|
||||
}
|
||||
},
|
||||
"errorConditions": [
|
||||
{
|
||||
"inputType": "RET_CODE",
|
||||
"condition": {
|
||||
"type": "NOT_EQUALS",
|
||||
"value": "200",
|
||||
"valueType": "TEXT"
|
||||
}
|
||||
}
|
||||
],
|
||||
"triggerCondition": [
|
||||
{
|
||||
"triggerType": "CONSECUTIVE_ERROR",
|
||||
"errorCount": 1,
|
||||
"errorPercent": 1,
|
||||
"interval": 5,
|
||||
"minimumRequest": 5
|
||||
}
|
||||
],
|
||||
"maxEjectionPercent": 0,
|
||||
"recoverCondition": {
|
||||
"sleepWindow": 60,
|
||||
"consecutiveSuccess": 3
|
||||
},
|
||||
"faultDetectConfig": {
|
||||
"enable": true
|
||||
},
|
||||
"fallbackConfig": {
|
||||
"enable": true,
|
||||
"response": {
|
||||
"code": 200,
|
||||
"headers": [{"key": "xxx", "value": "xxx"}],
|
||||
"body": "\"fallback from polaris server\""
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
{
|
||||
"@type": "type.googleapis.com/v1.CircuitBreakerRule",
|
||||
"id": "5f1601f01823474d9be39c0bbb26ab87",
|
||||
"name": "test",
|
||||
"namespace": "TestCircuitBreakerRule",
|
||||
"enable": true,
|
||||
"revision": "10b120c08706429f8fdc3fb44a53224b",
|
||||
"ctime": "1754-08-31 06:49:24",
|
||||
"mtime": "2023-02-21 17:35:31",
|
||||
"etime": "",
|
||||
"description": "",
|
||||
"level": "SERVICE",
|
||||
"ruleMatcher": {
|
||||
"source": {
|
||||
"service": "*",
|
||||
"namespace": "*"
|
||||
},
|
||||
"destination": {
|
||||
"service": "*",
|
||||
"namespace": "*",
|
||||
"method": null
|
||||
}
|
||||
},
|
||||
"errorConditions": [
|
||||
{
|
||||
"inputType": "RET_CODE",
|
||||
"condition": {
|
||||
"type": "NOT_EQUALS",
|
||||
"value": "200",
|
||||
"valueType": "TEXT"
|
||||
}
|
||||
}
|
||||
],
|
||||
"triggerCondition": [
|
||||
{
|
||||
"triggerType": "CONSECUTIVE_ERROR",
|
||||
"errorCount": 1,
|
||||
"errorPercent": 1,
|
||||
"interval": 5,
|
||||
"minimumRequest": 5
|
||||
}
|
||||
],
|
||||
"maxEjectionPercent": 0,
|
||||
"recoverCondition": {
|
||||
"sleepWindow": 60,
|
||||
"consecutiveSuccess": 3
|
||||
},
|
||||
"faultDetectConfig": {
|
||||
"enable": true
|
||||
},
|
||||
"fallbackConfig": {
|
||||
"enable": true,
|
||||
"response": {
|
||||
"code": 200,
|
||||
"headers": [{"key": "xxx", "value": "xxx"}],
|
||||
"body": "\"fallback from polaris server\""
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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.ratelimit.resolver;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.tencent.cloud.common.metadata.MetadataContextHolder;
|
||||
import com.tencent.cloud.polaris.context.ServiceRuleManager;
|
||||
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelReactiveResolver;
|
||||
import com.tencent.polaris.ratelimit.api.rpc.Argument;
|
||||
import com.tencent.polaris.specification.api.v1.traffic.manage.RateLimitProto;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAME;
|
||||
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE;
|
||||
|
||||
/**
|
||||
* resolve arguments from rate limit rule for Reactive.
|
||||
*
|
||||
* @author seansyyu 2023-03-09
|
||||
*/
|
||||
public class RateLimitRuleArgumentReactiveResolver {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RateLimitRuleArgumentReactiveResolver.class);
|
||||
|
||||
private final ServiceRuleManager serviceRuleManager;
|
||||
|
||||
private final PolarisRateLimiterLabelReactiveResolver labelResolver;
|
||||
|
||||
public RateLimitRuleArgumentReactiveResolver(ServiceRuleManager serviceRuleManager, PolarisRateLimiterLabelReactiveResolver labelResolver) {
|
||||
this.serviceRuleManager = serviceRuleManager;
|
||||
this.labelResolver = labelResolver;
|
||||
}
|
||||
|
||||
public Set<Argument> getArguments(ServerWebExchange request, String namespace, String service) {
|
||||
RateLimitProto.RateLimit rateLimitRule = serviceRuleManager.getServiceRateLimitRule(namespace, service);
|
||||
if (rateLimitRule == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
List<RateLimitProto.Rule> rules = rateLimitRule.getRulesList();
|
||||
if (CollectionUtils.isEmpty(rules)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return rules.stream()
|
||||
.flatMap(rule -> rule.getArgumentsList().stream())
|
||||
.map(matchArgument -> {
|
||||
String matchKey = matchArgument.getKey();
|
||||
Argument argument = null;
|
||||
switch (matchArgument.getType()) {
|
||||
case CUSTOM:
|
||||
argument = StringUtils.isBlank(matchKey) ? null :
|
||||
Argument.buildCustom(matchKey, Optional.ofNullable(getCustomResolvedLabels(request).get(matchKey))
|
||||
.orElse(StringUtils.EMPTY));
|
||||
break;
|
||||
case METHOD:
|
||||
argument = Argument.buildMethod(request.getRequest().getMethodValue());
|
||||
break;
|
||||
case HEADER:
|
||||
argument = StringUtils.isBlank(matchKey) ? null :
|
||||
Argument.buildHeader(matchKey, Optional.ofNullable(request.getRequest().getHeaders()
|
||||
.getFirst(matchKey)).orElse(StringUtils.EMPTY));
|
||||
break;
|
||||
case QUERY:
|
||||
argument = StringUtils.isBlank(matchKey) ? null :
|
||||
Argument.buildQuery(matchKey, Optional.ofNullable(request.getRequest().getQueryParams()
|
||||
.getFirst(matchKey)).orElse(StringUtils.EMPTY));
|
||||
break;
|
||||
case CALLER_SERVICE:
|
||||
String sourceServiceNamespace = MetadataContextHolder.getDisposableMetadata(DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE, true)
|
||||
.orElse(StringUtils.EMPTY);
|
||||
String sourceServiceName = MetadataContextHolder.getDisposableMetadata(DEFAULT_METADATA_SOURCE_SERVICE_NAME, true)
|
||||
.orElse(StringUtils.EMPTY);
|
||||
if (!StringUtils.isEmpty(sourceServiceNamespace) && !StringUtils.isEmpty(sourceServiceName)) {
|
||||
argument = Argument.buildCallerService(sourceServiceNamespace, sourceServiceName);
|
||||
}
|
||||
break;
|
||||
case CALLER_IP:
|
||||
InetSocketAddress remoteAddress = request.getRequest().getRemoteAddress();
|
||||
argument = Argument.buildCallerIP(remoteAddress != null ? remoteAddress.getAddress()
|
||||
.getHostAddress() : StringUtils.EMPTY);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return argument;
|
||||
}).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private Map<String, String> getCustomResolvedLabels(ServerWebExchange request) {
|
||||
if (labelResolver != null) {
|
||||
try {
|
||||
return labelResolver.resolve(request);
|
||||
}
|
||||
catch (Throwable e) {
|
||||
LOG.error("resolve custom label failed. resolver = {}", labelResolver.getClass().getName(), e);
|
||||
}
|
||||
}
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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.ratelimit.resolver;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import com.tencent.cloud.common.metadata.MetadataContextHolder;
|
||||
import com.tencent.cloud.polaris.context.ServiceRuleManager;
|
||||
import com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckServletFilter;
|
||||
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelServletResolver;
|
||||
import com.tencent.polaris.ratelimit.api.rpc.Argument;
|
||||
import com.tencent.polaris.specification.api.v1.traffic.manage.RateLimitProto;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAME;
|
||||
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE;
|
||||
|
||||
/**
|
||||
* resolve arguments from rate limit rule for Servlet.
|
||||
*
|
||||
* @author seansyyu 2023-03-09
|
||||
*/
|
||||
public class RateLimitRuleArgumentServletResolver {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(QuotaCheckServletFilter.class);
|
||||
|
||||
private final ServiceRuleManager serviceRuleManager;
|
||||
|
||||
private final PolarisRateLimiterLabelServletResolver labelResolver;
|
||||
|
||||
public RateLimitRuleArgumentServletResolver(ServiceRuleManager serviceRuleManager, PolarisRateLimiterLabelServletResolver labelResolver) {
|
||||
this.serviceRuleManager = serviceRuleManager;
|
||||
this.labelResolver = labelResolver;
|
||||
}
|
||||
|
||||
public Set<Argument> getArguments(HttpServletRequest request, String namespace, String service) {
|
||||
RateLimitProto.RateLimit rateLimitRule = serviceRuleManager.getServiceRateLimitRule(namespace, service);
|
||||
if (rateLimitRule == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
List<RateLimitProto.Rule> rules = rateLimitRule.getRulesList();
|
||||
if (CollectionUtils.isEmpty(rules)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return rules.stream()
|
||||
.flatMap(rule -> rule.getArgumentsList().stream())
|
||||
.map(matchArgument -> {
|
||||
String matchKey = matchArgument.getKey();
|
||||
Argument argument = null;
|
||||
switch (matchArgument.getType()) {
|
||||
case CUSTOM:
|
||||
argument = StringUtils.isBlank(matchKey) ? null :
|
||||
Argument.buildCustom(matchKey, Optional.ofNullable(getCustomResolvedLabels(request).get(matchKey)).orElse(StringUtils.EMPTY));
|
||||
break;
|
||||
case METHOD:
|
||||
argument = Argument.buildMethod(request.getMethod());
|
||||
break;
|
||||
case HEADER:
|
||||
argument = StringUtils.isBlank(matchKey) ? null :
|
||||
Argument.buildHeader(matchKey, Optional.ofNullable(request.getHeader(matchKey)).orElse(StringUtils.EMPTY));
|
||||
break;
|
||||
case QUERY:
|
||||
argument = StringUtils.isBlank(matchKey) ? null :
|
||||
Argument.buildQuery(matchKey, Optional.ofNullable(request.getParameter(matchKey)).orElse(StringUtils.EMPTY));
|
||||
break;
|
||||
case CALLER_SERVICE:
|
||||
String sourceServiceNamespace = MetadataContextHolder.getDisposableMetadata(DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE, true).orElse(StringUtils.EMPTY);
|
||||
String sourceServiceName = MetadataContextHolder.getDisposableMetadata(DEFAULT_METADATA_SOURCE_SERVICE_NAME, true).orElse(StringUtils.EMPTY);
|
||||
if (!StringUtils.isEmpty(sourceServiceNamespace) && !StringUtils.isEmpty(sourceServiceName)) {
|
||||
argument = Argument.buildCallerService(sourceServiceNamespace, sourceServiceName);
|
||||
}
|
||||
break;
|
||||
case CALLER_IP:
|
||||
argument = Argument.buildCallerIP(Optional.ofNullable(request.getRemoteAddr()).orElse(StringUtils.EMPTY));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return argument;
|
||||
}).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private Map<String, String> getCustomResolvedLabels(HttpServletRequest request) {
|
||||
if (labelResolver != null) {
|
||||
try {
|
||||
return labelResolver.resolve(request);
|
||||
}
|
||||
catch (Throwable e) {
|
||||
LOG.error("resolve custom label failed. resolver = {}", labelResolver.getClass().getName(), e);
|
||||
}
|
||||
}
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
/*
|
||||
* 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.ratelimit.resolver;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import com.google.protobuf.util.JsonFormat;
|
||||
import com.tencent.cloud.common.metadata.MetadataContext;
|
||||
import com.tencent.cloud.common.metadata.MetadataContextHolder;
|
||||
import com.tencent.cloud.polaris.context.ServiceRuleManager;
|
||||
import com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckServletFilterTest;
|
||||
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelReactiveResolver;
|
||||
import com.tencent.polaris.ratelimit.api.rpc.Argument;
|
||||
import com.tencent.polaris.specification.api.v1.traffic.manage.RateLimitProto;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAME;
|
||||
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
classes = RateLimitRuleArgumentReactiveResolverTest.TestApplication.class,
|
||||
properties = {
|
||||
"spring.cloud.polaris.namespace=Test", "spring.cloud.polaris.service=TestApp"
|
||||
})
|
||||
public class RateLimitRuleArgumentReactiveResolverTest {
|
||||
|
||||
private final PolarisRateLimiterLabelReactiveResolver labelResolver =
|
||||
exchange -> Collections.singletonMap("xxx", "xxx");
|
||||
|
||||
private final PolarisRateLimiterLabelReactiveResolver labelResolverEx =
|
||||
exchange -> {
|
||||
throw new RuntimeException();
|
||||
};
|
||||
|
||||
private RateLimitRuleArgumentReactiveResolver rateLimitRuleArgumentReactiveResolver1;
|
||||
private RateLimitRuleArgumentReactiveResolver rateLimitRuleArgumentReactiveResolver2;
|
||||
private RateLimitRuleArgumentReactiveResolver rateLimitRuleArgumentReactiveResolver3;
|
||||
private RateLimitRuleArgumentReactiveResolver rateLimitRuleArgumentReactiveResolver4;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws InvalidProtocolBufferException {
|
||||
MetadataContext.LOCAL_NAMESPACE = "TEST";
|
||||
|
||||
ServiceRuleManager serviceRuleManager = mock(ServiceRuleManager.class);
|
||||
|
||||
RateLimitProto.Rule.Builder ratelimitRuleBuilder = RateLimitProto.Rule.newBuilder();
|
||||
InputStream inputStream = QuotaCheckServletFilterTest.class.getClassLoader().getResourceAsStream("ratelimit.json");
|
||||
String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines().collect(Collectors.joining(""));
|
||||
JsonFormat.parser().ignoringUnknownFields().merge(json, ratelimitRuleBuilder);
|
||||
RateLimitProto.Rule rateLimitRule = ratelimitRuleBuilder.build();
|
||||
RateLimitProto.RateLimit rateLimit = RateLimitProto.RateLimit.newBuilder().addRules(rateLimitRule).build();
|
||||
when(serviceRuleManager.getServiceRateLimitRule(anyString(), anyString())).thenReturn(rateLimit);
|
||||
|
||||
// normal
|
||||
this.rateLimitRuleArgumentReactiveResolver1 = new RateLimitRuleArgumentReactiveResolver(serviceRuleManager, labelResolver);
|
||||
// ex
|
||||
this.rateLimitRuleArgumentReactiveResolver2 = new RateLimitRuleArgumentReactiveResolver(serviceRuleManager, labelResolverEx);
|
||||
// null
|
||||
ServiceRuleManager serviceRuleManager1 = mock(ServiceRuleManager.class);
|
||||
when(serviceRuleManager1.getServiceRateLimitRule(anyString(), anyString())).thenReturn(null);
|
||||
this.rateLimitRuleArgumentReactiveResolver3 = new RateLimitRuleArgumentReactiveResolver(serviceRuleManager1, labelResolver);
|
||||
// null 2
|
||||
ServiceRuleManager serviceRuleManager2 = mock(ServiceRuleManager.class);
|
||||
RateLimitProto.RateLimit rateLimit2 = RateLimitProto.RateLimit.newBuilder().build();
|
||||
when(serviceRuleManager2.getServiceRateLimitRule(anyString(), anyString())).thenReturn(rateLimit2);
|
||||
this.rateLimitRuleArgumentReactiveResolver4 = new RateLimitRuleArgumentReactiveResolver(serviceRuleManager2, labelResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetRuleArguments() {
|
||||
// Mock request
|
||||
MetadataContext.LOCAL_SERVICE = "Test";
|
||||
// Mock request
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("http://127.0.0.1:8080/test")
|
||||
.remoteAddress(new InetSocketAddress("127.0.0.1", 8080))
|
||||
.header("xxx", "xxx")
|
||||
.queryParam("yyy", "yyy")
|
||||
.build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
MetadataContext metadataContext = new MetadataContext();
|
||||
metadataContext.setUpstreamDisposableMetadata(new HashMap<String, String>() {{
|
||||
put(DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE, MetadataContext.LOCAL_NAMESPACE);
|
||||
put(DEFAULT_METADATA_SOURCE_SERVICE_NAME, MetadataContext.LOCAL_SERVICE);
|
||||
}});
|
||||
MetadataContextHolder.set(metadataContext);
|
||||
Set<Argument> arguments = rateLimitRuleArgumentReactiveResolver1.getArguments(exchange, MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE);
|
||||
Set<Argument> exceptRes = new HashSet<>();
|
||||
exceptRes.add(Argument.buildMethod("GET"));
|
||||
exceptRes.add(Argument.buildHeader("xxx", "xxx"));
|
||||
exceptRes.add(Argument.buildQuery("yyy", "yyy"));
|
||||
exceptRes.add(Argument.buildCallerIP("127.0.0.1"));
|
||||
exceptRes.add(Argument.buildCustom("xxx", "xxx"));
|
||||
exceptRes.add(Argument.buildCallerService(MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE));
|
||||
assertThat(arguments).isEqualTo(exceptRes);
|
||||
|
||||
rateLimitRuleArgumentReactiveResolver2.getArguments(exchange, MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE);
|
||||
rateLimitRuleArgumentReactiveResolver3.getArguments(exchange, MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE);
|
||||
rateLimitRuleArgumentReactiveResolver4.getArguments(exchange, MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE);
|
||||
}
|
||||
|
||||
@SpringBootApplication
|
||||
protected static class TestApplication {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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.ratelimit.resolver;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import com.google.protobuf.util.JsonFormat;
|
||||
import com.tencent.cloud.common.metadata.MetadataContext;
|
||||
import com.tencent.cloud.common.metadata.MetadataContextHolder;
|
||||
import com.tencent.cloud.polaris.context.ServiceRuleManager;
|
||||
import com.tencent.cloud.polaris.ratelimit.filter.QuotaCheckServletFilterTest;
|
||||
import com.tencent.cloud.polaris.ratelimit.spi.PolarisRateLimiterLabelServletResolver;
|
||||
import com.tencent.polaris.ratelimit.api.rpc.Argument;
|
||||
import com.tencent.polaris.specification.api.v1.traffic.manage.RateLimitProto;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAME;
|
||||
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
classes = RateLimitRuleArgumentServletResolverTest.TestApplication.class,
|
||||
properties = {
|
||||
"spring.cloud.polaris.namespace=Test", "spring.cloud.polaris.service=TestApp"
|
||||
})
|
||||
public class RateLimitRuleArgumentServletResolverTest {
|
||||
|
||||
private final PolarisRateLimiterLabelServletResolver labelResolver =
|
||||
exchange -> Collections.singletonMap("xxx", "xxx");
|
||||
private final PolarisRateLimiterLabelServletResolver labelResolverEx =
|
||||
exchange -> {
|
||||
throw new RuntimeException();
|
||||
};
|
||||
|
||||
private RateLimitRuleArgumentServletResolver rateLimitRuleArgumentServletResolver1;
|
||||
private RateLimitRuleArgumentServletResolver rateLimitRuleArgumentServletResolver2;
|
||||
private RateLimitRuleArgumentServletResolver rateLimitRuleArgumentServletResolver3;
|
||||
private RateLimitRuleArgumentServletResolver rateLimitRuleArgumentServletResolver4;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws InvalidProtocolBufferException {
|
||||
MetadataContext.LOCAL_NAMESPACE = "TEST";
|
||||
|
||||
ServiceRuleManager serviceRuleManager = mock(ServiceRuleManager.class);
|
||||
|
||||
RateLimitProto.Rule.Builder ratelimitRuleBuilder = RateLimitProto.Rule.newBuilder();
|
||||
InputStream inputStream = QuotaCheckServletFilterTest.class.getClassLoader().getResourceAsStream("ratelimit.json");
|
||||
String json = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines().collect(Collectors.joining(""));
|
||||
JsonFormat.parser().ignoringUnknownFields().merge(json, ratelimitRuleBuilder);
|
||||
RateLimitProto.Rule rateLimitRule = ratelimitRuleBuilder.build();
|
||||
RateLimitProto.RateLimit rateLimit = RateLimitProto.RateLimit.newBuilder().addRules(rateLimitRule).build();
|
||||
when(serviceRuleManager.getServiceRateLimitRule(anyString(), anyString())).thenReturn(rateLimit);
|
||||
|
||||
// normal
|
||||
this.rateLimitRuleArgumentServletResolver1 = new RateLimitRuleArgumentServletResolver(serviceRuleManager, labelResolver);
|
||||
// ex
|
||||
this.rateLimitRuleArgumentServletResolver2 = new RateLimitRuleArgumentServletResolver(serviceRuleManager, labelResolverEx);
|
||||
// null
|
||||
ServiceRuleManager serviceRuleManager1 = mock(ServiceRuleManager.class);
|
||||
when(serviceRuleManager1.getServiceRateLimitRule(anyString(), anyString())).thenReturn(null);
|
||||
this.rateLimitRuleArgumentServletResolver3 = new RateLimitRuleArgumentServletResolver(serviceRuleManager1, labelResolver);
|
||||
// null 2
|
||||
ServiceRuleManager serviceRuleManager2 = mock(ServiceRuleManager.class);
|
||||
RateLimitProto.RateLimit rateLimit2 = RateLimitProto.RateLimit.newBuilder().build();
|
||||
when(serviceRuleManager2.getServiceRateLimitRule(anyString(), anyString())).thenReturn(rateLimit2);
|
||||
this.rateLimitRuleArgumentServletResolver4 = new RateLimitRuleArgumentServletResolver(serviceRuleManager2, labelResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetRuleArguments() {
|
||||
// Mock request
|
||||
MetadataContext.LOCAL_SERVICE = "Test";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest(null, "GET", "/xxx");
|
||||
request.setParameter("yyy", "yyy");
|
||||
request.addHeader("xxx", "xxx");
|
||||
MetadataContext metadataContext = new MetadataContext();
|
||||
metadataContext.setUpstreamDisposableMetadata(new HashMap<String, String>() {{
|
||||
put(DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE, MetadataContext.LOCAL_NAMESPACE);
|
||||
put(DEFAULT_METADATA_SOURCE_SERVICE_NAME, MetadataContext.LOCAL_SERVICE);
|
||||
}});
|
||||
MetadataContextHolder.set(metadataContext);
|
||||
Set<Argument> arguments = rateLimitRuleArgumentServletResolver1.getArguments(request, MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE);
|
||||
Set<Argument> exceptRes = new HashSet<>();
|
||||
exceptRes.add(Argument.buildMethod("GET"));
|
||||
exceptRes.add(Argument.buildHeader("xxx", "xxx"));
|
||||
exceptRes.add(Argument.buildQuery("yyy", "yyy"));
|
||||
exceptRes.add(Argument.buildCallerIP("127.0.0.1"));
|
||||
exceptRes.add(Argument.buildCustom("xxx", "xxx"));
|
||||
exceptRes.add(Argument.buildCallerService(MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE));
|
||||
assertThat(arguments).isEqualTo(exceptRes);
|
||||
|
||||
rateLimitRuleArgumentServletResolver2.getArguments(request, MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE);
|
||||
rateLimitRuleArgumentServletResolver3.getArguments(request, MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE);
|
||||
rateLimitRuleArgumentServletResolver4.getArguments(request, MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE);
|
||||
}
|
||||
|
||||
@SpringBootApplication
|
||||
protected static class TestApplication {
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
{
|
||||
"id": "f7560cce829e4d4c8556a6be63539af5",
|
||||
"service": "xxx",
|
||||
"namespace": "default",
|
||||
"subset": {},
|
||||
"priority": 0,
|
||||
"resource": "QPS",
|
||||
"type": "GLOBAL",
|
||||
"labels": {},
|
||||
"amounts": [
|
||||
{
|
||||
"maxAmount": 1,
|
||||
"validDuration": "1s",
|
||||
"precision": null,
|
||||
"startAmount": null,
|
||||
"minAmount": null
|
||||
}
|
||||
],
|
||||
"action": "REJECT",
|
||||
"disable": false,
|
||||
"report": null,
|
||||
"ctime": "2022-12-11 21:56:59",
|
||||
"mtime": "2023-03-10 15:40:33",
|
||||
"revision": "6eec6f416bee40ecbf664c93add61358",
|
||||
"service_token": null,
|
||||
"adjuster": null,
|
||||
"regex_combine": true,
|
||||
"amountMode": "GLOBAL_TOTAL",
|
||||
"failover": "FAILOVER_LOCAL",
|
||||
"cluster": null,
|
||||
"method": {
|
||||
"type": "EXACT",
|
||||
"value": "/xxx",
|
||||
"value_type": "TEXT"
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"type": "CALLER_SERVICE",
|
||||
"key": "default",
|
||||
"value": {
|
||||
"type": "EXACT",
|
||||
"value": "xxx",
|
||||
"value_type": "TEXT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "HEADER",
|
||||
"key": "xxx",
|
||||
"value": {
|
||||
"type": "EXACT",
|
||||
"value": "xxx",
|
||||
"value_type": "TEXT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "QUERY",
|
||||
"key": "yyy",
|
||||
"value": {
|
||||
"type": "EXACT",
|
||||
"value": "yyy",
|
||||
"value_type": "TEXT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "METHOD",
|
||||
"key": "$method",
|
||||
"value": {
|
||||
"type": "EXACT",
|
||||
"value": "GET",
|
||||
"value_type": "TEXT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CALLER_IP",
|
||||
"key": "$caller_ip",
|
||||
"value": {
|
||||
"type": "EXACT",
|
||||
"value": "127.0.0.1",
|
||||
"value_type": "TEXT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CUSTOM",
|
||||
"key": "xxx",
|
||||
"value": {
|
||||
"type": "EXACT",
|
||||
"value": "xxx",
|
||||
"value_type": "TEXT"
|
||||
}
|
||||
}
|
||||
],
|
||||
"name": "xxx",
|
||||
"etime": "2023-03-10 15:40:33",
|
||||
"max_queue_delay": 30
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.common.constant;
|
||||
|
||||
/**
|
||||
* Built-in system http header fields.
|
||||
*/
|
||||
public final class HeaderConstant {
|
||||
|
||||
/**
|
||||
* The called service returns the real call result of its own processing request.
|
||||
*/
|
||||
public static final String INTERNAL_CALLEE_RET_STATUS = "internal-callee-retstatus";
|
||||
|
||||
/**
|
||||
* The name of the rule that the current limit/circiutbreaker rule takes effect.
|
||||
*/
|
||||
public static final String INTERNAL_ACTIVE_RULE_NAME = "internal-callee-activerule";
|
||||
|
||||
/**
|
||||
* The name information of the called service.
|
||||
*/
|
||||
public static final String INTERNAL_CALLEE_SERVICE_ID = "internal-callee-serviceid";
|
||||
|
||||
/**
|
||||
* The name information of the called instance host.
|
||||
*/
|
||||
public static final String INTERNAL_CALLEE_INSTANCE_HOST = "internal-callee-instance-host";
|
||||
|
||||
/**
|
||||
* The name information of the called instance port.
|
||||
*/
|
||||
public static final String INTERNAL_CALLEE_INSTANCE_PORT = "internal-callee-instance-port";
|
||||
|
||||
private HeaderConstant() {
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.common.spi.impl;
|
||||
|
||||
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.spi.InstanceMetadataProvider;
|
||||
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
|
||||
|
||||
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAME;
|
||||
import static com.tencent.cloud.common.constant.MetadataConstant.DefaultMetadata.DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE;
|
||||
import static com.tencent.cloud.common.metadata.MetadataContext.LOCAL_NAMESPACE;
|
||||
import static com.tencent.cloud.common.metadata.MetadataContext.LOCAL_SERVICE;
|
||||
|
||||
/**
|
||||
* DefaultInstanceMetadataProvider.
|
||||
* provide DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE, DEFAULT_METADATA_SOURCE_SERVICE_NAME
|
||||
*
|
||||
* @author sean yu
|
||||
*/
|
||||
public class DefaultInstanceMetadataProvider implements InstanceMetadataProvider {
|
||||
|
||||
private final ApplicationContextAwareUtils applicationContextAwareUtils;
|
||||
|
||||
// ensure ApplicationContextAwareUtils init before
|
||||
public DefaultInstanceMetadataProvider(ApplicationContextAwareUtils applicationContextAwareUtils) {
|
||||
this.applicationContextAwareUtils = applicationContextAwareUtils;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getMetadata() {
|
||||
return new HashMap<String, String>() {{
|
||||
put(DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE, LOCAL_NAMESPACE);
|
||||
put(DEFAULT_METADATA_SOURCE_SERVICE_NAME, LOCAL_SERVICE);
|
||||
}};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getDisposableMetadataKeys() {
|
||||
return new HashSet<>(Arrays.asList(DEFAULT_METADATA_SOURCE_SERVICE_NAMESPACE, DEFAULT_METADATA_SOURCE_SERVICE_NAME));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.common.util;
|
||||
|
||||
/**
|
||||
* Request Label Utils.
|
||||
*/
|
||||
public final class RequestLabelUtils {
|
||||
|
||||
private RequestLabelUtils() {
|
||||
}
|
||||
|
||||
public static String convertLabel(String label) {
|
||||
label = label.replaceAll("\"|\\{|\\}", "")
|
||||
.replaceAll(",", "|");
|
||||
return label;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.common.util;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import com.netflix.zuul.context.RequestContext;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.web.util.UriUtils;
|
||||
import org.springframework.web.util.WebUtils;
|
||||
|
||||
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.REQUEST_URI_KEY;
|
||||
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY;
|
||||
|
||||
/**
|
||||
* Utils for Zuul filter.
|
||||
*
|
||||
* @author Haotian Zhang
|
||||
*/
|
||||
public final class ZuulFilterUtils {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ZuulFilterUtils.class);
|
||||
|
||||
private ZuulFilterUtils() {
|
||||
}
|
||||
|
||||
public static String getServiceId(RequestContext context) {
|
||||
String serviceId = (String) context.get(SERVICE_ID_KEY);
|
||||
if (StringUtils.isBlank(serviceId)) {
|
||||
URL url = context.getRouteHost();
|
||||
if (url != null) {
|
||||
serviceId = url.getAuthority();
|
||||
context.set(SERVICE_ID_KEY, serviceId);
|
||||
}
|
||||
}
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public static String getPath(RequestContext context) {
|
||||
HttpServletRequest request = context.getRequest();
|
||||
String uri = request.getRequestURI();
|
||||
String contextURI = (String) context.get(REQUEST_URI_KEY);
|
||||
if (contextURI != null) {
|
||||
try {
|
||||
uri = UriUtils.encodePath(contextURI, characterEncoding(request));
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOGGER.debug("unable to encode uri path from context, falling back to uri from request", e);
|
||||
}
|
||||
}
|
||||
// remove double slashes
|
||||
uri = uri.replace("//", "/");
|
||||
return uri;
|
||||
}
|
||||
|
||||
private static String characterEncoding(HttpServletRequest request) {
|
||||
return request.getCharacterEncoding() != null ? request.getCharacterEncoding()
|
||||
: WebUtils.DEFAULT_CHARACTER_ENCODING;
|
||||
}
|
||||
}
|
0
spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-example-b/src/main/java/com/tencent/cloud/polaris/circuitbreaker/example/ServiceB.java → spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-callee-service/src/main/java/com/tencent/cloud/polaris/circuitbreaker/example/ServiceB.java
0
spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-example-b/src/main/java/com/tencent/cloud/polaris/circuitbreaker/example/ServiceB.java → spring-cloud-tencent-examples/polaris-circuitbreaker-example/polaris-circuitbreaker-callee-service/src/main/java/com/tencent/cloud/polaris/circuitbreaker/example/ServiceB.java
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue