Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/clojure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ jobs:
test:
strategy:
matrix:
java_version: [corretto-11,corretto-21]
java_version: [corretto-11,corretto-24]
test_clojure_alias: [clojure-1.10, clojure-1.11, clojure-1.12]
test_core_async_alias: [core.async-1.6, core.async-1.7, core.async-1.8]
runs-on: ubuntu-latest
steps:
- name: Checkout code
Expand All @@ -35,8 +34,7 @@ jobs:
- name: Run tests
env:
TEST_CLOJURE_ALIAS: ${{matrix.test_clojure_alias}}
TEST_CORE_ASYNC_ALIAS: ${{matrix.test_core_async_alias}}
run: make test TEST_CLOJURE_ALIAS=$TEST_CLOJURE_ALIAS TEST_CORE_ASYNC_ALIAS=$TEST_CORE_ASYNC_ALIAS
run: make test TEST_CLOJURE_ALIAS=$TEST_CLOJURE_ALIAS
build:
runs-on: ubuntu-latest
steps:
Expand Down
1 change: 1 addition & 0 deletions .mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
clojure = "https://github.com/asdf-community/asdf-clojure.git"

[tools]
java = "graalvm-22.3.1+java11"
clojure = "1.12"
15 changes: 13 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
This is a history of changes to k13labs/failsage

# 0.0.1-SNAPSHOT - Unreleased
* ...
# 0.1.0 - 2025-10-21

