Retry Sometimes in RxJava


If you’ve ever looked into retrying in RxJava. You’ll know there are two operators for retrying. First, repeat is for when a completion event. In addition, retry is for when an error event.

Maybe, you find yourself asking about retrying with a regular onNext event. In particular what if you only want to sometimes retry in RxJava?

This is the exact scenario I encountered. In particular, our request isn’t successful (http codes 4XX/5XX). Other times we make a call and it’s successful (think http code 200). Especially relevant, it’s accepted but not processed (http code 202). In my case the 202 would happen if another service in the cloud was down, but not the one I was talking to.

Business rules dictated that some successes cases (200) should be treated as terminal. While some successes (202) shouldn’t be. Since completions appear to be identical repeatWhen didn’t appear to be a viable option.

Yet, I should retry not just for error cases like 4xx and 5xx but also for 202. Also, in the case of a 202 we should still emit the response. Seems like we should notify the user that we are still processing the request.

As a result, in this post I’ll detail a recursive means for retrying. While I’ll explain this, I still have some other ideas. In particular I’ll explore in subsequent posts.

First of all, here is what the observable initially looked like:

fun makeCall(): Observable{
    return api.getResponse()
}

Much as I wanted, both retry and repeat didn’t look like options. While retry would definitely solve the problem for errors. But, what about the case of a 202? As a result repeat doesn’t appear to help here as it’s based strictly off of onComplete.

Inspecting Events With Materialize

The problem is that I need to inspect both onError and onNext. Hence, this logic would ideally be in the same place. If you are on my mailing list you might remember that I wrote to you about the materialize and dematerialize operators. As a result, they allow us to accomplish just that.

fun makeCall(): Observable{
    return api.getResponse()
              .materialize()
              .flatMap{ notification ->
                  // Temporary place holder logic for right now
                  Observable.just(notification)
              }
              .dematerialize()
}

In case you don’t remember the materialize operator wraps the items in the stream. In particular, they are placed in a wrapper class. While this maybe confusing, here is the marble diagram for clarity:

Materialize Operator Marble Diagram that we can use for our Retry logic

The notification class has methods determining if it contains an error, value, or completion. Thus we can expand our flatMap handling logic:

fun makeCall(): Observable{
    return api.getResponse()
              .materialize()
              .flatMap { notification ->
                  when {
                      (notification.isOnNext && !notification.value.isProcessed) ||
                      notification.isOnError -> {
                            Observable.timer(duration, TimeUnit.MILLISECONDS).flatMap {
                                          makeCall()
                                     }.materialize()
                      }
                      else -> Observable.just(notification)
                  }
                }
               .dematerialize()
}

Retry Delay

While you may not have caught it, we used timer to add a delay. Especially Relevant, as this will prevent hammering the server with one request after another. Instead a wait time will be used between attempts.

Nice we have retry logic. Now in my case I needed to retry only a few times before giving up. I could accomplish this by adding a tracking variable.

fun makeCall(tracking: Int): Observable{
    return api.getResponse()
              .materialize()
              .flatMap { notification ->
                  when {
                      (notification.isOnNext && !notification.value.isProcessed) ||
                      notification.isOnError -> {
                            Observable.timer(duration, TimeUnit.MILLISECONDS).flatMap {
                                          makeCall(tracking+1)
                                     }.materialize()
                      }
                      else -> Observable.just(notification)
                  }
                }
               .dematerialize()
}

Next, we also need to add logic for preventing this from running over and over. We need to check the value of the tracking variable before we call makeCall again.

return api.getResponse()
          .materialize()
          .flatMap { notification ->
                    when {
                        tracking == maxAttempts -> Observable.just(notification)
                        (notification.isOnNext && !notification.value.isProcessed) ||
                                notification.isOnError -> {
                            Observable.timer(duration, TimeUnit.MILLISECONDS).flatMap {
                                makeCall(1 + tracking)
                            }.materialize()
                        }
                        else -> Observable.just(notification)

                    }
           }.dematerialize()
}

So far we’ve accomplished retrying for errors, and the 202 case. But one minor detail, we need to let the 202 pass through as well as retrying. However, we can only emit one error. That means we have to emit the 202 first, and then tack on an error. Our the when block needs to be updated to split onNext and onError cases.

when {
    tracking == maxAttempts -> Observable.just(notification)
    (notification.isOnNext && !notification.value.isProcessed) -> {
        Observable.concat(Observable.just(notification), Observable.timer(duration, TimeUnit.MILLISECONDS).flatMap {
            makeCall(1 + tracking)
        }).materialize()
    }
    notification.isOnError -> {
        Observable.timer(duration, TimeUnit.MILLISECONDS).flatMap {
            makeCall(1 + tracking)
        }.materialize()
    }
    else -> Observable.just(notification)
}            

My use case was unique in that the business rules didn’t specify returning information about failures (4xx/5xx). Especially relevant we could have easily attached that information to a variation of the apiResponse and passed it through. This would allow us to get around the only one error being emitted rule as it would come through onNext instead of onError.

Update

While, the above solution works I’ve found a better way of retrying check it out here

Comments 1

  1. Pingback: Retry Sometimes Using RetryWhen - Learn Android The Easy Way

Leave a Reply

Your email address will not be published. Required fields are marked *