rxjava2-extras icon indicating copy to clipboard operation
rxjava2-extras copied to clipboard

Feature request: allow RetryWhen to count only consecutive errors

Open RobLewis opened this issue 6 years ago • 2 comments

The semantics would be "Retry when the count of consecutive errors has not exceeded a certain value." The error count would be reset upon a successful onNext call.

It could be done with a builder function like .consecutiveErrorCountLessThan( int n ). (If 2+ billion consecutive errors isn't enough, make the parameter a long :-)

I've implemented something like this as a composable ObservableTransformer. It also accepts an optional list of the types of Exception to be counted. Caveat: it's not well tested yet.

public class RetryLimiter<T> implements ObservableTransformer<T,T> {
    
    private final static String TAG = RetryLimiter.class.getSimpleName();
    
    private int errorCount = 0;
    private int maxErrorCount;
    
    private List<Class> countedExceptions;  // note errors not in this list (if defined) are not counted and will continue retries
    
    // constructor passes max tolerated consecutive errors (count is rezeroed upon a successful emission)
    public RetryLimiter( int maxErrors ) {
        if( maxErrors < 0 ) throw new IllegalArgumentException( "maxErrors cannot be < 0" );
        maxErrorCount = maxErrors;
    }
    
    // constructor that accepts a list of the types of exceptions to count (others are ignored)
    public RetryLimiter( int maxErrors, List<Class> exceptions ) {
        this( maxErrors );
        if( exceptions != null ) {
            for( Class c : exceptions ) {  // check that the supplied list contains only Throwable (sub)classes
                if( !Throwable.class.isAssignableFrom( c ) ) {  // is c a Throwable or subclass of Throwable?
                    throw new IllegalArgumentException( "List can only contain class Throwable or its subclasses" );
                }
            }
            countedExceptions = exceptions;
        } else {
            throw new NullPointerException( "If supplied, Exception Class list cannot be null" );
        }
    }
    
    @Override
    public ObservableSource<T> apply( Observable<T> upstream ) {
        return upstream
                .doOnError( err -> {
                    if( countedExceptions != null ) {
                        for( Class t : countedExceptions ) {
                            if( err.getClass().equals( t ) ) {
                                errorCount++;
                                break;        // don't count more than once
                            }
                        }
                    } else {  // null list of counted Exceptions
                        errorCount++;
                    }
                    if( DEBUG ) Log.d( TAG, "Consecutive error #" + errorCount + ": " + err.toString() );
                } )  
                .doOnNext( next -> errorCount = 0 )
                .retryUntil( () -> errorCount > maxErrorCount );
    }
}

Also, what about including an option for a general Predicate function to decide whether or not to retry? I realize this is essentially the same as the standard .retryUntil( Predicate ) but it could be a useful addition: .isTrue( Predicate p ). Not to gild the lily, but it could perhaps be two functions: .and( Predicate p ) and .or( Predicate p ). Arguments to the Predicate are TBD but would presumably include the Throwable.

RobLewis avatar Feb 14 '19 16:02 RobLewis

Ok, can you give me a use case (just wondering if there's another way to do it)?

I use retryWhen for IO calls especially and usually do so with a capped exponential backoff:

.retryWhen(
  RetryWhen.delays(
    Flowable.just(1, 2, 4, 8, 16, 30).compose(Transformers.repeatLast()),
  TimeUnit.SECONDS)

Another trick is to add .timeout(30, TimeUnit.SECONDS) to the source stream to detect no emissions.

davidmoten avatar Feb 15 '19 04:02 davidmoten

In your email you mentioned that your use case involves network IO. I suggest that a time-based retry is more appropriate. If you want to fail after 7 attempts with exponential backoff:

.retryWhen(
  RetryWhen.delays(
    Flowable.just(1, 2, 4, 8, 16, 30),
  TimeUnit.SECONDS)

Is there any reason why time-based retries are not the go?

Note also that I would expect the retryWhen operator to be attached to each of the IO calls (the Singles you mentioned) rather than further downstream after the flatmap.

davidmoten avatar Feb 17 '19 01:02 davidmoten