Fusebox Fault Tolerance Library

repository·master·Indexed 41 days ago

https://github.com/potetm/fusebox

A lightweight fault tolerance library for Clojure, ClojureScript, and Babashka. It provides modular resilience patterns including Bulkhead, Circuit Breaker, Retry, Fallback, Rate Limit (Token Bucket), Memoize, and Timeout using a functional, sequential approach. Features include the Bulwark macro for composing utilities and a registry for organizing specifications.

Tokens
5.1K
Snippets
14
Records
27
Agent score
3%

What's inside Fusebox

  1. Overview of Fusebox

    master

    Fusebox is an extremely lightweight fault tolerance library designed for Clojure, ClojureScript, and Babashka. It focuses on simplicity and performance by providing modular resilience patterns without the complexity of heavyweight alternatives.

    Key design principles include:

    • Fast: Optimized for performance.
    • Pure Functions: Prefers pure functions over complex configuration options.
    • Modular: You only load the specific resilience patterns you need.
    • Sequential Execution: Avoids callback-driven models in favor of sequential execution.
    • Simple Configuration: Uses un-nested hashmaps with namespaced keys.
    • Minimal Dependencies: Only depends on clojure/tools.logging.
  2. Understand Fusebox performance benchmarks

    master

    Fusebox performance is measured against Resilience4J using JMH (Java Microbenchmark Harness). Benchmarks are designed to measure the overhead Fusebox adds to an application by comparing a 'Baseline' (a simple CPU-consuming task) against the same task wrapped in Fusebox utilities.

    Key benchmarking characteristics:

    • Test Case: Each test performs a single call to (org.openjdk.jmh.infra.Blackhole/consumeCPU 100) to prevent JIT optimization from removing the code.
    • Concurrency: Tests include both 'serial' (1 thread) and 'parallel' (8 threads) runs to evaluate overhead in optimal throughput scenarios.
    • Optimization: Benchmarks are often run with direct-linking enabled to minimize Clojure's wrapping overhead, providing a more accurate comparison of the core logic.
  3. Manage and compose spec maps

    master

    The init function returns a hashmap known as a spec. Specs are standard Clojure hashmaps and can be treated as such: they can be passed around, stored in definitions, registered, or merged with other specs to create complex resilience configurations.

    When calling init, you can pass extra keys (like headers or metadata) and Fusebox will merge them without altering them.

    ;; Merging multiple specs into one configuration
    (merge 
      (retry/init {::retry/retry? (fn [n ms ex] (< n 10))})
      (to/init {::to/timeout-ms 500})
      (fallback/init {::fallback/fallback (fn [ex] :default-val!}))
      (rl/init {::rl/bucket-size 10}))
  4. Compose resilience utilities using pass-through invocations

    master

    Fusebox utilities are designed to be composable by accepting hashmaps that do not necessarily contain the required configuration keys. If a utility is called with a map that lacks its required keys (or with nil), it acts as a pass-through, executing the provided body as-is.

    This pattern allows you to define a general-purpose function wrapped in multiple resilience layers and then selectively 'opt-in' to specific behaviors by merging configuration maps into the request.

    (defn http [req]
      (retry/with-retry req
        (rl/with-rate-limit req
          (http/invoke req))))
    
    ;; Usage examples:
    (http (merge req retry-spec))          ;; Only retry
    (http (merge req rate-limit-spec))    ;; Only rate limit
    (http (merge req retry-limit-spec rl-spec)) ;; Both
  5. Use Fusebox in ClojureScript

    master

    Every utility has a corresponding .cljs. namespace (e.g., com.potetm.fusebox.cljs.retry). The API is identical to the Java/Clojure version with two key differences:

    1. Promises: Every utility accepts and returns Promises instead of regular forms/functions.
    2. AbortController: The with-timeout utility accepts an optional AbortController which can be passed to fetch to terminate network calls.

    Namespace mapping:

    • com.potetm.fusebox.cljs.bulkhead
    • com.potetm.fusebox.cljs.circuit-breaker
    • com.potetm.fusebox.cljs.fallback
    • com.potetm.fusebox.cljs.memoize
    • com.potetm.fusebox.cljs.rate-limit
    • com.potetm.fusebox.cljs.registry
    • com.potetm.fusebox.cljs.retry
    • com.potetm.fusebox.cljs.timeout
    ;; Example: Retry with Promises
    (-> (retry/with-retry (retry/init {::retry/retry? (fn [n ms ex] (< n 10))})
          (js/Promise.resolve :done!))
        (.then println))
    
    ;; Example: Timeout with AbortController
    (to/with-timeout [abort-controller] (to/init {::to/timeout-ms 1})
      (js/fetch "https://httpbin.org/delay/1"
                (js-obj "signal" (.-signal abort-controller))))
  6. Configure JVM options for optimal Fusebox performance

    master

    To achieve performance comparable to direct-linked environments (minimizing Clojure wrapping overhead), ensure the following JVM options are used:

    • -Dclojure.compiler.direct-linking=true: Enables direct linking to reduce function call overhead.
    • -Dfusebox.usePlatformThreads=false: Configures thread usage for Fusebox.
    • -XX:-OmitStackTraceInFastThrow: Prevents the JVM from optimizing away stack traces, which is useful for consistent error measurement.

    Example JVM options for a high-performance setup:

    -XX:-OmitStackTraceInFastThrow -Dfusebox.usePlatformThreads=false -Dclojure.compiler.direct-linking=true
  7. Install Fusebox

    master

    To use Fusebox in your Clojure or ClojureScript project, add the following dependency to your deps.edn file. Fusebox has only one dependency: clojure/tools.logging.

    com.potetm/fusebox {:mvn/version "1.0.11"}
  8. Initialize and shutdown Fusebox utilities

    master

    Every Fusebox namespace provides an init and shutdown function. Even for stateless utilities like Retry that are essentially maps of pure functions, these functions are provided to:

    1. Provide validation on startup.
    2. Maintain a uniform interface across all utilities.

    You should always call init and shutdown to ensure proper lifecycle management and configuration validation.

  9. Disable a utility for a single invocation

    master
    Every namespace includes a disable function. Calling this function disables the specific utility for that invocation only. It does not affect future calls or other threads. This is particularly useful for testing in a REPL when you want to bypass a failing utility (like a retry loop) without changing the global configuration.
  10. Use Bulwark to compose multiple utilities

    master

    Bulwark is a convenience macro that applies a default ordering of Fusebox utilities. It wraps your code in the following nested layers (from outermost to innermost):

    1. fallback/with-fallback
    2. retry/with-retry
    3. cb/with-circuit-breaker
    4. bh/with-bulkhead
    5. rl/with-rate-limit
    6. to/with-timeout

    Because of Fusebox's pass-through invocation design, you can use this ordering for any combination of utilities by merging their respective init specs into a single map.

    (require '[com.potetm.fusebox.bulkhead :as bh]
             '[com.potetm.fusebox.bulwark :as bw]
             '[com.potetm.fusebox.circuit-breaker :as cb]
             '[com.potetm.fusebox.fallback :as fallback]
             '[com.potetm.fusebox.rate-limit :as rl]
             '[com.potetm.fusebox.retry :as retry]
             '[com.potetm.fusebox.timeout :as to])
    
    (def spec
      (merge (retry/init {::retry/retry? (fn [c dur ex] (< c 10))
                          ::retry/delay (constantly 10)})
             (to/init {::to/timeout-ms 500})
             (fallback/init {::fallback/fallback (fn [ex] :yes!)})
             (cb/init {::cb/next-state (partial cb/next-state:default
                                                {:fail-pct 0.5
                                                 :slow-pct 0.5
                                                 :wait-for-count 3
                                                 :open->half-open-after-ms 100})
                       ::cb/hist-size 10
                       ::cb/half-open-tries 3
                       ::cb/slow-call-ms 100})
             (rl/init {::rl/bucket-size 10
                       ::rl/period-ms 1000
                       ::rl/wait-timeout-ms 100})
             (bh/init {::bh/concurrency 5
                       ::bh/wait-timeout-ms 100})))
    
    (bw/bulwark spec
      (run))
  11. Use Retry with custom logic

    master

    Retry retries a call in the event of failure.

    Configuration options:

    • ::retry/retry?: Predicate called after an exception. Takes (eval-count, exec-duration-ms, exception/failing-value). Returns true to retry.
    • ::retry/delay: Function calculating delay in millis before next attempt. Takes (eval-count, exec-duration-ms, exception/failing-value). Use helpers like delay-exp, delay-linear, and jitter to build this.
    • ::retry/success?: (Optional) Function determining if a return value is successful. If false, the body is retried. The last failing value is available under ::retry/val in the exception's data.
    • ::retry/exception: (Optional) Function returning the exception to throw once ::retry? is false. Options include default-ex, wrap-ex-after-retry, and no-wrap-ex.

    Diagnostic Bindings: You can pass [retry-count exec-duration-ms] as the first argument to with-retry to access these values inside your code block.

    (require '[com.potetm.fusebox.retry :as retry])
    
    (def retry
      (retry/init {::retry/retry? (fn [n ms ex] (< n 10))
                   ::retry/delay (fn [n ms ex] (min (retry/delay-exp n) 5000))}))
    
    (retry/with-retry retry
      (run))
  12. Use Rate Limit (Token Bucket)

    master

    A Rate Limiter limits the rate at which your application calls another application. Fusebox implements a Token Bucket algorithm.

    To implement a Leaky Bucket, set ::rl/bucket-size to 1 and adjust ::rl/period-ms.

    Configuration options:

    • ::rl/bucket-size: Integer number of tokens per period.
    • ::rl/period-ms: Milliseconds in each period.
    • ::rl/wait-timeout-ms: Maximum milliseconds a thread waits for a token.
    (require '[com.potetm.fusebox.rate-limit :as rl])
    
    (def rate-limit
      (rl/init {::rl/bucket-size 10
                ::rl/period-ms 1000
                ::rl/wait-timeout-ms 5000}))
    
    (rl/with-rate-limit rate-limit
      (run))