/* TestCircuitBreaker.java * * Copyright 2009-2015 Comcast Interactive Media, LLC. * * 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.fishwife.jrugged; import java.util.concurrent.Callable; import junit.framework.Assert; import org.junit.Before; import org.junit.Test; import static junit.framework.Assert.assertNull; import static org.easymock.EasyMock.createMock; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.verify; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class TestCircuitBreaker { private CircuitBreaker impl; private Callable<Object> mockCallable; private Runnable mockRunnable; Status theStatus; @SuppressWarnings("unchecked") @Before public void setUp() { impl = new CircuitBreaker(); mockCallable = createMock(Callable.class); mockRunnable = createMock(Runnable.class); } @Test public void testInvokeWithRunnableResultAndResultReturnsResult() throws Exception { final Object result = new Object(); mockRunnable.run(); replay(mockRunnable); Object theReturned = impl.invoke(mockRunnable, result); verify(mockRunnable); assertSame(result, theReturned); } @Test public void testInvokeWithRunnableResultAndByPassReturnsResult() throws Exception { final Object result = new Object(); impl.setByPassState(true); mockRunnable.run(); replay(mockRunnable); Object theReturned = impl.invoke(mockRunnable, result); verify(mockRunnable); assertSame(result, theReturned); } @Test(expected = CircuitBreakerException.class) public void testInvokeWithRunnableResultAndTripHardReturnsException() throws Exception { final Object result = new Object(); impl.tripHard(); mockRunnable.run(); replay(mockRunnable); impl.invoke(mockRunnable, result); verify(mockRunnable); } @Test public void testInvokeWithRunnableDoesNotError() throws Exception { mockRunnable.run(); replay(mockRunnable); impl.invoke(mockRunnable); verify(mockRunnable); } @Test public void testInvokeWithRunnableAndByPassDoesNotError() throws Exception { impl.setByPassState(true); mockRunnable.run(); replay(mockRunnable); impl.invoke(mockRunnable); verify(mockRunnable); } @Test(expected = CircuitBreakerException.class) public void testInvokeWithRunnableAndTripHardReturnsException() throws Exception { impl.tripHard(); mockRunnable.run(); replay(mockRunnable); impl.invoke(mockRunnable); verify(mockRunnable); } @Test public void testStaysClosedOnSuccess() throws Exception { impl.state = CircuitBreaker.BreakerState.CLOSED; final Object obj = new Object(); expect(mockCallable.call()).andReturn(obj); replay(mockCallable); Object result = impl.invoke(mockCallable); verify(mockCallable); assertSame(obj, result); assertEquals(CircuitBreaker.BreakerState.CLOSED, impl.state); } @Test public void testOpensOnFailure() throws Exception { long start = System.currentTimeMillis(); impl.state = CircuitBreaker.BreakerState.OPEN; expect(mockCallable.call()).andThrow(new RuntimeException()); replay(mockCallable); try { impl.invoke(mockCallable); fail("should have thrown an exception"); } catch (RuntimeException expected) { } long end = System.currentTimeMillis(); verify(mockCallable); assertEquals(CircuitBreaker.BreakerState.OPEN, impl.state); assertTrue(impl.lastFailure.get() >= start); assertTrue(impl.lastFailure.get() <= end); } @Test public void testOpenDuringCooldownThrowsCBException() throws Exception { impl.state = CircuitBreaker.BreakerState.OPEN; impl.lastFailure.set(System.currentTimeMillis()); replay(mockCallable); try { impl.invoke(mockCallable); fail("should have thrown an exception"); } catch (CircuitBreakerException expected) { } verify(mockCallable); assertEquals(CircuitBreaker.BreakerState.OPEN, impl.state); } @Test public void testOpenAfterCooldownGoesHalfClosed() throws Exception { impl.state = CircuitBreaker.BreakerState.OPEN; impl.resetMillis.set(1000); impl.lastFailure.set(System.currentTimeMillis() - 2000); assertEquals(Status.DEGRADED, impl.getStatus()); assertEquals(CircuitBreaker.BreakerState.HALF_CLOSED, impl.state); } @Test public void testHalfClosedFailureOpensAgain() throws Exception { impl.state = CircuitBreaker.BreakerState.HALF_CLOSED; impl.resetMillis.set(1000); impl.lastFailure.set(System.currentTimeMillis() - 2000); long start = System.currentTimeMillis(); expect(mockCallable.call()).andThrow(new RuntimeException()); replay(mockCallable); try { impl.invoke(mockCallable); fail("should have thrown exception"); } catch (RuntimeException expected) { } long end = System.currentTimeMillis(); verify(mockCallable); assertEquals(CircuitBreaker.BreakerState.OPEN, impl.state); assertTrue(impl.lastFailure.get() >= start); assertTrue(impl.lastFailure.get() <= end); } @Test public void testGetStatusNotUpdatingIsAttemptLive() throws Exception { impl.resetMillis.set(50); impl.trip(); assertEquals(CircuitBreaker.BreakerState.OPEN, impl.state); assertEquals(false, impl.isAttemptLive); Thread.sleep(200); // The getStatus()->canAttempt() call also updated isAttemptLive to true assertEquals(Status.DEGRADED.getValue(), impl.getStatus().getValue()); assertEquals(false, impl.isAttemptLive); } @Test public void testManualTripAndReset() throws Exception { impl.state = CircuitBreaker.BreakerState.OPEN; final Object obj = new Object(); expect(mockCallable.call()).andReturn(obj); replay(mockCallable); impl.trip(); try { impl.invoke(mockCallable); fail("Manual trip method failed."); } catch (CircuitBreakerException e) { } impl.reset(); Object result = impl.invoke(mockCallable); verify(mockCallable); assertSame(obj, result); assertEquals(CircuitBreaker.BreakerState.CLOSED, impl.state); } @Test public void testTripHard() throws Exception { expect(mockCallable.call()).andReturn("hi"); replay(mockCallable); impl.tripHard(); try { impl.invoke(mockCallable); fail("exception expected after CircuitBreaker.tripHard()"); } catch (CircuitBreakerException e) { } assertEquals(CircuitBreaker.BreakerState.OPEN, impl.state); impl.reset(); impl.invoke(mockCallable); assertEquals(CircuitBreaker.BreakerState.CLOSED, impl.state); verify(mockCallable); } @Test public void testGetTripCount() throws Exception { long tripCount1 = impl.getTripCount(); impl.tripHard(); long tripCount2 = impl.getTripCount(); assertEquals(tripCount1 + 1, tripCount2); impl.tripHard(); assertEquals(tripCount2, impl.getTripCount()); } @Test public void testGetStatusWhenOpen() { impl.state = CircuitBreaker.BreakerState.OPEN; Assert.assertEquals(Status.DOWN, impl.getStatus()); } @Test public void testGetStatusWhenHalfClosed() { impl.state = CircuitBreaker.BreakerState.HALF_CLOSED; assertEquals(Status.DEGRADED, impl.getStatus()); } @Test public void testGetStatusWhenOpenBeforeReset() { impl.state = CircuitBreaker.BreakerState.CLOSED; impl.resetMillis.set(1000); impl.lastFailure.set(System.currentTimeMillis() - 50); assertEquals(Status.UP, impl.getStatus()); } @Test public void testGetStatusWhenOpenAfterReset() { impl.state = CircuitBreaker.BreakerState.OPEN; impl.resetMillis.set(1000); impl.lastFailure.set(System.currentTimeMillis() - 2000); assertEquals(Status.DEGRADED, impl.getStatus()); } @Test public void testGetStatusAfterHardTrip() { impl.tripHard(); impl.resetMillis.set(1000); impl.lastFailure.set(System.currentTimeMillis() - 2000); assertEquals(Status.DOWN, impl.getStatus()); } @Test public void testStatusIsByPassWhenSet() { impl.setByPassState(true); assertEquals(Status.DEGRADED, impl.getStatus()); } @Test public void testByPassIgnoresCurrentBreakerStateWhenSet() { impl.state = CircuitBreaker.BreakerState.OPEN; assertEquals(Status.DOWN, impl.getStatus()); impl.setByPassState(true); assertEquals(Status.DEGRADED, impl.getStatus()); impl.setByPassState(false); assertEquals(Status.DOWN, impl.getStatus()); } @Test public void testByPassIgnoresBreakerStateAndCallsWrappedMethod() throws Exception { expect(mockCallable.call()).andReturn("hi").anyTimes(); replay(mockCallable); impl.tripHard(); impl.setByPassState(true); try { impl.invoke(mockCallable); } catch (CircuitBreakerException e) { fail("exception not expected when CircuitBreaker is bypassed."); } assertEquals(CircuitBreaker.BreakerState.OPEN, impl.state); assertEquals(Status.DEGRADED, impl.getStatus()); impl.reset(); impl.setByPassState(false); impl.invoke(mockCallable); assertEquals(CircuitBreaker.BreakerState.CLOSED, impl.state); verify(mockCallable); } @Test public void testNotificationCallback() throws Exception { CircuitBreakerNotificationCallback cb = new CircuitBreakerNotificationCallback() { public void notify(Status s) { theStatus = s; } }; impl.addListener(cb); impl.trip(); assertNotNull(theStatus); assertEquals(Status.DOWN, theStatus); } @Test(expected = Throwable.class) public void circuitBreakerKeepsExceptionThatTrippedIt() throws Throwable { try { impl.invoke(new FailingCallable("broken")); } catch (Exception e) { } Throwable tripException = impl.getTripException(); assertEquals("broken", tripException.getMessage()); throw tripException; } @Test(expected = Throwable.class) public void resetCircuitBreakerStillHasTripException() throws Throwable { try { impl.invoke(new FailingCallable("broken")); } catch (Exception e) { } impl.reset(); Throwable tripException = impl.getTripException(); assertEquals("broken", tripException.getMessage()); throw tripException; } @Test public void circuitBreakerReturnsExceptionAsString() { try { impl.invoke(new FailingCallable("broken")); } catch (Exception e) { } Throwable tripException = impl.getTripException(); String s = impl.getTripExceptionAsString(); assertTrue(impl.getTripExceptionAsString().startsWith("java.lang.Exception: broken\n")); assertTrue(impl.getTripExceptionAsString().contains("at org.fishwife.jrugged.TestCircuitBreaker$FailingCallable.call")); assertTrue(impl.getTripExceptionAsString().contains("Caused by: java.lang.Exception: The Cause\n")); } @Test public void neverTrippedCircuitBreakerReturnsNullForTripException() throws Exception { impl.invoke(mockCallable); Throwable tripException = impl.getTripException(); assertNull(tripException); } private class FailingCallable implements Callable<Object> { private final String exceptionMessage; public FailingCallable(String exceptionMessage) { this.exceptionMessage = exceptionMessage; } Exception causeException = new Exception("The Cause"); public Object call() throws Exception { throw new Exception(exceptionMessage, causeException); } } }