Ryan Baxter
5 years ago
18 changed files with 1017 additions and 26 deletions
@ -0,0 +1,93 @@
@@ -0,0 +1,93 @@
|
||||
/* |
||||
* Copyright 2013-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.cloud.gateway.config; |
||||
|
||||
import org.springframework.beans.factory.ObjectProvider; |
||||
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.circuitbreaker.resilience4j.ReactiveResilience4JAutoConfiguration; |
||||
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory; |
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory; |
||||
import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory; |
||||
import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerHystrixFilterFactory; |
||||
import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerResilience4JFilterFactory; |
||||
import org.springframework.cloud.netflix.hystrix.HystrixCircuitBreakerAutoConfiguration; |
||||
import org.springframework.cloud.netflix.hystrix.ReactiveHystrixCircuitBreakerFactory; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.web.reactive.DispatcherHandler; |
||||
|
||||
/** |
||||
* @author Ryan Baxter |
||||
*/ |
||||
@Configuration |
||||
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true) |
||||
@AutoConfigureAfter({ ReactiveResilience4JAutoConfiguration.class, |
||||
HystrixCircuitBreakerAutoConfiguration.class }) |
||||
@ConditionalOnClass({ DispatcherHandler.class, |
||||
ReactiveResilience4JAutoConfiguration.class, |
||||
HystrixCircuitBreakerAutoConfiguration.class }) |
||||
public class GatewayCircuitBreakerAutoConfiguration { |
||||
|
||||
@Configuration |
||||
@ConditionalOnClass({ ReactiveCircuitBreakerFactory.class, |
||||
ReactiveHystrixCircuitBreakerFactory.class }) |
||||
protected static class SpringCloudCircuitBreakerHystrixConfiguration { |
||||
|
||||
@Bean |
||||
@ConditionalOnBean(ReactiveHystrixCircuitBreakerFactory.class) |
||||
public SpringCloudCircuitBreakerHystrixFilterFactory springCloudCircuitBreakerHystrixFilterFactory( |
||||
ReactiveHystrixCircuitBreakerFactory reactiveCircuitBreakerFactory, |
||||
ObjectProvider<DispatcherHandler> dispatcherHandler) { |
||||
return new SpringCloudCircuitBreakerHystrixFilterFactory( |
||||
reactiveCircuitBreakerFactory, dispatcherHandler); |
||||
} |
||||
|
||||
@Bean |
||||
@ConditionalOnMissingBean(FallbackHeadersGatewayFilterFactory.class) |
||||
public FallbackHeadersGatewayFilterFactory fallbackHeadersGatewayFilterFactory() { |
||||
return new FallbackHeadersGatewayFilterFactory(); |
||||
} |
||||
|
||||
} |
||||
|
||||
@Configuration |
||||
@ConditionalOnClass({ ReactiveCircuitBreakerFactory.class, |
||||
ReactiveResilience4JCircuitBreakerFactory.class }) |
||||
protected static class Resilience4JConfiguration { |
||||
|
||||
@Bean |
||||
@ConditionalOnMissingBean(FallbackHeadersGatewayFilterFactory.class) |
||||
public FallbackHeadersGatewayFilterFactory fallbackHeadersGatewayFilterFactory() { |
||||
return new FallbackHeadersGatewayFilterFactory(); |
||||
} |
||||
|
||||
@Bean |
||||
@ConditionalOnBean(ReactiveResilience4JCircuitBreakerFactory.class) |
||||
public SpringCloudCircuitBreakerResilience4JFilterFactory springCloudCircuitBreakerResilience4JFilterFactory( |
||||
ReactiveResilience4JCircuitBreakerFactory reactiveCircuitBreakerFactory, |
||||
ObjectProvider<DispatcherHandler> dispatcherHandler) { |
||||
return new SpringCloudCircuitBreakerResilience4JFilterFactory( |
||||
reactiveCircuitBreakerFactory, dispatcherHandler); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,178 @@
@@ -0,0 +1,178 @@
|
||||
/* |
||||
* Copyright 2013-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.cloud.gateway.filter.factory; |
||||
|
||||
import java.net.URI; |
||||
import java.util.List; |
||||
|
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.beans.factory.ObjectProvider; |
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker; |
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory; |
||||
import org.springframework.cloud.gateway.filter.GatewayFilter; |
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain; |
||||
import org.springframework.cloud.gateway.support.HasRouteId; |
||||
import org.springframework.http.server.reactive.ServerHttpRequest; |
||||
import org.springframework.util.StringUtils; |
||||
import org.springframework.web.reactive.DispatcherHandler; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
import org.springframework.web.util.UriComponentsBuilder; |
||||
|
||||
import static java.util.Collections.singletonList; |
||||
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.containsEncodedParts; |
||||
|
||||
/** |
||||
* @author Ryan Baxter |
||||
*/ |
||||
public abstract class SpringCloudCircuitBreakerFilterFactory extends |
||||
AbstractGatewayFilterFactory<SpringCloudCircuitBreakerFilterFactory.Config> { |
||||
|
||||
private ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory; |
||||
|
||||
private ReactiveCircuitBreaker cb; |
||||
|
||||
private final ObjectProvider<DispatcherHandler> dispatcherHandlerProvider; |
||||
|
||||
// do not use this dispatcherHandler directly, use getDispatcherHandler() instead.
|
||||
private volatile DispatcherHandler dispatcherHandler; |
||||
|
||||
public SpringCloudCircuitBreakerFilterFactory( |
||||
ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory, |
||||
ObjectProvider<DispatcherHandler> dispatcherHandlerProvider) { |
||||
super(Config.class); |
||||
this.reactiveCircuitBreakerFactory = reactiveCircuitBreakerFactory; |
||||
this.dispatcherHandlerProvider = dispatcherHandlerProvider; |
||||
} |
||||
|
||||
private DispatcherHandler getDispatcherHandler() { |
||||
if (dispatcherHandler == null) { |
||||
dispatcherHandler = dispatcherHandlerProvider.getIfAvailable(); |
||||
} |
||||
|
||||
return dispatcherHandler; |
||||
} |
||||
|
||||
@Override |
||||
public List<String> shortcutFieldOrder() { |
||||
return singletonList(NAME_KEY); |
||||
} |
||||
|
||||
@Override |
||||
public GatewayFilter apply(Config config) { |
||||
ReactiveCircuitBreaker cb = reactiveCircuitBreakerFactory.create(config.getId()); |
||||
|
||||
return new GatewayFilter() { |
||||
@Override |
||||
public Mono<Void> filter(ServerWebExchange exchange, |
||||
GatewayFilterChain chain) { |
||||
return cb.run(chain.filter(exchange), t -> { |
||||
if (config.getFallbackUri() == null) { |
||||
return Mono.error(t); |
||||
} |
||||
|
||||
// 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); |
||||
|
||||
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(SpringCloudCircuitBreakerFilterFactory.this) |
||||
.append("name", config.getName()) |
||||
.append("fallback", config.fallbackUri).toString(); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
protected abstract Mono<Void> handleErrorWithoutFallback(Throwable t); |
||||
|
||||
private void addExceptionDetails(Throwable t, ServerWebExchange exchange) { |
||||
ofNullable(t).ifPresent(exception -> exchange.getAttributes() |
||||
.put(CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR, exception)); |
||||
} |
||||
|
||||
@Override |
||||
public String name() { |
||||
return "CircuitBreaker"; |
||||
} |
||||
|
||||
public static class Config implements HasRouteId { |
||||
|
||||
private String name; |
||||
|
||||
private URI fallbackUri; |
||||
|
||||
private String routeId; |
||||
|
||||
@Override |
||||
public void setRouteId(String routeId) { |
||||
this.routeId = routeId; |
||||
} |
||||
|
||||
public String getRouteId() { |
||||
return routeId; |
||||
} |
||||
|
||||
public URI getFallbackUri() { |
||||
return fallbackUri; |
||||
} |
||||
|
||||
public Config setFallbackUri(URI fallbackUri) { |
||||
this.fallbackUri = fallbackUri; |
||||
return this; |
||||
} |
||||
|
||||
public Config setFallbackUri(String fallbackUri) { |
||||
return setFallbackUri(URI.create(fallbackUri)); |
||||
} |
||||
|
||||
public String getName() { |
||||
return name; |
||||
} |
||||
|
||||
public Config setName(String name) { |
||||
this.name = name; |
||||
return this; |
||||
} |
||||
|
||||
public String getId() { |
||||
if (StringUtils.isEmpty(name) && !StringUtils.isEmpty(routeId)) { |
||||
return routeId; |
||||
} |
||||
return name; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,74 @@
@@ -0,0 +1,74 @@
|
||||
/* |
||||
* Copyright 2013-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.cloud.gateway.filter.factory; |
||||
|
||||
import com.netflix.hystrix.exception.HystrixRuntimeException; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.beans.factory.ObjectProvider; |
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory; |
||||
import org.springframework.cloud.gateway.support.ServiceUnavailableException; |
||||
import org.springframework.cloud.gateway.support.TimeoutException; |
||||
import org.springframework.core.annotation.AnnotatedElementUtils; |
||||
import org.springframework.web.bind.annotation.ResponseStatus; |
||||
import org.springframework.web.reactive.DispatcherHandler; |
||||
import org.springframework.web.server.ResponseStatusException; |
||||
|
||||
/** |
||||
* @author Ryan Baxter |
||||
*/ |
||||
public class SpringCloudCircuitBreakerHystrixFilterFactory |
||||
extends SpringCloudCircuitBreakerFilterFactory { |
||||
|
||||
public SpringCloudCircuitBreakerHystrixFilterFactory( |
||||
ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory, |
||||
ObjectProvider<DispatcherHandler> dispatcherHandlerProvider) { |
||||
super(reactiveCircuitBreakerFactory, dispatcherHandlerProvider); |
||||
} |
||||
|
||||
@Override |
||||
protected Mono<Void> handleErrorWithoutFallback(Throwable throwable) { |
||||
if (throwable instanceof HystrixRuntimeException) { |
||||
HystrixRuntimeException e = (HystrixRuntimeException) throwable; |
||||
HystrixRuntimeException.FailureType failureType = e.getFailureType(); |
||||
|
||||
switch (failureType) { |
||||
case TIMEOUT: |
||||
return Mono.error(new TimeoutException()); |
||||
case SHORTCIRCUIT: |
||||
return Mono.error(new ServiceUnavailableException()); |
||||
case COMMAND_EXCEPTION: { |
||||
Throwable cause = e.getCause(); |
||||
|
||||
/* |
||||
* We forsake here the null check for cause as HystrixRuntimeException |
||||
* will always have a cause if the failure type is COMMAND_EXCEPTION. |
||||
*/ |
||||
if (cause instanceof ResponseStatusException |
||||
|| AnnotatedElementUtils.findMergedAnnotation(cause.getClass(), |
||||
ResponseStatus.class) != null) { |
||||
return Mono.error(cause); |
||||
} |
||||
} |
||||
default: |
||||
break; |
||||
} |
||||
} |
||||
return Mono.error(throwable); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
|
||||
/* |
||||
* Copyright 2013-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.cloud.gateway.filter.factory; |
||||
|
||||
import io.github.resilience4j.circuitbreaker.CallNotPermittedException; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.beans.factory.ObjectProvider; |
||||
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory; |
||||
import org.springframework.cloud.gateway.support.ServiceUnavailableException; |
||||
import org.springframework.cloud.gateway.support.TimeoutException; |
||||
import org.springframework.web.reactive.DispatcherHandler; |
||||
|
||||
/** |
||||
* @author Ryan Baxter |
||||
*/ |
||||
public class SpringCloudCircuitBreakerResilience4JFilterFactory |
||||
extends SpringCloudCircuitBreakerFilterFactory { |
||||
|
||||
public SpringCloudCircuitBreakerResilience4JFilterFactory( |
||||
ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory, |
||||
ObjectProvider<DispatcherHandler> dispatcherHandlerProvider) { |
||||
super(reactiveCircuitBreakerFactory, dispatcherHandlerProvider); |
||||
} |
||||
|
||||
@Override |
||||
protected Mono<Void> handleErrorWithoutFallback(Throwable t) { |
||||
if (java.util.concurrent.TimeoutException.class.isInstance(t)) { |
||||
return Mono.error(new TimeoutException()); |
||||
} |
||||
if (CallNotPermittedException.class.isInstance(t)) { |
||||
return Mono.error(new ServiceUnavailableException()); |
||||
} |
||||
return Mono.error(t); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,108 @@
@@ -0,0 +1,108 @@
|
||||
/* |
||||
* Copyright 2013-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.cloud.gateway.filter.factory; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
import org.springframework.cloud.gateway.test.BaseWebClientTests; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.util.Assert; |
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8; |
||||
import static org.springframework.http.MediaType.TEXT_HTML; |
||||
|
||||
/** |
||||
* @author Ryan Baxter |
||||
*/ |
||||
public abstract class SpringCloudCircuitBreakerFilterFactoryTests |
||||
extends BaseWebClientTests { |
||||
|
||||
@Test |
||||
public void cbFilterWorks() { |
||||
testClient.get().uri("/get").header("Host", "www.sccbsuccess.org").exchange() |
||||
.expectStatus().isOk().expectHeader() |
||||
.valueEquals(ROUTE_ID_HEADER, "sccb_success_test"); |
||||
} |
||||
|
||||
@Test |
||||
public void cbFilterTimesout() { |
||||
testClient.get().uri("/delay/3").header("Host", "www.sccbtimeout.org").exchange() |
||||
.expectStatus().isEqualTo(HttpStatus.GATEWAY_TIMEOUT).expectBody() |
||||
.jsonPath("$.status") |
||||
.isEqualTo(String.valueOf(HttpStatus.GATEWAY_TIMEOUT.value())); |
||||
} |
||||
|
||||
/* |
||||
* Tests that timeouts bubbling from the underpinning WebClient are treated the same |
||||
* as CircuitBreaker timeouts in terms of outside response. (Internally, timeouts from the |
||||
* WebClient are seen as command failures and trigger the opening of circuit breakers |
||||
* the same way timeouts do; it may be confusing in terms of the CircuitBreaker metrics |
||||
* though) |
||||
*/ |
||||
@Test |
||||
public void timeoutFromWebClient() { |
||||
testClient.get().uri("/delay/10") |
||||
.header("Host", "www.circuitbreakerresponsestall.org").exchange() |
||||
.expectStatus().isEqualTo(HttpStatus.GATEWAY_TIMEOUT); |
||||
} |
||||
|
||||
@Test |
||||
public void filterFallback() { |
||||
testClient.get().uri("/delay/3?a=b") |
||||
.header("Host", "www.circuitbreakerfallback.org").exchange() |
||||
.expectStatus().isOk().expectBody() |
||||
.json("{\"from\":\"circuitbreakerfallbackcontroller\"}"); |
||||
} |
||||
|
||||
@Test |
||||
public void filterWorksJavaDsl() { |
||||
testClient.get().uri("/get").header("Host", "www.circuitbreakerjava.org") |
||||
.exchange().expectStatus().isOk().expectHeader() |
||||
.valueEquals(ROUTE_ID_HEADER, "circuitbreaker_java"); |
||||
} |
||||
|
||||
@Test |
||||
public void filterFallbackJavaDsl() { |
||||
testClient.get().uri("/delay/3").header("Host", "www.circuitbreakerjava.org") |
||||
.exchange().expectStatus().isOk().expectBody() |
||||
.json("{\"from\":\"circuitbreakerfallbackcontroller2\"}"); |
||||
} |
||||
|
||||
@Test |
||||
public void filterConnectFailure() { |
||||
testClient.get().uri("/delay/3") |
||||
.header("Host", "www.circuitbreakerconnectfail.org").exchange() |
||||
.expectStatus().is5xxServerError(); |
||||
} |
||||
|
||||
@Test |
||||
public void filterErrorPage() { |
||||
testClient.get().uri("/delay/3") |
||||
.header("Host", "www.circuitbreakerconnectfail.org").accept(TEXT_HTML) |
||||
.exchange().expectStatus().is5xxServerError().expectBody() |
||||
.consumeWith(res -> { |
||||
final String body = new String(res.getResponseBody(), UTF_8); |
||||
|
||||
Assert.isTrue(body.contains("<h1>Whitelabel Error Page</h1>"), |
||||
"Cannot find the expected white-label error page title in the response"); |
||||
Assert.isTrue( |
||||
body.contains("(type=Internal Server Error, status=500)"), |
||||
"Cannot find the expected error status report in the response"); |
||||
}); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,82 @@
@@ -0,0 +1,82 @@
|
||||
/* |
||||
* Copyright 2013-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.cloud.gateway.filter.factory; |
||||
|
||||
import com.netflix.config.ConfigurationManager; |
||||
import com.netflix.hystrix.Hystrix; |
||||
import com.netflix.hystrix.metric.consumer.HealthCountsStream; |
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
|
||||
import org.springframework.boot.test.context.SpringBootTest; |
||||
import org.springframework.cloud.gateway.filter.GatewayFilter; |
||||
import org.springframework.cloud.netflix.hystrix.ReactiveHystrixCircuitBreakerFactory; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.test.annotation.DirtiesContext; |
||||
import org.springframework.test.context.ContextConfiguration; |
||||
import org.springframework.test.context.junit4.SpringRunner; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.hamcrest.core.StringContains.containsString; |
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; |
||||
import static org.springframework.cloud.gateway.filter.factory.ExceptionFallbackHandler.RETRIEVED_EXCEPTION; |
||||
|
||||
/** |
||||
* @author Ryan Baxter |
||||
*/ |
||||
@RunWith(SpringRunner.class) |
||||
@SpringBootTest(webEnvironment = RANDOM_PORT, properties = { "debug=true", |
||||
"spring.cloud.circuitbreaker.resilience4j.enabled=false" }) |
||||
@ContextConfiguration(classes = SpringCloudCircuitBreakerTestConfig.class) |
||||
@DirtiesContext |
||||
public class SpringCloudCircuitBreakerHystrixFilterFactoryTests |
||||
extends SpringCloudCircuitBreakerFilterFactoryTests { |
||||
|
||||
@Test |
||||
public void hystrixFilterServiceUnavailable() { |
||||
HealthCountsStream.reset(); |
||||
Hystrix.reset(); |
||||
ConfigurationManager.getConfigInstance() |
||||
.setProperty("hystrix.command.failcmd.circuitBreaker.forceOpen", true); |
||||
|
||||
testClient.get().uri("/delay/3").header("Host", "www.sccbfailure.org").exchange() |
||||
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE); |
||||
|
||||
HealthCountsStream.reset(); |
||||
Hystrix.reset(); |
||||
ConfigurationManager.getConfigInstance() |
||||
.setProperty("hystrix.command.failcmd.circuitBreaker.forceOpen", false); |
||||
} |
||||
|
||||
@Test |
||||
public void hystrixFilterExceptionFallback() { |
||||
testClient.get().uri("/delay/3") |
||||
.header("Host", "www.circuitbreakerexceptionfallback.org").exchange() |
||||
.expectStatus().isOk().expectHeader() |
||||
.value(RETRIEVED_EXCEPTION, containsString("HystrixTimeoutException")); |
||||
} |
||||
|
||||
@Test |
||||
public void toStringFormat() { |
||||
SpringCloudCircuitBreakerFilterFactory.Config config = new SpringCloudCircuitBreakerFilterFactory.Config() |
||||
.setName("myname").setFallbackUri("forward:/myfallback"); |
||||
GatewayFilter filter = new SpringCloudCircuitBreakerHystrixFilterFactory( |
||||
new ReactiveHystrixCircuitBreakerFactory(), null).apply(config); |
||||
assertThat(filter.toString()).contains("myname").contains("forward:/myfallback"); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,98 @@
@@ -0,0 +1,98 @@
|
||||
/* |
||||
* Copyright 2013-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.cloud.gateway.filter.factory; |
||||
|
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.boot.SpringBootConfiguration; |
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; |
||||
import org.springframework.boot.test.context.SpringBootTest; |
||||
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory; |
||||
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory; |
||||
import org.springframework.cloud.client.circuitbreaker.Customizer; |
||||
import org.springframework.cloud.gateway.filter.GatewayFilter; |
||||
import org.springframework.cloud.gateway.test.BaseWebClientTests; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Import; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.test.annotation.DirtiesContext; |
||||
import org.springframework.test.context.ContextConfiguration; |
||||
import org.springframework.test.context.junit4.SpringRunner; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.hamcrest.core.StringContains.containsString; |
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; |
||||
import static org.springframework.cloud.gateway.filter.factory.ExceptionFallbackHandler.RETRIEVED_EXCEPTION; |
||||
|
||||
/** |
||||
* @author Ryan Baxter |
||||
*/ |
||||
@RunWith(SpringRunner.class) |
||||
@SpringBootTest(webEnvironment = RANDOM_PORT, properties = { "debug=true", |
||||
"spring.cloud.circuitbreaker.hystrix.enabled=false" }) |
||||
@ContextConfiguration( |
||||
classes = SpringCloudCircuitBreakerResilience4JFilterFactoryTests.Config.class) |
||||
@DirtiesContext |
||||
public class SpringCloudCircuitBreakerResilience4JFilterFactoryTests |
||||
extends SpringCloudCircuitBreakerFilterFactoryTests { |
||||
|
||||
@Autowired |
||||
private Resilience4JCircuitBreakerFactory factory; |
||||
|
||||
@Test |
||||
public void r4jFilterServiceUnavailable() { |
||||
testClient.get().uri("/delay/3").header("Host", "www.sccbfailure.org").exchange() |
||||
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE); |
||||
} |
||||
|
||||
@Test |
||||
public void r4jFilterExceptionFallback() { |
||||
testClient.get().uri("/delay/3") |
||||
.header("Host", "www.circuitbreakerexceptionfallback.org").exchange() |
||||
.expectStatus().isOk().expectHeader() |
||||
.value(RETRIEVED_EXCEPTION, containsString("TimeoutException")); |
||||
} |
||||
|
||||
@Test |
||||
public void toStringFormat() { |
||||
SpringCloudCircuitBreakerFilterFactory.Config config = new SpringCloudCircuitBreakerFilterFactory.Config() |
||||
.setName("myname").setFallbackUri("forward:/myfallback"); |
||||
GatewayFilter filter = new SpringCloudCircuitBreakerResilience4JFilterFactory( |
||||
new ReactiveResilience4JCircuitBreakerFactory(), null).apply(config); |
||||
assertThat(filter.toString()).contains("myname").contains("forward:/myfallback"); |
||||
} |
||||
|
||||
@EnableAutoConfiguration |
||||
@SpringBootConfiguration |
||||
@Import(BaseWebClientTests.DefaultTestConfig.class) |
||||
@RestController |
||||
static class Config extends SpringCloudCircuitBreakerTestConfig { |
||||
|
||||
@Bean |
||||
public Customizer<ReactiveResilience4JCircuitBreakerFactory> slowCusomtizer() { |
||||
return factory -> { |
||||
factory.addCircuitBreakerCustomizer( |
||||
cb -> cb.transitionToForcedOpenState(), "failcmd"); |
||||
}; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,116 @@
@@ -0,0 +1,116 @@
|
||||
/* |
||||
* Copyright 2013-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.cloud.gateway.filter.factory; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.Map; |
||||
|
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.beans.factory.annotation.Value; |
||||
import org.springframework.boot.SpringBootConfiguration; |
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; |
||||
import org.springframework.cloud.gateway.route.RouteLocator; |
||||
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; |
||||
import org.springframework.cloud.gateway.test.BaseWebClientTests; |
||||
import org.springframework.cloud.netflix.ribbon.RibbonClient; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Import; |
||||
import org.springframework.web.bind.annotation.RequestMapping; |
||||
import org.springframework.web.bind.annotation.RequestParam; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
import org.springframework.web.reactive.function.server.RouterFunction; |
||||
import org.springframework.web.reactive.function.server.ServerRequest; |
||||
import org.springframework.web.reactive.function.server.ServerResponse; |
||||
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR; |
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET; |
||||
import static org.springframework.web.reactive.function.server.RouterFunctions.route; |
||||
|
||||
/** |
||||
* @author Ryan Baxter |
||||
*/ |
||||
@EnableAutoConfiguration |
||||
@SpringBootConfiguration |
||||
@Import(BaseWebClientTests.DefaultTestConfig.class) |
||||
@RestController |
||||
@RibbonClient(name = "badservice", configuration = TestBadRibbonConfig.class) |
||||
public class SpringCloudCircuitBreakerTestConfig { |
||||
|
||||
@Value("${test.uri}") |
||||
private String uri; |
||||
|
||||
@RequestMapping("/circuitbreakerFallbackController") |
||||
public Map<String, String> fallbackcontroller(@RequestParam("a") String a) { |
||||
return Collections.singletonMap("from", "circuitbreakerfallbackcontroller"); |
||||
} |
||||
|
||||
@RequestMapping("/circuitbreakerFallbackController2") |
||||
public Map<String, String> fallbackcontroller2() { |
||||
return Collections.singletonMap("from", "circuitbreakerfallbackcontroller2"); |
||||
} |
||||
|
||||
@Bean |
||||
public RouteLocator circuitBreakerRouteLocator(RouteLocatorBuilder builder) { |
||||
return builder.routes() |
||||
.route("circuitbreaker_java", r -> r.host("**.circuitbreakerjava.org") |
||||
.filters(f -> f.prefixPath("/httpbin") |
||||
.circuitBreaker(config -> config.setFallbackUri( |
||||
"forward:/circuitbreakerFallbackController2"))) |
||||
.uri(uri)) |
||||
.route("circuitbreaker_connection_failure", r -> r |
||||
.host("**.circuitbreakerconnectfail.org") |
||||
.filters(f -> f.prefixPath("/httpbin").circuitBreaker(config -> { |
||||
})).uri("lb:badservice")) |
||||
/* |
||||
* This is a route encapsulated in a circuit breaker that is ready to wait |
||||
* for a response far longer than the underpinning WebClient would. |
||||
*/ |
||||
.route("circuitbreaker_response_stall", |
||||
r -> r.host("**.circuitbreakerresponsestall.org") |
||||
.filters(f -> f.prefixPath("/httpbin").circuitBreaker( |
||||
config -> config.setName("stalling-command"))) |
||||
.uri(uri)) |
||||
.build(); |
||||
} |
||||
|
||||
@Bean |
||||
CircuitBreakerExceptionFallbackHandler exceptionFallbackHandler() { |
||||
return new CircuitBreakerExceptionFallbackHandler(); |
||||
} |
||||
|
||||
@Bean |
||||
RouterFunction<ServerResponse> routerFunction( |
||||
CircuitBreakerExceptionFallbackHandler exceptionFallbackHandler) { |
||||
return route(GET("/circuitbreakerExceptionFallback"), |
||||
exceptionFallbackHandler::retrieveExceptionInfo); |
||||
} |
||||
|
||||
} |
||||
|
||||
class CircuitBreakerExceptionFallbackHandler { |
||||
|
||||
static final String RETRIEVED_EXCEPTION = "Retrieved-Exception"; |
||||
|
||||
Mono<ServerResponse> retrieveExceptionInfo(ServerRequest serverRequest) { |
||||
String exceptionName = serverRequest |
||||
.attribute(CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR) |
||||
.map(exception -> exception.getClass().getName()).orElse(""); |
||||
return ServerResponse.ok().header(RETRIEVED_EXCEPTION, exceptionName).build(); |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue