Max Myslyvtsev Max Myslyvtsev - 4 months ago 57
Java Question

Hystrix circuit breaker with business exceptions

I have observed that Hystrix treats all exceptions coming out of commands as failures for circuit breaking purposes. It includes exceptions which are thrown from command run () method and created by Hystrix itself, e.g. HystrixTimeoutException.

But I have business exceptions to be thrown from run() method that signify that service responded with valid error which has to be processed further.
One example of such exception is WebServiceFaultException while using WebServiceTemplate from SpringWS.

So I do not need those specific exceptions to trip the circuit.
How this behavior can be achieved?

There is an obvious way of wrapping business exceptions into a holder object, returning it from run() method, then unwrapping it back to the Exception and rethrow. But it was wondering if there is a cleaner way.

Answer

There are following solutions available.

Return exception instead of throwing

Most straightforward and dirty approach. This looks a little funky, because you have to erase the command to Object and there is a lot of type casting.

Observable<BusinessResponse> observable = new HystrixCommand<Object>() {
    @Override
    protected Object run() throws Exception {
        try {
            return doStuff(...);
        } catch (BusinessException e) {
            return e; // so Hystrix won't treat it as a failure
        }
    }
})
.observe()
.flatMap(new Func1<Object, Observable<BusinessResponse>>() {
    @Override
    public Observable<BusinessResponse> call(Object o) {
        if (o instanceof BusinessException) {
            return Observable.error((BusinessException)o);
        } else {
            return Observable.just((BusinessResponse)o);
        }
    }
});

Use holder object to hold both result and exception

This apporach requires introduction of additional holder class (which can also be used on it's own for other purposes).

class ResultHolder<T, E extends Exception> {
    private T result;
    private E exception;

    public ResultHolder(T result) {
        this.result = result;
    }
    public ResultHolder(E exception) {
        if (exception == null) {
            throw new IllegalArgumentException("exception can not be null");
        }
        this.exception = exception;
    }

    public T get() throws E {
        if (exception != null) {
            throw exception;
        } else {
            return result;
        }
    }

    public Observable<T> observe() {
        if (exception != null) {
            return Observable.error(exception);
        } else {
            return Observable.just(result);
        }
    }

    @SuppressWarnings("unchecked")
    public static <T, E extends Exception> ResultHolder<T, E> wrap(BusinessMethod<T, E> method) {
        try {
            return new ResultHolder<>(method.call());
        } catch (Exception e) {
            return new ResultHolder<>((E)e);
        }
    }


    public static <T, E extends Exception> Observable<T> unwrap(ResultHolder<T, E> holder) {
        return holder.observe();
    }

    interface BusinessMethod<T, E extends Exception> {
        T call() throws E;
    }
}

Now code that uses it looks much cleaner, the only downside might be a fair amount of generics. Also this approach is at it's best in Java 8 where lambdas and method references are available, otherwise it will look clunky.

new HystrixCommand<ResultHolder<BusinessResponse, BusinessException>>() {
    @Override
    protected ResultHolder<BusinessResponse, BusinessException> run() throws Exception {
        return ResultHolder.wrap(() -> doStuff(...));
    }
}
.observe()
.flatMap(ResultHolder::unwrap);

Use HystrixBadRequestException

HystrixBadRequestException is a special kind of exception which will not count as a failure in terms of circuit breaker and metrics. As seen in documentation:

Unlike all other exceptions thrown by a HystrixCommand this will not trigger fallback, not count against failure metrics and thus not trigger the circuit breaker.

Instances of HystrixBadRequestException are not created by Hystrix itself, so it is safe to use it as a wrapper for business exceptions. However, original business exception still requires to be unwrapped.

new HystrixCommand<BusinessResponse>() {
    @Override
    protected BusinessResponse run() throws Exception {
        try {
            return doStuff(...);
        } catch (BusinessException e) {
            throw new HystrixBadRequestException("Business exception occurred", e);
        }
    }
}
.observe()
.onErrorResumeNext(e -> {
    if (e instanceof HystrixBadRequestException) {
        e = e.getCause(); // Unwrap original BusinessException
    }
    return Observable.error(e);
})
Comments