/*
* 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.assertTrue;
import static org.junit.Assert.fail;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import org.junit.After;
import org.junit.Before;
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.avanza.astrix.test.util.AstrixTestUtil;
import com.avanza.hystrix.multiconfig.MultiConfigId;
import com.netflix.hystrix.Hystrix;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandMetrics;
import com.netflix.hystrix.HystrixEventType;
import com.netflix.hystrix.HystrixObservableCommand.Setter;
import com.netflix.hystrix.util.HystrixRollingNumberEvent;
import rx.Observable;
import rx.Observable.OnSubscribe;
import rx.Subscriber;
public class HystrixObservableCommandFacadeTest {
private final String groupKey = UUID.randomUUID().toString();
private final String commandKey = UUID.randomUUID().toString();
private final MultiConfigId multiConfigId = MultiConfigId.create("astrix");
private final Setter commandSettings = Setter.withGroupKey(multiConfigId.createCommandGroupKey(groupKey))
.andCommandKey(multiConfigId.createCommandKey(commandKey))
.andCommandPropertiesDefaults(com.netflix.hystrix.HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds(25)
.withExecutionIsolationSemaphoreMaxConcurrentRequests(1));
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), 25)
.set(AstrixBeanSettings.MAX_CONCURRENT_REQUESTS, AstrixBeanKey.create(Ping.class), 1)
.set("pingUri", DirectComponent.registerAndGetUri(Ping.class, pingServer))
.configure();
ping = context.getBean(Ping.class);
initMetrics(ping);
}
private void initMetrics(Ping ping) throws InterruptedException {
// Black hystrix magic here :(
try {
ping.ping();
} 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);
}
@After
public void after() {
context.destroy();
}
@Test
public void underlyingObservableIsWrappedWithFaultTolerance() throws Throwable {
pingServer.setResult(Observable.just("foo"));
String result = ping.ping().toBlocking().first();
assertEquals("foo", result);
eventually(() -> {
assertEquals(1, getEventCountForCommand(HystrixRollingNumberEvent.SUCCESS));
});
}
@Test
public void serviceUnavailableThrownByUnderlyingObservableShouldCountAsFailure() throws Exception {
pingServer.setResult(Observable.<String>error(new ServiceUnavailableException("")));
try {
ping.ping().toBlocking().first();
fail("Expected service unavailable");
} catch (ServiceUnavailableException e) {
// Expcected
}
eventually(() -> {
assertEquals(0, getEventCountForCommand(HystrixRollingNumberEvent.SUCCESS));
assertEquals(1, getEventCountForCommand(HystrixRollingNumberEvent.FAILURE));
});
}
@Test
public void normalExceptionsThrownIsTreatedAsPartOfNormalApiFlowAndDoesNotCountAsFailure() throws Exception {
pingServer.setResult(Observable.<String>error(new MyDomainException()));
try {
ping.ping().toBlocking().first();
fail("All regular exception should be propagated as is from underlying observable");
} catch (MyDomainException e) {
// Expcected
}
eventually(() -> {
// Note that from the perspective of a circuit-breaker an exception thrown
// by the underlying observable (typically a service call) should not
// count as failure and therefore not (possibly) trip circuit breaker.
assertEquals(1, getEventCountForCommand(HystrixRollingNumberEvent.SUCCESS));
assertEquals(0, getEventCountForCommand(HystrixRollingNumberEvent.FAILURE));
});
}
@Test
public void throwsServiceUnavailableOnTimeouts() throws Exception {
pingServer.setResult(Observable.create(new OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> t1) {
// Simulate timeout by not invoking subscriber
}
}));
try {
ping.ping().toBlocking().first();
fail("All ServiceUnavailableException should be thrown on timeout");
} catch (ServiceUnavailableException e) {
// Expcected
}
eventually(() -> {
assertEquals(0, getEventCountForCommand(HystrixRollingNumberEvent.SUCCESS));
assertEquals(1, getEventCountForCommand(HystrixRollingNumberEvent.TIMEOUT));
assertEquals(0, getEventCountForCommand(HystrixRollingNumberEvent.SEMAPHORE_REJECTED));
});
}
@Test
public void semaphoreRejectedCountsAsFailure() throws Exception {
pingServer.setResult(Observable.create(new OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> t1) {
// Simulate timeout by not invoking subscriber
}
}));
Observable<String> ftObservable1 = ping.ping();
Observable<String> ftObservable2 = ping.ping();
// Subscribe to observables, ignore emitted items/errors
ftObservable1.subscribe((item) -> {}, (exception) -> {});
ftObservable2.subscribe((item) -> {}, (exception) -> {});
eventually(() -> {
assertEquals(0, getEventCountForCommand(HystrixRollingNumberEvent.SUCCESS));
assertEquals(1, getEventCountForCommand(HystrixRollingNumberEvent.SEMAPHORE_REJECTED));
});
}
@Test
public void subscribesEagerlyToCreatedObserver() throws Exception {
AtomicBoolean subscribed = new AtomicBoolean(false);
Supplier<Observable<String>> timeoutCommandSupplier = new Supplier<Observable<String>>() {
@Override
public Observable<String> get() {
return Observable.create(t1 -> {
subscribed.set(true);
});
}
};
HystrixObservableCommandFacade.observe(timeoutCommandSupplier, commandSettings);
assertTrue(subscribed.get());
}
@Test
public void doesNotInvokeSupplierWhenBulkHeadIsFull() throws Exception {
final AtomicInteger supplierInvocationCount = new AtomicInteger();
Supplier<Observable<String>> timeoutCommandSupplier = new Supplier<Observable<String>>() {
@Override
public Observable<String> get() {
supplierInvocationCount.incrementAndGet();
return Observable.create(new OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> t1) {
// Simulate timeout by not invoking subscriber
}
});
}
};
Observable<String> ftObservable1 = HystrixObservableCommandFacade.observe(timeoutCommandSupplier, commandSettings);
final Observable<String> ftObservable2 = HystrixObservableCommandFacade.observe(timeoutCommandSupplier, commandSettings);
ftObservable1.subscribe(); // Ignore
assertEquals(1, supplierInvocationCount.get());
AstrixTestUtil.serviceInvocationException(() -> ftObservable2.toBlocking().first(), AstrixTestUtil.isExceptionOfType(ServiceUnavailableException.class));
assertEquals(1, supplierInvocationCount.get());
}
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 {
Observable<String> ping();
}
public static class PingImpl implements Ping {
private Observable<String> result;
public Observable<String> ping() {
return result;
}
public void setResult(Observable<String> result) {
this.result = result;
}
}
@AstrixApiProvider
public static class PingApi {
@AstrixConfigDiscovery("pingUri")
@Service
public Ping ping() {
return new PingImpl();
}
}
private void eventually(Runnable assertion) throws InterruptedException {
new AssertBlockPoller(3000, 25).check(assertion);
}
}