Angular 2 Http RetryWhen

2019-05-10 22:13发布

问题:

I'm trying to use retryWhen in HTTP calls.

It works perfectly when try to use like this:

return this.http.get(`${environment.apiUrl}/track/${this.user.instance._id}/${this.currentPlayer.playlist.id}/next?s=${this.playerCounter}`, options)
      .timeout(500, new TimeoutError(`Timeout trying to get next track. [instanceId=${this.user.instance._id}]`))
      .retryWhen(attempts => {
        return Observable.range(1, 3).zip(attempts, i => i).flatMap(i => 3 === i ? Observable.throw(attempts) : Observable.timer(i * 1000));
      })

It makes a maximum of 3 tries if get a Timeout error.

But, always have a buuut, I want to make this more abstract to use on various use cases and for this, I have to check the type of the error.

Only TechnicalErros will be retried.

So I tried this without success.

.retryWhen(attempts => {
    return attempts.flatMap(error => {
      if(error instanceof TechnicalError) {
        return Observable.range(1, 3).zip(attempts, i => i).flatMap(i => 3 === i ? Observable.throw(attempts) : Observable.timer(i * 1000));
      } else {
        Observable.throw(error);
      }
    });
  })

It stops at first try and does not execute the Observable.timer(), neither the Observable.throw().

I have almost sure that the problem is about the first flatMap, I already tried to use mergeMap, without success.

Thanks in advance!

回答1:

In RxJS 5 flatMap() is just alias to mergeMap() :).

The problem is in the way you use the callback for retryWhen() operator. It's called just once and then every time an error signal arrives it's pushed to the Observable returned from this callback.

In your second example you're returning Observable from attempts.flatMap and then subscribing to it again that callback with .zip(attempts, i => i). But this zip operator is never called because it's called after the value has been already consumed by attempts.flatMap. Also this is why the Observable.range(1, 3) starts always from the beginning.

I know this looks confusing. Just be aware thet:

  • the callback for retryWhen() is called just once.
  • the callback for attempts.flatMap() is called every time an error arrives.

So you just need to restructure your code, for example like the following:

var source = Observable.create(obs => {
        obs.next(1);
        obs.next(2);
        obs.error(new TechnicalError('error from source'));
    })
    .retryWhen(attempts => {
        console.log('retryWhen callback');
        let count = 0;

        return attempts.flatMap(error => {
            if (error instanceof TechnicalError) {
                console.log(error);
                return ++count >= 3 ? Observable.throw(error) : Observable.timer(count * 1000);
            } else {
                return Observable.throw(error);
            }
        });
    })
    .subscribe(
        val => console.log(val),
        err => console.log('subscribe error', err),
        _ => console.log('complete')
    );

This prints to console:

1
2
retryWhen callback
TechnicalError { msg: 'error from source' }
1
2
TechnicalError { msg: 'error from source' }
1
2
TechnicalError { msg: 'error from source' }
subscribe error TechnicalError { msg: 'error from source' }

See live demo: https://jsbin.com/hobeda/3/edit?js,console



回答2:

I came across same issue and found a way to use range operator to generate the retries count instead of the counter variable, here is the code:

  var source = Observable.create(obs => {
  obs.next(1);
  obs.next(2);
  obs.error(new TechnicalError('error from source'));
})
  .retryWhen(error => {
    console.log("Error occured, retryWhen initialized");
    return error.zip(Observable.range(1, 4), (error, i) => {
      return {
        ErrorObj: error,
        RetryCount: i
      }
    })
      .map(obj => {
        if (error instanceof TechnicalError) {
          if (obj.RetryCount > 3)
            throw obj.ErrorObj;
          //Retry every one sec for 3 times
          console.log('Retry # ' + obj.RetryCount);
          return Observable.timer(obj.RetryCount * 1000);
        }
        else {
          throw obj.ErrorObj;
        }
      }).concatAll()
  })
  .subscribe(
  val => console.log(val),
  err => console.log('subscribe error', err),
  _ => console.log('complete')
  );

The idea is to place the range operator where it will be initialized only once (the retryWhen callback) this way the zip operator will combine any error with a new range number and pass this to the map operator to do the error check logic