cockatiel

repository·master·Indexed 23 days ago

https://github.com/connor4312/cockatiel

A resilience and transient-fault-handling library for JavaScript and TypeScript, inspired by .NET Polly. It allows developers to implement policies such as Backoff, Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner. Version 4.0.0.

Tokens
12.3K
Snippets
28
Records
68
Agent score
82%

What's inside cockatiel

  1. Understand the IPolicy interface

    master

    All Cockatiel fault handling policies (fallbacks, circuit breakers, bulkheads, timeouts, retries) implement the IPolicy interface.

    Key features:

    • execute<T>(fn, signal): The primary method to run a function through the policy's behavior. It returns a Promise<T>. You can optionally pass an AbortSignal.
    • onSuccess: An event that fires when a request successfully completes. In a retry policy, this fires once even if multiple retries occurred.
    • onFailure: An event that fires when a request fails due to a handled reason. Cockatiel does not treat all thrown errors as failures; you must define which errors the policy should handle.

    Note: The context object passed to the function in execute contains metadata (like AbortSignal or retry attempt numbers) depending on the policies applied.

  2. Implement or use custom backoff strategies with IBackoff

    master

    In Cockatiel, backoff strategies determine the delay between retry attempts. The IBackoff<T> interface defines a strategy that provides a duration (in milliseconds) for the current attempt. It extends IBackoffFactory<T>, which provides a next(context: T) method to calculate the next backoff interval based on the current retry context.

    Common implementations include ConstantBackoff, ExponentialBackoff, and IterableBackoff.

    // Example of what an IBackoff implementation looks like conceptually
    export interface IBackoff<T> extends IBackoffFactory<T> {
      readonly duration: number;
    }
  3. Understand CircuitState transitions

    master

    The CircuitBreakerPolicy operates in one of four states:

    • CircuitState.Closed: Normal operation. Actions are executed. If the breaker determines a failure threshold is met, the state changes to Open.
    • CircuitState.Open: The circuit is broken. All calls to execute() immediately throw a BrokenCircuitError. The circuit stays in this state for the duration specified by halfOpenAfter.
    • CircuitState.HalfOpen: The circuit is testing the waters. A limited number of calls (halfOpenSampling.calls) are permitted. If they succeed (within the threshold), the circuit returns to Closed. If they fail, it returns to Open.
    • CircuitState.Isolated: The circuit is manually held open via the isolate() method. All calls throw an IsolatedCircuitError.
  4. How policies and filters work together

    master

    In Cockatiel, a Policy defines which errors or results should trigger a specific behavior (like retrying or falling back). You create a base Policy by defining errorFilter and resultFilter options. You can then extend this base policy using methods like .orType(), .orWhen(), or .orResultType() to add more conditions to the existing filters.

    Common patterns include:

    • Error Filtering: Deciding which thrown errors should be handled by the policy.
    • Result Filtering: Deciding which successful return values should be treated as failures (e.g., a response with a 500 status code).

    Once a Policy is configured, it is passed to specific policy builders like retry(), circuitBreaker(), or fallback() to create an executable IPolicy.

    // retry both network errors and response errors with a 503 status code
    new Policy()
     .orType(NetworkError)
     .orType(ResponseError, err => err.statusCode === 503)
     .retry()
     .attempts(3)
     .execute(() => getJsonFrom('https://example.com'));
  5. Quickstart: Combining Retry and Circuit Breaker policies

    master

    Cockatiel allows you to compose multiple resilience patterns. You can create a retry policy, a circuit breaker policy, and then use wrap() to combine them into a single policy. This is useful for scenarios where you want to retry an operation but also want to stop calling a failing service to allow it to recover.

    import {
      ConsecutiveBreaker,
      ExponentialBackoff,
      retry,
      handleAll,
      circuitBreaker,
      wrap,
    } from 'cockatiel';
    import { database } from './my-db';
    
    // Create a retry policy that'll try whatever function we execute 3
    // times with a randomized exponential backoff.
    const retryPolicy = retry(handleAll, { maxAttempts: 3, backoff: new ExponentialBackoff() });
    
    // Create a circuit breaker that'll stop calling the executed function for 10
    // seconds if it fails 5 times in a row.
    const circuitBreakerPolicy = circuitBreaker(handleAll, {
      halfOpenAfter: 10 * 1000,
      breaker: new ConsecutiveBreaker(5),
    });
    
    // Combine these! Create a policy that retries 3 times, calling through the circuit breaker
    const retryWithBreaker = wrap(retryPolicy, circuitBreakerPolicy);
    
    exports.handleRequest = async (req, res) => {
      // Call your database safely!
      const data = await retryWithBreaker.execute(() => database.getInfo(req.params.id));
      return res.json(data);
    };
  6. Use a no-op policy for conditional logic

    master

    The noop policy does nothing and simply returns the result of the function passed to execute. This is useful for providing a default policy that can be swapped out for a real policy (like retry) based on environment settings (e.g., production vs. test).

    import { noop, handleAll, retry } from 'cockatiel';
    
    const policy = isProduction ? retry(handleAll, { attempts: 3 }) : noop;
    
    export async function handleRequest() {
      return policy.execute(() => getInfoFromDatabase());
    }
  7. Use `DelegateBackoff` for custom logic

    master

    The DelegateBackoff allows you to determine the delay using a custom function.

    Signature: new DelegateBackoff((context, lastError) => ...)

    Context: The context object (of type IRetryBackoffContext) provides:

    • attempt: The current retry attempt (starting at 1).
    • result: { error: unknown } | { value: ReturnType } representing the last result.

    You can also return an object containing { state: S, delay: number } to maintain state across attempts.

    import { DelegateBackoff } from 'cockatiel';
    
    const myDelegateBackoff = new DelegateBackoff((context, lastError) => {
      // Stop retrying if we get the same error twice in a row
      if (context.result.error && context.result.error === lastError) {
        throw context.result.error;
      }
    
      // Otherwise, do exponential backoff with state tracking
      return { 
        delay: 100 * Math.pow(2, context.attempt), 
        state: context.result.error 
      };
    });
  8. Prevent Node.js process hang with `retry.dangerouslyUnref()`

    master

    By default, the timers used for backoff keep the Node.js event loop active. If you want the process to be able to exit even if a retry delay is pending, call .dangerouslyUnref() on the retry builder before calling .execute().

    const response1 = await retry(handleAll, { maxAttempts: 3 })
      .dangerouslyUnref()
      .execute(() => getJson('https://example.com'));
  9. Listen to an Event Once with `Event.once`

    master

    The Event.once(event, callback) utility waits for an event to fire exactly once, then automatically unregisters the listener.

    This method returns an IDisposable, which allows you to manually unregister the listener if the event hasn't fired yet.

    import { Event } from 'cockatiel';
    
    async function waitForFallback(policy) {
      Event.once(policy.onFallback, () => {
        console.log('a fallback happened!');
      });
    }
  10. Subscribe to Policy Events

    master

    Cockatiel policies provide events for monitoring lifecycle changes (e.g., onFailure, onFallback, onRetry, onSuccess, onGiveUp). You can subscribe to these events by passing a callback function.

    Each subscription returns an IDisposable instance. To stop listening and prevent memory leaks, call .dispose() on that instance. It is safe to call .dispose() multiple times.

    const listener = policy.onFailure(error => {
      console.log(error);
    });
    
    // Later, to unsubscribe:
    listener.dispose();
  11. Convert Events to Promises with `Event.toPromise`

    master

    The Event.toPromise(event[, signal]) utility returns a promise that resolves once a specific event fires.

    If you provide an AbortSignal, the promise will reject with a TaskCancelledError if the signal is aborted before the event occurs.

    import { Event } from 'cockatiel';
    
    async function waitForFallback(policy) {
      await Event.toPromise(policy.onFallback);
      console.log('a fallback happened!');
    }