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

Career Development Coach

Web Consultant

Tech Speaker

Published Author

Retry pattern

Why do we fall?

So that we can learn to

pick ourselves up.

Alfred (Batman Begins)

import { Observable, catchError, retry } from 'rxjs';

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

import { Observable, catchError, repeat, retry, timer } from 'rxjs';

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

import { Subject, Observable, mergeMap } from 'rxjs';

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

Queues

Semaphores

Thread Pools

Processes

RxJS mergeMap operator

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

import { Observable, of, throwError, catchError, retry, ignoreElements } from 'rxjs';

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

  • 78