/** * Copyright 2014 Opower, Inc. * 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 com.opower.rest.client.generator.hystrix; import com.google.common.base.Optional; import com.google.common.base.Throwables; import com.netflix.config.ConfigurationManager; import com.netflix.hystrix.HystrixCommand; import com.netflix.hystrix.exception.HystrixRuntimeException; import java.lang.reflect.InvocationTargetException; import java.util.Properties; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import org.hamcrest.Description; import org.hamcrest.TypeSafeMatcher; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import static com.opower.rest.client.generator.hystrix.TestHystrixCommandKeys.TEST_CMD; import static com.opower.rest.client.generator.hystrix.TestHystrixGroupKeys.TEST_GROUP; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; /** * * Tests for the HystrixProxyCommand. Mainly to verify the exception behavior. * */ public class TestProxyCommandExceptionHandling { private static final String EXCEPTION_MESSAGE = "testMessage"; @Rule public final ExpectedException testRuleExpectedException = ExpectedException.none(); /** * Things that cause the command to fail before the underlying work completes (short circuit, timeout, rejected from thread * pool etc) and also have a failed fallback should just propagate the HystrixRuntimeException. * @throws Throwable for convenience */ @Test public void exceptionInFallbackBeforeWork() throws Throwable { TestCommand command = new TestCommand(); command.setForceCircuitOpen(true); command.setFallback(new Callable<Object>() { @Override public Object call() throws Exception { throw new RuntimeException(EXCEPTION_MESSAGE); } }); this.testRuleExpectedException.expect(HystrixRuntimeException.class); this.testRuleExpectedException.expectMessage("TEST_CMD short-circuited and failed retrieving fallback"); this.testRuleExpectedException.expect(new FallbackExceptionMatcher(RuntimeException.class, EXCEPTION_MESSAGE)); this.testRuleExpectedException.expect(new FailureTypeMatcher(HystrixRuntimeException.FailureType.SHORTCIRCUIT)); HystrixCommandInvocationHandler.execute(command); } /** * Things that cause the command to fail before the underlying work completes (see above) should return the * result of a successful fallback. * @throws Throwable for convenience */ @Test public void successfulFallbackBeforeWorkCompletes() throws Throwable { TestCommand command = new TestCommand(); command.setForceCircuitOpen(true); int result = (int) HystrixCommandInvocationHandler.execute(command); assertThat(result, is(0)); } /** * Things that cause a command to fail before the underlying work completes (see above) with no fallback * should just propagate the HystrixRuntimeException. * @throws Throwable for convenience */ @Test public void noFallbackAfterShortCircuit() throws Throwable { TestCommand command = new TestCommand(); command.setForceCircuitOpen(true); command.setFallback(null); this.testRuleExpectedException.expect(HystrixRuntimeException.class); this.testRuleExpectedException.expectMessage("TEST_CMD short-circuited and fallback disabled"); this.testRuleExpectedException.expect(new FailureTypeMatcher(HystrixRuntimeException.FailureType.SHORTCIRCUIT)); HystrixCommandInvocationHandler.execute(command); } /** * This is a similar test just showing the behavior works regardless of the cause of the pre-work failure. In this case it's * a hystrix timeout. * @throws Throwable for convenience */ @Test public void timeoutNoFallback() throws Throwable { TestCommand command = new TestCommand(); command.setTimeout(1); command.setFallback(null); command.setBehavior(new Callable<Object>() { @Override public Object call() throws Exception { Thread.sleep(TimeUnit.MINUTES.toMillis(1)); return 1; } }); this.testRuleExpectedException.expect(HystrixRuntimeException.class); this.testRuleExpectedException.expect(new FailureTypeMatcher(HystrixRuntimeException.FailureType.TIMEOUT)); this.testRuleExpectedException.expectMessage("TEST_CMD timed-out and fallback disabled."); HystrixCommandInvocationHandler.execute(command); } /** * When the underlying work throws, successful fallbacks should be returned. * @throws Throwable for convenience */ @Test public void exceptionInWorkWithFallbackThatSucceeds() throws Throwable { TestCommand command = new TestCommand(); command.setBehavior(new Callable<Object>() { @Override public Object call() throws Exception { throw new RuntimeException(); } }); int result = (int)HystrixCommandInvocationHandler.execute(command); assertThat(result, is(0)); } /** * When the underlying work throws and the fallback also throws, the HystrixRuntimeException should be propagated. * @throws Throwable for convenience */ @Test public void exceptionInWorkFallbackFails() throws Throwable { TestCommand command = new TestCommand(); command.setBehavior(new Callable<Object>() { @Override public Object call() throws Exception { throw new RuntimeException(); } }); command.setFallback(new Callable<Object>() { @Override public Object call() throws Exception { throw new IllegalStateException(EXCEPTION_MESSAGE); } }); this.testRuleExpectedException.expect(HystrixRuntimeException.class); this.testRuleExpectedException.expectMessage("TEST_CMD failed and failed retrieving fallback."); this.testRuleExpectedException.expect(new FailureTypeMatcher(HystrixRuntimeException.FailureType.COMMAND_EXCEPTION)); this.testRuleExpectedException.expect(new FallbackExceptionMatcher(IllegalStateException.class, EXCEPTION_MESSAGE)); HystrixCommandInvocationHandler.execute(command); } /** * In the case of our REST clients, they will always throw InvocationTargetExceptions (since they are jdk proxies). This * test is for the case where potentially they throw a different type of exception. In that case that exception should be * propagated as is. * @throws Throwable for convenience */ @Test public void nonProxyExceptionInWorkNoFallback() throws Throwable { TestCommand command = new TestCommand(); command.setBehavior(new Callable<Object>() { @Override public Object call() throws Exception { throw new IllegalStateException(EXCEPTION_MESSAGE); } }); command.setFallback(null); this.testRuleExpectedException.expect(IllegalStateException.class); this.testRuleExpectedException.expectMessage(EXCEPTION_MESSAGE); HystrixCommandInvocationHandler.execute(command); } /** * When the underlying work throws InvocationTargetExceptions (such as in the case of our clients) the target exception * should be this.testRuleExpectedException. * @throws Throwable for convenience */ @Test public void proxyExceptionInWorkNoFallback() throws Throwable { TestCommand command = new TestCommand(); command.setBehavior(new Callable<Object>() { @Override public Object call() throws Exception { throw new InvocationTargetException(new IllegalStateException(EXCEPTION_MESSAGE)); } }); command.setFallback(null); this.testRuleExpectedException.expect(IllegalStateException.class); this.testRuleExpectedException.expectMessage(EXCEPTION_MESSAGE); HystrixCommandInvocationHandler.execute(command); } /** * Because of the way hystrix caches settings for commands, we have to do some fancy dancing. */ private class TestCommand extends HystrixCommand { private Callable<Object> behavior; private Optional<Callable<Object>> fallback; public TestCommand() { super(HystrixCommand.Setter.withGroupKey(TEST_GROUP).andCommandKey(TEST_CMD)); setTimeout((int)TimeUnit.SECONDS.toMillis(1)); setForceCircuitOpen(false); setFallback(new Callable<Object>() { @Override public Object call() throws Exception { return 0; } }); setBehavior(new Callable<Object>() { @Override public Object call() throws Exception { return 1; } }); } private String propName(String prop) { return String.format("hystrix.command.%s.%s", TEST_CMD.name(), prop); } private void setProp(String name, Object value) { Properties props = new Properties(); props.put(name, value); ConfigurationManager.loadProperties(props); } public void setTimeout(int timeoutInMillis) { setProp(propName(".execution.isolation.thread.timeoutInMilliseconds"), timeoutInMillis); } public void setForceCircuitOpen(boolean forceCircuitOpen) { setProp(propName("circuitBreaker.forceOpen"), forceCircuitOpen); } public void setBehavior(Callable<Object> behavior) { this.behavior = behavior; } public void setFallback(Callable<Object> fallback) { this.fallback = Optional.fromNullable(fallback); setProp(propName("fallback.enabled"), this.fallback.isPresent()); } @Override protected Object run() throws Exception { return this.behavior.call(); } @Override protected Object getFallback() { try { return this.fallback.get().call(); } catch (Exception e) { throw Throwables.propagate(e); } } } private final class FailureTypeMatcher extends TypeSafeMatcher<HystrixRuntimeException> { private final HystrixRuntimeException.FailureType expectedType; private FailureTypeMatcher(HystrixRuntimeException.FailureType expectedType) { this.expectedType = expectedType; } @Override protected boolean matchesSafely(HystrixRuntimeException item) { return this.expectedType.equals(item.getFailureType()); } @Override public void describeTo(Description description) { description.appendText(String.format("expected failure type [%s] ", this.expectedType)); } @Override protected void describeMismatchSafely(HystrixRuntimeException item, Description mismatchDescription) { mismatchDescription.appendText(String.format("failure type [%s]", item.getFailureType())); } } private final class FallbackExceptionMatcher extends TypeSafeMatcher<HystrixRuntimeException> { private final Class<?> expectedExceptionType; private final String expectedMessage; private FallbackExceptionMatcher(Class<?> expectedExceptionType, String expectedMessage) { this.expectedExceptionType = expectedExceptionType; this.expectedMessage = expectedMessage; } @Override protected boolean matchesSafely(HystrixRuntimeException item) { return item.getFallbackException() != null && this.expectedExceptionType.equals(item.getFallbackException().getClass()) && this.expectedMessage.equals(item.getFallbackException().getMessage()); } @Override public void describeTo(Description description) { description.appendText(String.format("expected fallbackException of type [%s] with message [%s]", this.expectedExceptionType, this.expectedMessage)); } @Override protected void describeMismatchSafely(HystrixRuntimeException item, Description mismatchDescription) { mismatchDescription.appendText(String.format("[%s] with message [%s]", item.getFallbackException().getClass(), item.getFallbackException().getMessage())); } } }