Resiliency patterns in Angular, NestJS and RxJS

Resiliency in real life

Not so resilient

Not so resilient

getRecipes$(): Observable<Recipe[]> {
  return this.httpClient.get<Recipe[]>('https://super-recipes.com/api/recipes').pipe(
    catchError((error) => {
      this._snackBar.open('Recipes could not be fetched.', 'Close', {
        verticalPosition: 'top',
        horizontalPosition: 'right',
        panelClass: ['mat-error'],
      });
      return throwError(() => new Error('Recipes could not be fetched.'));
    }),
  );
}

Nikola Mitrović

Freelance Developer

📍Novi Sad, Serbia

Educator / Career Coach

Web Consultant

Tech Speaker

(Soon to be) Published Author

Retry pattern

Why do we fall?

So that we can learn to

pick ourselves up.

Alfred (Batman Begins)

getRecipes$(): Observable<Recipe[]> {
  return this.httpClient.get<Recipe[]>('https://super-recipes.com/api/recipes').pipe(
    catchError((error) => {
      this.showFeedbackMessage('Recipes could not be fetched.');
      return throwError(() => new Error('Recipes could not be fetched.'));
    }),
    retry(3)
  );
}

Retry pattern

RxJS retry operator

Exponential backoff pattern

Tim S. Grover

After you lose or when you get knocked down. Stay down there for a minute. Understand why you lost.

Because if you just jump right back up, you’re gonna lose again and again.

And you continue to lose the same way.

Exponential backoff pattern

getRecipesWithBackoffStrategy$(): Observable<Recipe[]> {
   return this.httpClient.get<Recipe[]>('https://super-recipes.com/api/recipes').pipe(
    catchError((error) => {
      this.showFeedbackMessage('Recipes could not be fetched.');
      return throwError(() => new Error('Recipes could not be fetched.'));
    }),
    retry({
      count: 3,
      delay: (error, retryCount) => {
        console.log(
          `Attempt ${retryCount}: 
           Error occurred during network request, 
           retrying in ${Math.pow(2, retryCount)} seconds...
          `
        );
        return timer(Math.pow(2, retryCount) * 1000);
      },
    }),
   );
  }

RxJS timer function

Bulkhead pattern


export const BulkheadInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, next: HttpHandlerFn) {
  private readonly MAX_CONCURRENT_REQUESTS = 3;
  private requestQueue = new Subject<Observable<any>>();

  constructor() {
    this.requestQueue
      .pipe(mergeMap((task) => task, this.MAX_CONCURRENT_REQUESTS))
      .subscribe();
  }

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    this.requestQueue.next(next.handle());

    return this.requestQueue.asObservable();
  }
}

Bulkhead pattern

@Injectable()
export class BulkheadInterceptor implements NestInterceptor {
  private readonly MAX_CONCURRENT_REQUESTS = 3;
  private activeRequests = 0;
  private requestQueue: (() => Observable<any>)[] = [];

  private processQueue() {
    if (
      this.requestQueue.length > 0 &&
      this.activeRequests < this.MAX_CONCURRENT_REQUESTS
    ) {
      const task = this.requestQueue.shift()!;
      this.activeRequests++;

      task().subscribe({
        complete: () => {
          this.activeRequests--;
          this.processQueue();
        },
      });
    }
  }

  intercept<T>(context: ExecutionContext, next: CallHandler): Observable<T> {
    const request$ = next.handle().pipe(
      catchError((err) => of(err)),
      finalize(() => {
        this.activeRequests--;
        this.processQueue();
      }),
    );

    if (this.activeRequests < this.MAX_CONCURRENT_REQUESTS) {
      this.activeRequests++;

      return request$;
    }

    const queueRequest$: Observable<T> = new Observable((observer) => {
      this.requestQueue.push(() => {
        return next.handle().pipe(
          catchError((err) => {
            observer.error(err);

            return of(err);
          }),
          map((result) => {
            observer.next(result);

            return result;
          }),
        );
      });

      // try to process the queue right away
      this.processQueue();
    });

    return queueRequest$;
  }
}

Queues

Semaphores

Thread Pools

Processes

Circuit breaker pattern

Circuit breaker - scenarios

CLOSED -> OPEN - Consecutive failures reach threshold

OPEN - Automatic reject

OPEN -> HALF_OPEN - After timeout period expires

HALF_OPEN -> OPEN - If next request fails

HALF_OPEN -> CLOSED - If next request pass

Circuit breaker

private openCircuit() {
  if (this.state === 'OPEN') return;

  this.state = 'OPEN';
  timer(5000)
    .subscribe(() => this.halfOpenCircuit());
}

private halfOpenCircuit() {
  this.state = 'HALF_OPEN';
}

