retry-ts

repository·master·Indexed 41 days ago

https://github.com/gcanti/retry-ts

A TypeScript port of the PureScript 'purescript-aff-retry' and Haskell 'retry' packages. It provides a functional approach to defining and executing composable retry policies for asynchronous operations, specifically designed for monadic actions that signal failure via types (such as fp-ts TaskEither). Features include exponential backoff, retry limits, and delay capping.

Tokens
5.4K
Snippets
28
Records
28
Agent score
49%

What's inside retry-ts

  1. Understand the RetryPolicy concept

    master

    A RetryPolicy is a function that determines whether to continue retrying and how long to wait between attempts. It accepts a RetryStatus object and returns an Option<number> representing the delay in milliseconds.

    • If the function returns a delay, the retry continues after that delay.
    • If the function returns None, the retry process terminates.

    Iteration numbers (found in RetryStatus) start at 0 for the first attempt and increase by one on each subsequent retry.

    export interface RetryPolicy {
      (status: RetryStatus): O.Option<number>
    }
  2. Compose retry policies with Monoid.concat

    master

    Retry policies in retry-ts are composable. You can use Monoid.concat to combine multiple policy primitives into a single complex policy.

    Common primitives include:

    • exponentialBackoff(initialDelay): Increases the delay exponentially between retries.
    • limitRetries(n): Limits the total number of retry attempts to n.
    • capDelay(maxDelay, policy): Ensures that the delay produced by the underlying policy never exceeds maxDelay.
    import { capDelay, exponentialBackoff, limitRetries, Monoid } from 'retry-ts'
    
    const policy = capDelay(2000, Monoid.concat(exponentialBackoff(200), limitRetries(5)))
  3. Combine multiple RetryPolicies using Monoid

    master

    The RetryPolicy implements a Monoid, allowing you to combine multiple strategies into a single policy using the concat method.

    When combining policies:

    1. Termination: If either policy returns None, the combined policy returns None (terminating the retries).
    2. Delay Selection: If both policies return a delay, the larger of the two delays is used.

    This is useful for layering constraints, such as applying an exponential backoff while also enforcing a maximum number of retries.

    import { monoidRetryPolicy, exponentialBackoff, limitRetries } from 'retry-ts'
    
    // One can easily define an exponential backoff policy with a limited
    // number of retries:
    export const limitedBackoff = monoidRetryPolicy.concat(exponentialBackoff(50), limitRetries(5))
  4. Use retry-ts for retrying TaskEither operations

    master

    To retry an asynchronous operation (like a TaskEither), use the retrying function from retry-ts/Task. This function requires a retry policy and a predicate function to determine if the operation should be retried.

    Core Components

    • Policy: A combination of delay and limit strategies (e.g., exponentialBackoff, limitRetries, capDelay).
    • Predicate: A function that receives the current RetryStatus and returns true if the operation should be retried, or false to stop.
    • RetryStatus: An object containing information about the current attempt, such as previousDelay (an Option containing the delay used for the last attempt).

    Policies can be composed using Monoid.concat to combine different behaviors like backoff and retry limits.

    import { capDelay, exponentialBackoff, limitRetries, Monoid } from 'retry-ts'
    import { retrying } from 'retry-ts/Task'
    import * as E from 'fp-ts/Either'
    import * as TE from 'fp-ts/TaskEither'
    import { pipe } from 'fp-ts/function'
    
    // 1. Define a policy: exponential backoff starting at 200ms, 
    // capped at 2000ms, with a maximum of 5 retries.
    const policy = capDelay(2000, Monoid.concat(exponentialBackoff(200), limitRetries(5)))
    
    // 2. Define the operation to retry
    const fakeAPI = TE.left('API errored out')
    
    // 3. Execute the retry logic
    // The predicate `E.isLeft` tells the engine to retry if the result is a Left (error).
    const result = retrying(
      policy, 
      (status) => pipe(TE.rightIO(()), TE.apSecond(fakeAPI)), 
      E.isLeft
    )
    
    // result() returns a TaskEither
    result().then((e) => console.log(e))
  5. What is a RetryPolicy and how does it work?

    master

    A RetryPolicy is a function that determines how long to wait before the next attempt based on the current RetryStatus.

    It takes a RetryStatus object and returns an Option<number> representing the delay in milliseconds.

    • If the function returns Some(delay), the system waits for that delay before retrying.
    • If the function returns None, it implies the retry limit has been reached and the process should stop.

    Iteration numbers start at 0 for the first attempt and increase by one on each subsequent retry.

    export interface RetryPolicy {
      (status: RetryStatus): O.Option<number>
    }
  6. Use retry-ts for retrying asynchronous operations

    master

    The retry-ts library provides tools to define retry policies and execute asynchronous tasks with those policies. You can compose policies using Monoid.concat to combine different strategies like backoff, delay capping, and retry limits.

    To use it with fp-ts tasks, use the retrying function from retry-ts/Task. This function takes a policy, a function that receives the current RetryStatus (allowing for side effects like logging based on the current attempt), and a predicate to determine if the operation should be retried (e.g., checking if an Either is a Left).

    import { log } from 'fp-ts/Console'
    import * as E from 'fp-ts/Either'
    import { pipe } from 'fp-ts/function'
    import * as O from 'fp-ts/Option'
    import * as TE from 'fp-ts/TaskEither'
    import { capDelay, exponentialBackoff, limitRetries, Monoid, RetryStatus } from 'retry-ts'
    import { retrying } from 'retry-ts/Task'
    
    // Define a policy: exponential backoff starting at 200ms, capped at 2000ms, with a maximum of 5 retries
    const policy = capDelay(2000, Monoid.concat(exponentialBackoff(200), limitRetries(5)))
    
    const fakeAPI = TE.left('API errored out')
    
    // A side-effect function that logs the current status
    const logDelay = (status: RetryStatus) =>
      TE.rightIO(
        log(
          pipe(
            status.previousDelay,
            O.map((delay) => `retrying in ${delay} milliseconds...`),
            O.getOrElse(() => 'first attempt...')
          )
        )
      )
    
    // Execute the task with the policy
    const result = retrying(policy, (status) => pipe(logDelay(status), TE.apSecond(fakeAPI)), E.isLeft)
    
    result().then((e) => console.log(e))
  7. Use applyAndDelay to manage retry status and delays

    master

    The applyAndDelay function is used to apply a RetryPolicy to an existing RetryStatus. If the policy dictates a retry, it calculates the necessary delay and returns an updated RetryStatus wrapped in a ReaderTask. This is typically used within a retry loop to handle the timing and state transitions between attempts.

    export declare const applyAndDelay: <R>(policy: RetryPolicy, status: RetryStatus) => ReaderTask<R, RetryStatus>
  8. Use retrying to retry actions that signal failure via types

    master

    The retrying combinator is designed for actions that do not throw exceptions but instead signal failure through their return types (e.g., Option, Either, or EitherT).

    To use it, you provide:

    1. A RetryPolicy to define how to retry.
    2. An action function that takes the current RetryStatus and returns a Task<A>.
    3. A check function that takes the result of the action (A) and returns true if the result indicates a failure that should trigger a retry, or false otherwise.

    This allows you to wrap logic that returns values like Left or None and automatically retry based on the policy until the check function returns false (success) or the policy is exhausted.

    export declare function retrying<A>(
      policy: RetryPolicy,
      action: (status: RetryStatus) => T.Task<A>,
      check: (a: A) => boolean
    ): T.Task<A>
  9. Cap delays with capDelay

    master

    The capDelay(maxDelay: number, policy: RetryPolicy) function wraps an existing policy to ensure that no single delay ever exceeds maxDelay.

    Note: capDelay does not terminate retries. If the underlying policy is infinite (like constantDelay), the combined policy will retry forever with a delay of maxDelay once the cap is hit. To ensure termination, combine capDelay with a limitRetries variant.

    export declare function capDelay(maxDelay: number, policy: RetryPolicy): RetryPolicy
  10. Create an exponential backoff policy with exponentialBackoff

    master

    The exponentialBackoff(delay: number) function creates a policy where the delay grows exponentially by a factor of two each iteration, starting from the provided initial delay.

    export declare function exponentialBackoff(delay: number): RetryPolicy