Perseverance Clojure Library

repository·master·Indexed 41 days ago

https://github.com/grammarly/perseverance

A flexible Clojure library for retrying operations that decouples the marking of retriable code from the retry strategy. It provides the `retriable` macro to mark blocks of code and the `retry` macro to define execution contexts using strategies like `constant-retry-strategy` and `progressive-retry-strategy` (exponential backoff).

Tokens
1.5K
Snippets
8
Records
9
Agent score
2%

What's inside Perseverance

  1. Nesting retry blocks with selectors

    master

    You can nest retry blocks to apply different strategies to different types of failures within the same execution flow by using :selector with keywords.

    (retry {:strategy (constant-retry-strategy 500)} ;; Catch-all strategy
      (retry {:strategy (progressive-retry-strategy :initial-delay 2000, :max-delay 10000)
              :selector ::list-files} ;; Specific strategy for :list-files
        (download-all-files)))
    (retry {:strategy (constant-retry-strategy 500)} ;; Catches everything.
      (retry {:strategy (progressive-retry-strategy :initial-delay 2000, :max-delay 10000)
              :selector ::list-files}
        (download-all-files)))
  2. Considerations for multi-threaded and lazy code

    master

    Perseverance relies on Clojure's dynamic variables and try/catch blocks. This leads to two main risks:

    1. Concurrency: If the code inside retriable or retry escapes the dynamic scope (e.g., by moving to a different thread via future or core.async's go blocks), the retry context may be lost. While some modern primitives forward dynamic bindings, caution is required.
    2. Laziness: Lazily evaluated code can cause issues because the actual execution (and thus the exception) might occur outside the scope of the retry macro.

    Additionally, ensure your operations are idempotent before using Perseverance, as retries will re-execute the code block.

  3. How retriable and retry work together

    master

    Perseverance decouples the logic of what is retriable from the decision of how to retry it using two main macros:

    1. retriable: Used in low-level code to mark a block as suitable for retrying. It catches specific exceptions and can attach a :tag or wrap the exception.
    2. retry: Used in high-level code to define the execution context (strategy, selector, logging) for retrying the marked blocks.

    An error inside a retriable block will not be retried unless it is wrapped in a retry block that matches its criteria (via :selector or :tag).

    ;; 1. Mark low-level code as retriable
    (defn my-function []
      (retriable {:catch [RuntimeException] :tag ::my-tag}
        (do-something-unreliable)))
    
    ;; 2. Define the retry strategy in high-level code
    (retry {:selector ::my-tag :strategy (constant-retry-strategy 100)}
      (my-function))
  4. Create a custom retry strategy

    master

    A strategy is a function that takes the current attempt number (starting from 1) and returns a delay in milliseconds, or nil if retrying should stop.

    (defn my-custom-strategy [attempt] 
      (if (< attempt 5) 500 0)) ;; Retry 4 times with 500ms delay
    
    (retry {:strategy my-custom-strategy} 
      (some-retriable-code))
    (defn my-custom-strategy [attempt] 500)
  5. Use the retriable macro to mark code for retry

    master

    The retriable macro marks a piece of code as suitable for retrying. It takes an options map and a body.

    ** Options:

    • :catch: A list of Exception classes to catch. Defaults to [java.io.IOException].
    • :tag: A keyword attached to the caught exception so a retry block can identify it.
    • :ex-wrapper: A function #(ex-info "msg" {:data %}) called on the caught exception to return a wrapped exception. If provided, :tag is ignored.
    (retriable {:catch [RuntimeException]
                :tag ::list-files
                ;; :ex-wrapper #(ex-info "My wrapped exception". {:e %, :foo :bar})
                }
      (list-s3-files))
    (retriable {:catch [RuntimeException]
                :tag ::list-files
                }
      (list-s3-files))
  6. Use the retry macro to execute retries

    master

    The retry macro provides the execution context for retrying code marked with retriable. It takes an options map and a body.

    ** Options:

    • :strategy: Specifies the delay and max attempts. If omitted, a progressive strategy with default settings is used. See Strategies.
    • :selector: Determines which errors trigger a retry. Can be a keyword (matches the :tag from retriable) or a predicate function (called with the wrapped exception). If omitted, it handles any error from a retriable block.
    • :log-fn: A function [wrapped-ex attempt delay] called on each retry. Defaults to printing to stdout.
    (retry {:strategy (constant-retry-strategy 100)
            :selector ::list-files
            }
      (download-all-files))
    (retry {:strategy (constant-retry-strategy 100)
            :selector ::list-files
            }
      (download-all-files))
  7. Configure constant-retry-strategy

    master

    The constant-retry-strategy returns the same delay for every attempt. If :max-count is provided, it returns nil after the limit is reached, signaling Perseverance to stop retrying.

    (constant-retry-strategy 100)
    ;; or with a limit
    (constant-retry-strategy 100 :max-count 5)
    (constant-retry-strategy 100)
  8. Configure progressive-retry-strategy

    master

    The progressive-retry-strategy implements an exponential backoff algorithm.

    ** Parameters:

    • :initial-delay: The starting delay in milliseconds.
    • :stable-length: The number of times the initial-delay is returned before the multiplier is applied.
    • :multiplier: The factor by which the delay is multiplied after the stable period.
    • :max-delay: The maximum allowed delay.
    • :max-count: (Optional) The number of attempts after which the strategy returns nil to stop retrying.

    Example configuration:

    (progressive-retry-strategy :initial-delay 1000, :stable-length 4, :multiplier 2, :max-delay 10000)
    ;; Delays: 1000, 1000, 1000, 1000, 2000, 4000, 8000, 10000, 10000, 10000...
    (progressive-retry-strategy :initial-delay 1000, :stable-length 4, :multiplier 2, :max-delay 10000)