private closeCircuit() {
  this.state = 'CLOSED';
}
getRecipesWithCircuitBreakerStrategy$(): Observable<Recipe[]> {
  return defer(() => this.sendRequestIfCircuitNotOpen()).pipe(
    catchError((error) => {
      this.showMessage();
      return throwError(
        () => new Error('Recipes could not be fetched.'));
      }),
      retry({
        count: this.state === 'HALF_OPEN' ? 1 : 3,
        delay: (error, retryCount) => {
          if (this.state === 'HALF_OPEN' || retryCount === 3) {
            this.openCircuit(); 
            return throwError(
              () => new Error('Circuit is open'));
          }

          return timer(2000);
        },
      }),
      tap(() => {
        // Successful retry, close the circuit
        this.closeCircuit(); 
        this._snackBar.dismiss();
      }),
   );

RxJS defer operator

OPEN -> HALF_OPEN - After timeout period expires

Circuit breaker pattern

sendRequestIfCircuitNotOpen() {
  if (this.state === 'OPEN') {
    return throwError(() => new Error('Circuit is open'));
  }
  
  return this.httpClient.get<Recipe[]>('https://super-recipes.com/api/recipes');
}

OPEN - Automatic reject

Circuit breaker

getRecipesWithCircuitBreakerStrategy$(): Observable<Recipe[]> {
  return defer(() => this.sendRequestIfCircuitNotOpen()).pipe(
    catchError((error) => {
      this.showMessage();
      return throwError(
        () => new Error('Recipes could not be fetched.'));
    }),
    retry({
      count: this.state === 'HALF_OPEN' ? 1 : 3,
      delay: (error, retryCount) => {
        if (this.state === 'HALF_OPEN' || retryCount === 3) {
          this.openCircuit(); 
          return throwError(() => new Error('Circuit is open'));
        }

        return timer(2000);
      },
    }),
    tap(() => {
      // Successful retry, close the circuit
      this.closeCircuit(); 
      this._snackBar.dismiss();
    }),
  );
}

CLOSED -> OPEN

OPEN

OPEN -> HALF_OPEN

HALF_OPEN -> OPEN

HALF_OPEN -> CLOSED

Optimistic update

Helen Keller

Optimism is the faith that leads to achievement.

 

Optimistic update

postRecipe(): void {
  this.http
    .post<Recipe>('https://super-recipes.com/api/recipes', this.recipe)
    .pipe(
      optimisticUpdate(this.recipe, (originalItem: Recipe, error: Error) => {
        // Rollback UI changes here
        console.error('Error updating item:', originalItem);
        this.recipes$.next(error);
      })
    )
    .subscribe((recipe: Recipe) => this.recipes$.next(recipe));
}

Optimistic update

export function optimisticUpdate<T, E = any>(
  originalValue: T,
  rollback: (value: T, error: E) => void,
  retryConfig: { count: number; delay: number } = { count: 1, delay: 20000 }
): (source: Observable<T>) => Observable<T> {
  return (source: Observable<T>) => {
    return concat(
      of(originalValue),
      source.pipe(
        ignoreElements(),
        retry(retryConfig),
        catchError((error) => {
          rollback(originalValue, error);
          return throwError(() => error);
        })
      )
    );
  };
}

Optimistic update

postRecipe(): void {
  this.http
    .post<Recipe>('https://super-recipes.com/api/recipes', this.recipe)
    .pipe(
      optimisticUpdate(this.recipe, (originalItem: Recipe, error: Error) => {
        // Rollback UI changes here
        console.error('Error updating item:', originalItem);
        this.recipes$.next(error);
      })
    )
    .subscribe((recipe: Recipe) => this.recipes$.next(recipe));
}

NestJS

Retry

Exponential backoff

Circuit breaker

Bulkhead

getRecipes$(): Observable<Recipe[]> {
  return this.httpClient.get<Recipe[]>('/api/recipes').pipe(
    catchError((error) => {
      this.logError(error);
      
      return throwError(
        () => new Error('Recipes could not be fetched.'));
    }),
    retry(3)
  );
}
getRecipesWithBackoffStrategy$(): Observable<Recipe[]> {
   return this.httpClient.get<Recipe[]>('/api/recipes').pipe(
    catchError((error) => {
      this.logError(error);
      return throwError(
        () => new Error('Recipes could not be fetched.'));
    }),
    retry({
      count: 3,
      delay: (error, retryCount) => {
        return timer(Math.pow(2, retryCount) * 1000);
      },
    }),
   );
  }
getRecipesWithCircuitBreakerStrategy$(): Observable<Recipe[]> {
  return defer(() => this.sendRequestIfCircuitNotOpen()).pipe(
    catchError((error) => {
      this.logError(error);
      return throwError(
        () => new Error('Recipes could not be fetched.'));
    }),
    retry({
      count: this.state === 'HALF_OPEN' ? 1 : 3,
      delay: (error, retryCount) => {
        if (this.state === 'HALF_OPEN' || retryCount === 3) {
          this.openCircuit(); 
          return throwError(() => new Error('Circuit is open'));
        }

        return timer(2000);
      },
    }),
    tap(() => {
      // Successful retry, close the circuit
      this.closeCircuit(); 
    }),
  );
}
import { Observable, Subject, mergeMap } from 'rxjs';

@Injectable()
export class BulkheadInterceptor implements NestInterceptor {
  private readonly MAX_CONCURRENT_REQUESTS = 3;
  private requestQueue = new Subject<Observable<any>>();

  constructor() {
    this.requestQueue
      .pipe(mergeMap((task) => task, this.MAX_CONCURRENT_REQUESTS))
      .subscribe();
  }

  intercept(
    context: ExecutionContext, 
    next: CallHandler
  ): Observable<any> {
    this.requestQueue.next(next.handle());

    return this.requestQueue.asObservable();
  }
}

Thank you!

nikola.mitrovic1313@gmail.com

You can find me at

Link to the slides

https://github.com/PacktPublishing/RxJS-Cookbook-for-Reactive-Developers

Resiliency patterns

By nmitrovic

Resiliency patterns

  • 19