Skip to content

Commit

Permalink
Fallback cause is propagated to fallback provider
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
DominikMostek committed Sep 7, 2017
1 parent 6dbdb1d commit 4c53fd2
Show file tree
Hide file tree
Showing 4 changed files with 324 additions and 2 deletions.
@@ -0,0 +1,36 @@
/*
* 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);
}
Expand Up @@ -23,7 +23,9 @@
/**
* Provides fallback when a failure occurs on a route.
* @author Ryan Baxter
* @deprecated Use {@link FallbackProvider}
*/
@Deprecated
public interface ZuulFallbackProvider {

/**
Expand Down
Expand Up @@ -22,6 +22,7 @@
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.ZuulFallbackProvider;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.client.ClientHttpResponse;
import com.netflix.client.AbstractLoadBalancerAwareClient;
import com.netflix.client.ClientRequest;
Expand Down Expand Up @@ -68,7 +69,13 @@ public AbstractRibbonCommand(String commandKey, LBC client,
public AbstractRibbonCommand(String commandKey, LBC client,
RibbonCommandContext context, ZuulProperties zuulProperties,
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.context = context;
this.zuulFallbackProvider = fallbackProvider;
Expand Down Expand Up @@ -124,11 +131,24 @@ protected ClientHttpResponse run() throws Exception {
@Override
protected ClientHttpResponse getFallback() {
if(zuulFallbackProvider != null) {
return zuulFallbackProvider.fallbackResponse();
return getFallbackResponse();
}
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() {
return client;
}
Expand Down
@@ -0,0 +1,264 @@
/*
* 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();
}
};
}
}

0 comments on commit 4c53fd2

Please sign in to comment.