A general-purpose resilience library for safely calling unreliable external resources (HTTP APIs, databases, message queues, etc.).
Plain data, higher-order function pattern, natural composition via comp / ->.
;; project.clj
[org.clojars.konkon/resilience-clj "0.2.0"](require '[resilience-clj.circuit-breaker :as cb])
;; Create
(def breaker (cb/create {:failure-threshold 5
:reset-timeout-ms 10000
:half-open-calls 1
:failure? (fn [result]
(and (map? result)
(>= (:status result) 500)))
:on-state-change (fn [old new]
(println "Circuit:" old "->" new))}))
;; Wrap a function
(def safe-call (cb/wrap breaker http-get))
;; Use
(safe-call "https://api.example.com")
;; => On success: returns http-get result as-is
;; => When open: {:resilience/rejected :circuit-open}
;; Check status
(cb/status breaker) ;; => :closed | :open | :half-open
;; Manual reset
(cb/reset! breaker)
;; Shutdown (stops go-loop)
(cb/shutdown! breaker)Notes:
:failure-thresholdcounts consecutive failures — a success while:closedresets the counter.:half-opendoes not limit concurrent probe calls;:half-open-callsonly sets how many successes are needed to close.
(require '[resilience-clj.retry :as retry])
;; Synchronous
(def resilient-call
(retry/wrap http-get {:max-attempts 3
:backoff-ms 1000
:backoff-factor 2
:jitter 0.1
:retry? (fn [r] (>= (:status r) 500))}))
(resilient-call "https://api.example.com")
;; Asynchronous (returns a core.async channel)
(require '[clojure.core.async :as async])
(def async-call
(retry/wrap-async http-get {:max-attempts 3 :backoff-ms 500}))
(async/<!! (async-call "https://api.example.com"))Notes:
wrap-asyncrunsfon a dedicated thread (async/thread), so blocking functions (HTTP calls etc.) are safe to wrap.- When retries are exhausted by exceptions, the last exception object is
put on the channel as a value (not re-thrown) — check with
(instance? Throwable result).
(require '[resilience-clj.timeout :as timeout])
(def safe-call (timeout/wrap http-get {:timeout-ms 3000}))
(safe-call "https://api.example.com")
;; => On success: returns http-get result as-is
;; => On timeout: {:resilience/rejected :timeout}Notes:
- Exceptions thrown by
fare re-thrown as-is (ex-data preserved). - On timeout the underlying future is cancelled via thread interrupt; if
fignores interruption, its thread keeps running untilfreturns.
All modules express rejection using the unified format {:resilience/rejected <reason>}.
(require '[resilience-clj.rejected :as rejected])
(rejected/rejected? {:resilience/rejected :timeout}) ;; => true
(rejected/reason {:resilience/rejected :circuit-open}) ;; => :circuit-open
(rejected/reject :custom-reason) ;; => {:resilience/rejected :custom-reason};; Circuit Breaker + Retry
(def breaker (cb/create {:failure-threshold 5 :reset-timeout-ms 10000}))
(def resilient-call
(-> http-get
(retry/wrap {:max-attempts 3 :backoff-ms 500})
(->> (cb/wrap breaker))))
(resilient-call "https://api.example.com")
;; retry is applied first; if it still fails, circuit breaker counts the failure
;; Timeout + Retry + Circuit Breaker
(def resilient-call
(-> http-get
(timeout/wrap {:timeout-ms 3000})
(retry/wrap {:max-attempts 3 :backoff-ms 500 :retry? rejected/rejected?})
(->> (cb/wrap breaker))))- Plain data (no defrecord)
- Function in, function out (higher-order function pattern)
- Each pattern is independently usable
- Composition via
compor->feels natural - State is managed explicitly with controllable lifecycle
MIT