Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular/RXJS - Keeping track of HTTP requests stuck in retry mode

I have a function which is handling multiple APIs request for me and puts each request in Retry mode if failed. Now, in case one request is already in Retry loop and another instance of the same API call arrives, my function is not able to trace this and add the redundant API call in retry loop again.

Assuming i am placing a call to
/api/info/authors

What is happening

1stREQ| [re0]------>[re1]------>[re2]------>[re3]------>[re4]------>[re5]
2ndREQ|                         [re0]------>[re1]------>[re2]------>[re3]------>[re4]------>[re5]


What should happen,

1stREQ| [re0]------>[re1]------>[re2]------>[re3]------>[re4]------>[re5]
2ndREQ|                         [re0]/ (MERGE)

Following is my Service aling with my Retry function,

import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { retryWhen, mergeMap, finalize, share, shareReplay } from 'rxjs/operators';
import { Observable, throwError, of, timer } from 'rxjs';


@Injectable({
  providedIn: 'root'
})
export class DataService {
  constructor(private http: HttpClient) { }

  private apiUrl: string = 'http://localhost/api-slim-php/public/api';
  public dataServerURL: string = 'http://localhost/';

  /* 
  This function fetches all the info from API /info/{category}/{id}
  category  : author    & id  : '' or 1,2,3... or a,b,c...
  category  : form      & id  : '' or 1,2,3...
  category  : location  & id  : '' or 1,2,3...
  category  : school    & id  : '' or 1,2,3...
  category  : timeframe & id  : '' or 1,2,3...
  category  : type      & id  : '' or 1,2,3...
  */
  public getInfoAPI(category: string, id: string = "", page: string = "1", limit: string = "10") {
    var callURL: string = '';

    if (!!id.trim() && !isNaN(+id)) callURL = this.apiUrl + '/info/' + category + '/' + id;
    else callURL = this.apiUrl + '/info/' + category;

    return this.http.get(callURL, {
      params: new HttpParams()
        .set('page', page)
        .set('limit', limit)
    }).pipe(
      retryWhen(genericRetryStrategy({ maxRetryAttempts: 5, scalingDuration: 1000 })),
      shareReplay()
    );
  }
}
export const genericRetryStrategy = ({
  maxRetryAttempts = 3,
  scalingDuration = 1000,
  excludedStatusCodes = []
}: {
  maxRetryAttempts?: number,
  scalingDuration?: number,
  excludedStatusCodes?: number[]
} = {}) => (attempts: Observable<any>) => {
  return attempts.pipe(
    mergeMap((error, i) => {
      const retryAttempt = i + 1;
      // if maximum number of retries have been met
      // or response is a status code we don't wish to retry, throw error
      if (
        retryAttempt > maxRetryAttempts ||
        excludedStatusCodes.find(e => e === error.status)
      ) {
        console.log(error);
        return throwError(error);
      }
      console.log(
        `Attempt ${retryAttempt}: retrying in ${retryAttempt *
        scalingDuration}ms`
      );
      // retry after 1s, 2s, etc...
      return timer(retryAttempt * scalingDuration);
    }),
    finalize(() => console.log('We are done!'))
  );
};

Note:

Someone had suggested about shareReplay() and so I had tried implementing it, but it was not able to handle the same request made from two other components/sources.

Following should only be 6, instead they are 12 on rapid click of two buttons calling same API (scaling duration was 1000ms).

enter image description here

NOTE:

Kindly avoid using FLAGS it's the final nuke in my opinion.

like image 612
Mercurial Avatar asked Oct 16 '19 18:10

Mercurial


1 Answers

Note, each time you call getInfoAPI() http.get() creates a new observable, and shareReplay() shares that new observable, it doesn't merge two calls. If you want the caller to get a merged observable, you can return the same observable from both calls. But it's wrong solution, I'll explain it later. For example:

export class DataService {
  private readonly getInfoRequest = new Subject<GetInfoRequest>();
  private readonly getInfoResponse = this.getInfoRequest.pipe(
    exhaustMap(request => { 
      const callURL = createGetInfoUrl(request);
      const callParams = createGetInfoParams(request);
      return this.http.get(callURL, callParams).pipe(
        retryWhen( ... );
      );
    })
  );

  public getInfoAPI(category:string, id:string = "", page:string = "1", limit:string = "10") {
    this.getInfoRequest.next({ category: category, id: id, page: page, limit: limit });
    return this.getInfoResponse;
  }

  ...
}

The code above does the same thing, that you are trying to implement by shareReplay(), but what if the calls arguments doesn't match? One component has requested the first page, but an other component has requested the second, the second component will receive the first page instead of the second. So, we should consider the call parameters too, the things become more complicated.

A solution could be to wrap the HttpService with a repository, which will handle the caching, it could cache data in memory, inside a database or somewhere else, but I doubt that it's what you want. As I understand the problem is simultaneous requests, the better way is to prevent such request. For example, if a request is triggered by a button click, just disable the button while a request is executing, or skip repeated requests. It's the usual way to solve such problems.

like image 155
Valeriy Katkov Avatar answered Oct 19 '22 02:10

Valeriy Katkov