Commit 4c53fd2b by Dominik Mostek

Fallback cause is propagated to fallback provider

Cause can now be propagated to fallback provider in order to respond accordingly to a exception type. New interface is introduced in order not to break any existing falback providers.
parent 6dbdb1d8
/*
* Copyright 2017 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
*
* http://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.netflix.zuul.filters.route;
import org.springframework.http.client.ClientHttpResponse;
/**
* Extension of {@link ZuulFallbackProvider} which adds possibility to choose proper response
* based on the exception which caused the main method to fail.
*
* @author Dominik Mostek
*/
public interface FallbackProvider extends ZuulFallbackProvider {
/**
* Provides a fallback response based on the cause of the failed execution.
*
* @param cause cause of the main method failure
* @return the fallback response
*/
ClientHttpResponse fallbackResponse(Throwable cause);
}
...@@ -23,7 +23,9 @@ import org.springframework.http.client.ClientHttpResponse; ...@@ -23,7 +23,9 @@ import org.springframework.http.client.ClientHttpResponse;
/** /**
* Provides fallback when a failure occurs on a route. * Provides fallback when a failure occurs on a route.
* @author Ryan Baxter * @author Ryan Baxter
* @deprecated Use {@link FallbackProvider}
*/ */
@Deprecated
public interface ZuulFallbackProvider { public interface ZuulFallbackProvider {
/** /**
......
...@@ -22,6 +22,7 @@ import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; ...@@ -22,6 +22,7 @@ import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommand; import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommand;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext; import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext;
import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider; import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.ClientHttpResponse;
import com.netflix.client.AbstractLoadBalancerAwareClient; import com.netflix.client.AbstractLoadBalancerAwareClient;
import com.netflix.client.ClientRequest; import com.netflix.client.ClientRequest;
...@@ -68,7 +69,13 @@ public abstract class AbstractRibbonCommand<LBC extends AbstractLoadBalancerAwar ...@@ -68,7 +69,13 @@ public abstract class AbstractRibbonCommand<LBC extends AbstractLoadBalancerAwar
public AbstractRibbonCommand(String commandKey, LBC client, public AbstractRibbonCommand(String commandKey, LBC client,
RibbonCommandContext context, ZuulProperties zuulProperties, RibbonCommandContext context, ZuulProperties zuulProperties,
ZuulFallbackProvider fallbackProvider, IClientConfig config) { ZuulFallbackProvider fallbackProvider, IClientConfig config) {
super(getSetter(commandKey, zuulProperties)); this(getSetter(commandKey, zuulProperties), client, context, fallbackProvider, config);
}
protected AbstractRibbonCommand(Setter setter, LBC client,
RibbonCommandContext context,
ZuulFallbackProvider fallbackProvider, IClientConfig config) {
super(setter);
this.client = client; this.client = client;
this.context = context; this.context = context;
this.zuulFallbackProvider = fallbackProvider; this.zuulFallbackProvider = fallbackProvider;
...@@ -124,11 +131,24 @@ public abstract class AbstractRibbonCommand<LBC extends AbstractLoadBalancerAwar ...@@ -124,11 +131,24 @@ public abstract class AbstractRibbonCommand<LBC extends AbstractLoadBalancerAwar
@Override @Override
protected ClientHttpResponse getFallback() { protected ClientHttpResponse getFallback() {
if(zuulFallbackProvider != null) { if(zuulFallbackProvider != null) {
return zuulFallbackProvider.fallbackResponse(); return getFallbackResponse();
} }
return super.getFallback(); return super.getFallback();
} }
protected ClientHttpResponse getFallbackResponse() {
if (zuulFallbackProvider instanceof FallbackProvider) {
Throwable cause = getFailedExecutionException();
cause = cause == null ? getExecutionException() : cause;
if (cause == null) {
zuulFallbackProvider.fallbackResponse();
} else {
return ((FallbackProvider) zuulFallbackProvider).fallbackResponse(cause);
}
}
return zuulFallbackProvider.fallbackResponse();
}
public LBC getClient() { public LBC getClient() {
return client; return client;
} }
......
/*
* Copyright 2017 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
*
* http://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.netflix.zuul.filters.route.support;
import com.netflix.client.AbstractLoadBalancerAwareClient;
import com.netflix.client.ClientException;
import com.netflix.client.ClientRequest;
import com.netflix.client.IResponse;
import com.netflix.client.RequestSpecificRetryHandler;
import com.netflix.client.config.IClientConfig;
import com.netflix.client.http.HttpResponse;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.hystrix.exception.HystrixTimeoutException;
import org.junit.Test;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Dominik Mostek
*/
public class RibbonCommandCauseFallbackPropagationTest {
@Test
public void providerIsCalledInCaseOfException() throws Exception {
TestZuulFallbackProviderWithoutCause provider = new TestZuulFallbackProviderWithoutCause(HttpStatus.INTERNAL_SERVER_ERROR);
RuntimeException exception = new RuntimeException("Failed!");
TestRibbonCommand testCommand = new TestRibbonCommand(new TestClient(exception), provider);
ClientHttpResponse response = testCommand.execute();
assertThat(response).isNotNull();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
}
@Test
public void causeIsProvidedForNewInterface() throws Exception {
TestFallbackProvider provider = TestFallbackProvider.withResponse(HttpStatus.NOT_FOUND);
RuntimeException exception = new RuntimeException("Failed!");
TestRibbonCommand testCommand = new TestRibbonCommand(new TestClient(exception), provider);
ClientHttpResponse response = testCommand.execute();
assertThat(response).isNotNull();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
Throwable cause = provider.getCause();
assertThat(cause.getClass()).isEqualTo(exception.getClass());
assertThat(cause.getMessage()).isEqualTo(exception.getMessage());
}
@Test
public void executionExceptionIsUsedInsteadWhenFailedExceptionIsNull() throws Exception {
TestFallbackProvider provider = TestFallbackProvider.withResponse(HttpStatus.BAD_GATEWAY);
final RuntimeException exception = new RuntimeException("Failed!");
TestRibbonCommand testCommand = new TestRibbonCommand(new TestClient(exception), provider) {
@Override
public Throwable getFailedExecutionException() {
return null;
}
@Override
public Throwable getExecutionException() {
return exception;
}
};
ClientHttpResponse response = testCommand.execute();
assertThat(response).isNotNull();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_GATEWAY);
}
@Test
public void timeoutExceptionIsPropagated() throws Exception {
TestFallbackProvider provider = TestFallbackProvider.withResponse(HttpStatus.CONFLICT);
RuntimeException exception = new RuntimeException("Failed!");
TestRibbonCommand testCommand = new TestRibbonCommand(new TestClient(exception), provider, 1) {
@Override
protected ClientRequest createRequest() throws Exception {
Thread.sleep(5);
return super.createRequest();
}
};
ClientHttpResponse response = testCommand.execute();
assertThat(response).isNotNull();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
assertThat(provider.getCause()).isNotNull();
assertThat(provider.getCause().getClass()).isEqualTo(HystrixTimeoutException.class);
}
public static class TestRibbonCommand
extends AbstractRibbonCommand<AbstractLoadBalancerAwareClient<ClientRequest, HttpResponse>, ClientRequest, HttpResponse> {
public TestRibbonCommand(AbstractLoadBalancerAwareClient<ClientRequest, HttpResponse> client, ZuulFallbackProvider fallbackProvider) {
this(client, new ZuulProperties(), fallbackProvider);
}
public TestRibbonCommand(AbstractLoadBalancerAwareClient<ClientRequest, HttpResponse> client,
ZuulProperties zuulProperties,
ZuulFallbackProvider fallbackProvider) {
super("testCommand", client, null, zuulProperties, fallbackProvider);
}
public TestRibbonCommand(AbstractLoadBalancerAwareClient<ClientRequest, HttpResponse> client,
ZuulFallbackProvider fallbackProvider,
int timeout) {
// different name is used because of properties caching
super(getSetter("testCommand2", new ZuulProperties()).andCommandPropertiesDefaults(defauts(timeout)),
client, null, fallbackProvider, null);
}
private static HystrixCommandProperties.Setter defauts(final int timeout) {
return HystrixCommandProperties.Setter().withExecutionTimeoutEnabled(true)
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)
.withExecutionTimeoutInMilliseconds(timeout);
}
@Override
protected ClientRequest createRequest() throws Exception {
return null;
}
}
public static class TestClient extends AbstractLoadBalancerAwareClient {
private final RuntimeException exception;
public TestClient(RuntimeException exception) {
super(null);
this.exception = exception;
}
@Override
public IResponse executeWithLoadBalancer(final ClientRequest request, final IClientConfig requestConfig) throws ClientException {
throw exception;
}
@Override
public RequestSpecificRetryHandler getRequestSpecificRetryHandler(final ClientRequest clientRequest, final IClientConfig iClientConfig) {
return null;
}
@Override
public IResponse execute(final ClientRequest clientRequest, final IClientConfig iClientConfig) throws Exception {
return null;
}
}
public static class TestFallbackProvider implements FallbackProvider {
private final ClientHttpResponse response;
private Throwable cause;
public TestFallbackProvider(final ClientHttpResponse response) {
this.response = response;
}
@Override
public ClientHttpResponse fallbackResponse(final Throwable cause) {
this.cause = cause;
return response;
}
@Override
public String getRoute() {
return "test-route";
}
@Override
public ClientHttpResponse fallbackResponse() {
throw new UnsupportedOperationException("fallback without cause is not supported");
}
public Throwable getCause() {
return cause;
}
public static TestFallbackProvider withResponse(final HttpStatus status) {
return new TestFallbackProvider(getClientHttpResponse(status));
}
}
public static class TestZuulFallbackProviderWithoutCause implements ZuulFallbackProvider {
private final ClientHttpResponse response;
public TestZuulFallbackProviderWithoutCause(final ClientHttpResponse response) {
this.response = response;
}
public TestZuulFallbackProviderWithoutCause(final HttpStatus status) {
this(getClientHttpResponse(status));
}
@Override
public String getRoute() {
return "test-route";
}
@Override
public ClientHttpResponse fallbackResponse() {
return response;
}
}
private static ClientHttpResponse getClientHttpResponse(final HttpStatus status) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return status;
}
@Override
public int getRawStatusCode() throws IOException {
return getStatusCode().value();
}
@Override
public String getStatusText() throws IOException {
return getStatusCode().getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("test".getBytes());
}
@Override
public HttpHeaders getHeaders() {
return new HttpHeaders();
}
};
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment