/*
* 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 java.util.function.Supplier;
import com.avanza.astrix.core.ServiceUnavailableException;
import com.netflix.hystrix.HystrixObservableCommand;
import com.netflix.hystrix.HystrixObservableCommand.Setter;
import com.netflix.hystrix.exception.HystrixRuntimeException;
import rx.Observable;
import rx.functions.Func1;
/**
* @author Elias Lindholm (elilin)
*/
class HystrixObservableCommandFacade<T> {
public static <T> Observable<T> observe(final Supplier<Observable<T>> observableFactory, Setter settings) {
Observable<Result<T>> faultToleranceProtectedObservable = new HystrixObservableCommand<Result<T>>(settings) {
@Override
protected Observable<Result<T>> construct() {
return observableFactory.get().map(new Func1<T, Result<T>>() {
@Override
public Result<T> call(T t1) {
return Result.success(t1);
}
}).onErrorResumeNext(new Func1<Throwable, Observable<? extends Result<T>>>() {
@Override
public Observable<? extends Result<T>> call(Throwable t1) {
if (t1 instanceof ServiceUnavailableException) {
return Observable.error(t1);
}
// Wrap all exception thrown by underlying observable
// in Result.exception. This will by-pass hystrix
// circuit-breaker logic and not count as a failure.
// I.e we don't want the circuit-breaker to open
// due to exceptions thrown by the underlying service.
return Observable.just(Result.<T>exception(t1));
}
});
}
@Override
protected Observable<Result<T>> resumeWithFallback() {
/*
* This method will will be invoked in any of these circumstances:
* - Underlying observable did not emit event before timeout, "service timeout"
* - Circuit Breaker rejected subscription to underlying observable, "circuit open"
* - Bulk Head rejected subscription to underlying observable, "too many outstanding requests"
* - Underlying observable threw ServiceUnavailableException
*
* Either way, just return a ServiceUnavailableException.
*
*/
return Observable.just(Result.<T>exception(createServiceUnavailableException()));
}
private ServiceUnavailableException createServiceUnavailableException() {
if (isResponseRejected()) {
return new ServiceUnavailableException(String.format("cause=%s service=%s",
"REJECTED_EXECUTION", getCommandKey().name()));
}
if (isResponseTimedOut()) {
return new ServiceUnavailableException(String.format("cause=%s service=%s executionTime=%s",
"TIMEOUT", getCommandKey().name(), getExecutionTimeInMilliseconds()));
}
if (isResponseShortCircuited()) {
return new ServiceUnavailableException(String.format("cause=%s service=%s",
"SHORT_CIRCUITED", getCommandKey().name()));
}
if (isFailedExecution() && (getFailedExecutionException() instanceof ServiceUnavailableException)) {
ServiceUnavailableException result = (ServiceUnavailableException) getFailedExecutionException();
appendStackTrace(result, new ServiceUnavailableException(String.format("service=%s", getCommandKey().name())));
return result;
}
return new ServiceUnavailableException(String.format("cause=%s service=%s",
"UNKNOWN", getCommandKey().name()));
}
}.observe(); // Eagerly start execution of underlying observable to fulfill contract of BeanProxy.proxyAsyncInvocation
return faultToleranceProtectedObservable
.flatMap(resultWrapper -> resultWrapper.toObservable())
.onErrorResumeNext(error -> {
if (error instanceof HystrixRuntimeException) {
HystrixRuntimeException e = (HystrixRuntimeException) error;
if (e.getCause() != null) {
// Can this happen?
return Observable.error(e.getCause());
}
return Observable.error(new ServiceUnavailableException(e.getFailureType().toString()));
}
return Observable.error(error);
});
}
private static void appendStackTrace(Throwable target, Throwable trace) {
Throwable lastThowableInChain = target;
while (lastThowableInChain.getCause() != null) {
lastThowableInChain = lastThowableInChain.getCause();
}
lastThowableInChain.initCause(trace);
}
private static class Result<T> {
private final T value;
private final Throwable exception;
public Result(T value, Throwable exception) {
this.value = value;
this.exception = exception;
}
public static <T> Result<T> success(T result) {
return new Result<T>(result, null);
}
/**
* @param throwable
* @return
*/
public static <T> Result<T> exception(Throwable throwable) {
return new Result<T>(null, throwable);
}
public Observable<T> toObservable() {
if (this.exception != null) {
return Observable.error(this.exception);
}
return Observable.just(this.value);
}
}
}