Initial release providing idiomatic Clojure wrappers for [Failsafe.dev](https://failsafe.dev/).

### Features

* Policy builders: `retry`, `circuit-breaker`, `fallback`, `timeout`, `rate-limiter`, `bulkhead`
* Synchronous execution via `execute`
* Asynchronous execution via `execute-async` with futurama integration
* Policy composition support
* Event callbacks for observability
* Comprehensive test coverage across Clojure 1.10, 1.11, 1.12
* GraalVM native image support
9 changes: 2 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
.PHONY: repl test clean deploy install format-check format-fix

TEST_CLOJURE_ALIAS ?= clojure-1.12
TEST_CORE_ASYNC_ALIAS ?= core.async-1.8

REPL_CLOJURE_ALIAS ?= clojure-1.12
REPL_CORE_ASYNC_ALIAS ?= core.async-1.8

SHELL := /bin/bash
JAVA_OPTS ?= -Dfailsage.executor-factory=clojure.core.async.impl.dispatch/executor-for

export _JAVA_OPTIONS := $(JAVA_OPTS)

env:
env

repl:
clojure -M:$(REPL_CLOJURE_ALIAS):$(REPL_CORE_ASYNC_ALIAS):dev:test:app:repl
clojure -M:$(REPL_CLOJURE_ALIAS):dev:test:app:repl

test:
clojure -M:$(TEST_CLOJURE_ALIAS):$(TEST_CORE_ASYNC_ALIAS):dev:test:app:runner \
clojure -M:$(TEST_CLOJURE_ALIAS):dev:test:app:runner \
--focus :unit --reporter kaocha.report/documentation --no-capture-output

clean:
Expand Down
277 changes: 274 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,287 @@

# _About_

Failsage is a Clojure library looking to provide a translation layer to use Failsafe.dev from Clojure in a way that is compatible and consistent for sync and async code via Clojure [core.async](https://github.com/clojure/core.async).
Failsage is a Clojure library that provides an idiomatic translation layer for [Failsafe.dev](https://failsafe.dev/), making it easier to use Failsafe's battle-tested resilience patterns from Clojure.

## Rationale

Failsage is designed for simplicity and ergonomics. You can pass policies directly to execute functions without creating executors, use optional context bindings only when you need execution state, and compose multiple policies through simple vectors. Rather than hiding context in dynamic variables or nesting macros, failsage makes state explicit and easy to test. It provides first-class async integration with [futurama](https://github.com/k13labs/futurama) using the same straightforward API. The result is a library where simple cases require minimal code and complex scenarios remain clear and composable.

Note: You can also use Failsafe directly via Java interop or through alternatives like [diehard](https://github.com/sunng87/diehard).

## Features

Rather than reinventing the resilience patterns, Failsage wisely wraps Failsafe with:

- **Idiomatic Clojure API**: Keyword arguments, immutable data structures, and functional style
- **Async Support**: Seamless async execution integration via [futurama](https://github.com/k13labs/futurama), including channels/futures/promises/deferreds.
- **Unified Interface**: Consistent API for both synchronous and asynchronous code paths

Failsage supports all core Failsafe policies:
- **Retry**: Automatic retry with configurable backoff strategies
- **Circuit Breaker**: Prevent cascading failures by temporarily blocking requests
- **Fallback**: Graceful degradation with alternative results
- **Timeout**: Time-bound execution with cancellation support
- **Rate Limiter**: Control execution rate to prevent system overload
- **Bulkhead**: Limit concurrent executions to isolate resources

For detailed information on each policy's behavior and configuration options, see the [Failsafe documentation](https://failsafe.dev/).

# _Usage_

Here's a simple example.
## Basic Example

```clj
(require '[failsage.core :as fs])

;; Simplest form - no executor needed
(fs/execute
(call-unreliable-service))

;; With a retry policy
(def retry-policy
(fs/retry {:max-retries 3
:delay-ms 100
:backoff-delay-factor 2.0}))

;; Pass policy directly - no need to create executor
(fs/execute retry-policy
(call-unreliable-service))

;; Or create an executor explicitly
(def executor (fs/executor retry-policy))
(fs/execute executor
(call-unreliable-service))
```

## Synchronous Execution

### Retry with Exponential Backoff

```clj
(def retry-policy
(fs/retry {:max-retries 3
:backoff-delay-ms 100
:backoff-max-delay-ms 5000
:backoff-delay-factor 2.0}))

;; Pass policy directly
(fs/execute retry-policy
(http/get "https://api.example.com/data"))
```

### Circuit Breaker

```clj
(def circuit-breaker
(fs/circuit-breaker {:delay-ms 60000 ;; Wait 1 minute before half-open
:failure-threshold 5 ;; Open after 5 consecutive failures
:on-open-fn (fn [e] (log/warn "Circuit breaker opened!"))
:on-close-fn (fn [e] (log/info "Circuit breaker closed!"))}))

;; Pass policy directly
(fs/execute circuit-breaker
(call-flaky-service))
```

See the existing tests for more examples.
### Fallback

```clj
(def fallback-policy
(fs/fallback {:result {:status :degraded :data []}
:handle-exception Exception}))

;; Returns fallback value on any exception
(fs/execute fallback-policy
(fetch-user-data user-id))
;; => {:status :degraded :data []}
```

### Timeout

```clj
(def timeout-policy
(fs/timeout {:timeout-ms 5000 ;; 5 second timeout
:interrupt true})) ;; Interrupt thread on timeout

;; Pass policy directly
(fs/execute timeout-policy
(slow-database-query))
```

### Rate Limiting

```clj
;; Allow 100 requests per second
(def rate-limiter
(fs/rate-limiter {:max-executions 100
:period-ms 1000
:burst true}))

;; Pass policy directly
(fs/execute rate-limiter
(process-request request))
```

### Bulkhead

```clj
;; Limit to 10 concurrent executions
(def bulkhead-policy
(fs/bulkhead {:max-concurrency 10
:max-wait-time-ms 1000})) ;; Wait up to 1 second for permit

;; Pass policy directly
(fs/execute bulkhead-policy
(process-task task))
```

## Policy Composition

Policies can be composed together. They are applied in order from innermost to outermost:

```clj
;; Compose retry, circuit breaker, and fallback
;; Execution order: retry -> circuit breaker -> fallback
(def retry-policy (fs/retry {:max-retries 3}))
(def cb-policy (fs/circuit-breaker {:failure-threshold 5 :delay-ms 60000}))
(def fallback-policy (fs/fallback {:result :fallback-value}))

;; Policies compose: fallback wraps circuit-breaker wraps retry
(def executor (fs/executor [fallback-policy cb-policy retry-policy]))

(fs/execute executor
(unreliable-operation))
;; - First tries the operation
;; - Retries up to 3 times on failure
;; - Circuit breaker tracks failures
;; - If everything fails, fallback returns :fallback-value
```

### Common Composition Patterns

**Timeout + Retry**: Prevent long waits while retrying
```clj
(def timeout-policy (fs/timeout {:timeout-ms 2000}))
(def retry-policy (fs/retry {:max-retries 3 :delay-ms 100}))

;; Timeout applies to EACH retry attempt
(def executor (fs/executor [retry-policy timeout-policy]))
```

**Bulkhead + Circuit Breaker + Fallback**: Complete resilience stack
```clj
(def bulkhead (fs/bulkhead {:max-concurrency 20}))
(def cb (fs/circuit-breaker {:failure-threshold 10 :delay-ms 30000}))
(def fallback (fs/fallback {:result {:status :degraded}}))

;; Limit concurrency, break on failures, degrade gracefully
(def executor (fs/executor [fallback cb bulkhead]))
```

## Asynchronous Execution

Failsage integrates with [futurama](https://github.com/k13labs/futurama) for async execution:

```clj
(require '[failsage.core :as fs])
(require '[futurama.core :as f])

;; Simplest form - uses default thread pool
(fs/execute-async
(f/async
(let [result (f/<!< (async-http-call))]
(process-result result))))

;; With a policy
(def retry-policy (fs/retry {:max-retries 3}))
(fs/execute-async retry-policy
(f/async
(let [result (f/<!< (async-http-call))]
(process-result result))))

;; With explicit executor and thread pool
(def executor (fs/executor :io retry-policy)) ;; Use :io thread pool
(fs/execute-async executor
(f/async
(let [result (f/<!< (async-http-call))]
(process-result result))))
```

## Accessing Execution Context

You can access execution context information (like attempt count, start time, etc.) by providing a context binding:

```clj
;; Synchronous execution with context
(def retry-policy (fs/retry {:max-retries 3}))

(fs/execute retry-policy ctx
(let [attempt (.getAttemptCount ctx)
start-time (.getStartTime ctx)]
(log/info "Attempt" attempt "started at" start-time)
(call-service)))

;; Asynchronous execution with context
(fs/execute-async retry-policy ctx
(f/async
(log/info "Async attempt" (.getAttemptCount ctx))
(f/<!< (async-call-service))))
```

The context object provides access to:
- `.getAttemptCount` - Current attempt number (0-indexed)
- `.getStartTime` - When execution started
- `.getElapsedTime` - Time elapsed since execution started
- And more - see [ExecutionContext](https://failsafe.dev/javadoc/core/dev/failsafe/ExecutionContext.html) / [AsyncExecution](https://failsafe.dev/javadoc/core/dev/failsafe/AsyncExecution.html) docs

## Event Callbacks

All policies support event callbacks for observability:

```clj
(def retry-policy
(fs/retry {:max-retries 3
:on-retry-fn (fn [event]
(log/info "Retrying after failure"
{:attempt (.getAttemptCount event)
:exception (.getLastException event)}))
:on-success-fn (fn [event]
(log/debug "Execution succeeded"))
:on-failure-fn (fn [event]
(log/error "Execution failed after retries"))}))
```

## Dynamic Behavior

Handle specific exceptions or results:

```clj
;; Retry only on specific exceptions
(def retry-policy
(fs/retry {:max-retries 3
:handle-exception [java.net.SocketTimeoutException
java.io.IOException]}))

;; Retry based on result
(def retry-policy
(fs/retry {:max-retries 5
:handle-result-fn (fn [result]
(and (map? result)
(= (:status result) :retry)))}))

;; Abort retry on specific exceptions
(def retry-policy
(fs/retry {:max-retries 10
:abort-on-exception [IllegalArgumentException
SecurityException]}))
```

## More Examples

See the [test suite](test/failsage/core_test.clj) for comprehensive examples of all policies and configuration options.

For detailed documentation on policy behavior, configuration, and advanced patterns, refer to the [Failsafe documentation](https://failsafe.dev/).

# _Building_

Expand Down
8 changes: 6 additions & 2 deletions app/failsage/main.clj
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
(ns failsage.main
(:require
[failsage.core :as fsg])
[failsage.core :as fs])
(:gen-class))

(defn -main
"this is a test to compile some failsage code into native code"
[& args]
(println "welcome to failsage"))
(let [policy (fs/fallback {:result {:status :degraded :data []}
:handle-exception Exception})
executor (fs/executor policy)]
(println "welcome to failsage, result:"
(fs/execute executor (throw (ex-info "simulated failure" {}))))))
Loading