/* * Copyright 2014 Avanza Bank AB * * 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.avanza.astrix.ft.hystrix; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import com.avanza.astrix.beans.core.AstrixBeanKey; import com.avanza.astrix.beans.core.AstrixBeanSettings; import com.avanza.astrix.beans.ft.BeanFaultToleranceFactorySpi; import com.avanza.astrix.beans.service.DirectComponent; import com.avanza.astrix.context.AstrixApplicationContext; import com.avanza.astrix.context.AstrixContext; import com.avanza.astrix.context.TestAstrixConfigurer; import com.avanza.astrix.core.ServiceUnavailableException; import com.avanza.astrix.provider.core.AstrixApiProvider; import com.avanza.astrix.provider.core.AstrixConfigDiscovery; import com.avanza.astrix.provider.core.Service; import com.avanza.astrix.test.util.AssertBlockPoller; import com.netflix.hystrix.Hystrix; import com.netflix.hystrix.HystrixCommandKey; import com.netflix.hystrix.HystrixCommandMetrics; import com.netflix.hystrix.HystrixEventType; import com.netflix.hystrix.util.HystrixRollingNumberEvent; @Ignore public class HystrixCommandFacadeTest { // TODO: Rename to sync-service invocation test private AstrixContext context; private Ping ping; private PingImpl pingServer = new PingImpl(); @Before public void before() throws InterruptedException { Hystrix.reset(); context = new TestAstrixConfigurer().enableFaultTolerance(true) .registerApiProvider(PingApi.class) .set(AstrixBeanSettings.TIMEOUT, AstrixBeanKey.create(Ping.class), 250) .set(AstrixBeanSettings.CORE_SIZE, AstrixBeanKey.create(Ping.class), 1) .set(AstrixThreadPoolProperties.MAX_QUEUE_SIZE, AstrixBeanKey.create(Ping.class), -1) // NO QUEUE .set("pingUri", DirectComponent.registerAndGetUri(Ping.class, pingServer)) .configure(); ping = context.getBean(Ping.class); initMetrics(ping); } @After public void after() { context.destroy(); } @Test public void underlyingObservableIsWrappedWithFaultTolerance() throws Throwable { String result = ping.ping("foo"); assertEquals("foo", result); eventually(() -> { assertEquals(1, getEventCountForCommand(HystrixRollingNumberEvent.SUCCESS)); }); } @Test public void serviceUnavailableThrownByUnderlyingObservableShouldCountAsFailure() throws Exception { initMetrics(ping); pingServer.setFault(() -> { throw new ServiceUnavailableException(""); }); try { ping.ping("foo"); fail("Expected service unavailable"); } catch (ServiceUnavailableException e) { // Expcected } catch (Throwable e) { fail("Expected exception of type ServiceUnavailableException, got: " + e.getClass().getName()); } eventually(() -> { assertEquals(1, getEventCountForCommand(HystrixRollingNumberEvent.SUCCESS)); // 1 from init assertEquals(1, getEventCountForCommand(HystrixRollingNumberEvent.FAILURE)); }); } @Test public void normalExceptionsThrownIsTreatedAsPartOfNormalApiFlowAndDoesNotCountAsFailure() throws Throwable { initMetrics(ping); pingServer.setFault(() -> { throw new MyDomainException(); }); try { ping.ping("foo"); fail("Expected service unavailable"); } catch (MyDomainException e) { // Expcected } eventually(() -> { // Note that from the perspective of a circuit-breaker an exception thrown // by the underlying service call (typically a service call) should not // count as failure and therefore not (possibly) trip circuit breaker. assertEquals(2, getEventCountForCommand(HystrixRollingNumberEvent.SUCCESS)); //+1 from init assertEquals(0, getEventCountForCommand(HystrixRollingNumberEvent.FAILURE)); }); } @Test public void throwsServiceUnavailableOnTimeouts() throws Throwable { initMetrics(ping); pingServer.setFault(() -> { sleep(10_000); //simulate timeout by sleeping }); try { ping.ping("foo"); fail("A ServiceUnavailableException should be thrown on timeout"); } catch (ServiceUnavailableException e) { // Expcected } eventually(() -> { assertEquals(1, getEventCountForCommand(HystrixRollingNumberEvent.SUCCESS)); // 1 from init assertEquals(1, getEventCountForCommand(HystrixRollingNumberEvent.TIMEOUT)); assertEquals(0, getEventCountForCommand(HystrixRollingNumberEvent.SEMAPHORE_REJECTED)); }); } @Test public void threadPoolRejectedCountsAsFailure() throws Exception { initMetrics(ping); pingServer.setFault(() -> { sleep(10_000); //simulate timeout by sleeping }); // One of these invocations should be rejected CountDownLatch done = new CountDownLatch(1); new Thread(() -> { try { ping.ping("foo"); } catch (Exception e) { } done.countDown(); }).start(); new Thread(() -> { try { ping.ping("foo"); } catch (Exception e) { } done.countDown(); }).start(); done.await(5, TimeUnit.SECONDS); eventually(() -> { assertEquals(1, getEventCountForCommand(HystrixRollingNumberEvent.SUCCESS)); // 1 from init assertEquals(1, getEventCountForCommand(HystrixRollingNumberEvent.THREAD_POOL_REJECTED)); }); } @Test public void doesNotInvokeServiceWhenBulkHeadIsFull() throws Exception { AtomicInteger invocationCount = new AtomicInteger(); CountDownLatch done = new CountDownLatch(1); CountDownLatch serverInvocationCompleted = new CountDownLatch(1); pingServer.setFault(() -> { invocationCount.incrementAndGet(); try { done.await(10, TimeUnit.SECONDS); //simulate timeout by waiting on latch } catch (Exception e) { } serverInvocationCompleted.countDown(); }); Thread t1 = new Thread(() -> { try { ping.ping("foo"); } catch (ServiceUnavailableException e) { } done.countDown(); }); t1.start(); Thread t2 = new Thread(() -> { try { ping.ping("foo"); } catch (ServiceUnavailableException e) { } done.countDown(); }); t2.start(); if (!done.await(3, TimeUnit.SECONDS)) { fail("Expected one ping invocation to be aborted by fault tolerance layer"); } if (!serverInvocationCompleted.await(3, TimeUnit.SECONDS)) { fail("Expected server invocation to complete when second service call is aborted"); } assertEquals(1, invocationCount.get()); } private void eventually(Runnable assertion) throws InterruptedException { new AssertBlockPoller(3000, 25).check(assertion); } private void initMetrics(Ping ping) throws InterruptedException { // Black hystrix magic here :( try { ping.ping("foo"); } catch (Exception e) { } HystrixFaultToleranceFactory faultTolerance = (HystrixFaultToleranceFactory) AstrixApplicationContext.class.cast(this.context).getInstance(BeanFaultToleranceFactorySpi.class); HystrixCommandKey key = faultTolerance.getCommandKey(AstrixBeanKey.create(Ping.class)); HystrixCommandMetrics.getInstance(key).getCumulativeCount(HystrixEventType.SUCCESS); } private static void sleep(long time) { try { Thread.sleep(time); } catch (InterruptedException e) { } } private int getEventCountForCommand(HystrixRollingNumberEvent hystrixRollingNumberEvent) { HystrixFaultToleranceFactory faultTolerance = (HystrixFaultToleranceFactory) AstrixApplicationContext.class.cast(this.context).getInstance(BeanFaultToleranceFactorySpi.class); HystrixCommandKey commandKey = faultTolerance.getCommandKey(AstrixBeanKey.create(Ping.class)); HystrixCommandMetrics metrics = HystrixCommandMetrics.getInstance(commandKey); int currentConcurrentExecutionCount = (int) metrics.getCumulativeCount(hystrixRollingNumberEvent); return currentConcurrentExecutionCount; } public static class MyDomainException extends RuntimeException { private static final long serialVersionUID = 1L; } public static interface Ping { String ping(String msg); } public static class PingImpl implements Ping { private Runnable simulatedFault = () -> {}; public String ping(String msg) { simulatedFault.run(); return msg; } public void setFault(Runnable fault) { this.simulatedFault = fault; } } @AstrixApiProvider public static class PingApi { @AstrixConfigDiscovery("pingUri") @Service public Ping ping() { return new PingImpl(); } } }