diff --git a/.github/workflows/clojure.yml b/.github/workflows/clojure.yml
index 850e006..7d581d2 100644
--- a/.github/workflows/clojure.yml
+++ b/.github/workflows/clojure.yml
@@ -12,8 +12,9 @@ jobs:
test:
strategy:
matrix:
- java_version: [corretto-11,corretto-24]
- test_clojure_alias: [clojure-1.10, clojure-1.11, clojure-1.12]
+ java_version: [corretto-24]
+ test_clojure_alias: [clojure-1.12]
+ fail-fast: false
runs-on: ubuntu-latest
steps:
- name: Checkout code
@@ -53,8 +54,6 @@ jobs:
echo "GRAALVM_HOME=$(mise where java)" >> $GITHUB_ENV
echo "$(mise where java)/bin" >> $GITHUB_PATH
gu install native-image
- - name: Run tests
- run: make test
- name: Run clj-kondo linter
run: make lint
- name: Run build jar
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8273247..1152856 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
This is a history of changes to k13labs/failsage
+# 0.3.0 - 2025-10-23
+
+## Changes
+
+* Fixed a problem due to limitations with AsyncExecution that was preventing the async task from being canceled.
+* Upgrade futurama dependency to 1.4.0 to get latest fixes and improvements.
+* Simplify test matrix, although older Clojure versions should work fine.
+
# 0.2.0 - 2025-10-22
### Features
diff --git a/deps.edn b/deps.edn
index fcc676d..84fb240 100644
--- a/deps.edn
+++ b/deps.edn
@@ -1,12 +1,10 @@
{:paths ["src" "resources"]
:deps {org.clojure/clojure {:mvn/version "1.12.3"}
dev.failsafe/failsafe {:mvn/version "3.3.2"}
- com.github.k13labs/futurama {:mvn/version "1.3.1"}}
+ com.github.k13labs/futurama {:mvn/version "1.4.0"}}
:aliases
- {:clojure-1.10 {:extra-deps {org.clojure/clojure {:mvn/version "1.10.3"}}}
- :clojure-1.11 {:extra-deps {org.clojure/clojure {:mvn/version "1.11.4"}}}
- :clojure-1.12 {:extra-deps {org.clojure/clojure {:mvn/version "1.12.3"}}}
+ {:clojure-1.12 {:extra-deps {org.clojure/clojure {:mvn/version "1.12.3"}}}
:dev {:extra-paths ["dev"]
:extra-deps {reloaded.repl/reloaded.repl {:mvn/version "0.2.4"}
diff --git a/pom.xml b/pom.xml
index aea0507..d7b303e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,9 +5,9 @@
com.github.k13labs
failsage
failsage
- 0.2.0
+ 0.3.0
- 0.2.0
+ 0.3.0
https://github.com/k13labs/failsage
scm:git:git://github.com/k13labs/failsage.git
scm:git:ssh://git@github.com/k13labs/failsage.git
@@ -32,7 +32,7 @@
com.github.k13labs
futurama
- 1.3.1
+ 1.4.0
diff --git a/src/failsage/core.clj b/src/failsage/core.clj
index 4d85eca..21f4e59 100644
--- a/src/failsage/core.clj
+++ b/src/failsage/core.clj
@@ -2,11 +2,10 @@
"Defines failsafe policies for handling failures, retries, etc."
(:require
[failsage.impl :as impl]
- [futurama.core :refer [! (Timeout/builder (Duration/ofMillis (long timeout-ms)))
interrupt
(.withInterrupt)
diff --git a/src/failsage/impl.clj b/src/failsage/impl.clj
index 9372fae..aa71387 100644
--- a/src/failsage/impl.clj
+++ b/src/failsage/impl.clj
@@ -2,17 +2,25 @@
(:require
[futurama.core :as f])
(:import
- [dev.failsafe
- AsyncExecution
- FailsafeExecutor]
+ [dev.failsafe ExecutionContext FailsafeExecutor]
[dev.failsafe.event EventListener]
[dev.failsafe.function
- AsyncRunnable
CheckedFunction
CheckedPredicate
+ CheckedRunnable
ContextualSupplier]
[java.util.concurrent ExecutorService]))
+(deftype FailsafeCheckedRunnable [f]
+ CheckedRunnable
+ (run [_]
+ (f)))
+
+(defn ->checked-runnable
+ "Converts a Clojure function to a Failsafe CheckedRunnable."
+ ^CheckedRunnable [f]
+ (FailsafeCheckedRunnable. f))
+
(deftype FailsafeEventListener [f]
EventListener
(accept [_ event]
@@ -45,16 +53,6 @@
^CheckedPredicate [f]
(FailsafeCheckedPredicate. f))
-(deftype FailsafeAsyncRunnable [f]
- AsyncRunnable
- (run [_ execution]
- (f execution)))
-
-(defn ->async-runnable
- "Converts a Clojure function to a Failsafe AsyncRunnable."
- ^AsyncRunnable [f]
- (FailsafeAsyncRunnable. f))
-
(deftype FailsafeContextualSupplier [f]
ContextualSupplier
(get [_ context]
@@ -73,16 +71,6 @@
(f/get-pool pool)
(or pool f/*thread-pool* (f/get-pool :io))))
-(defn record-async-success
- "Records the result of an execution in the given ExecutionContext."
- [^AsyncExecution context ^Object result]
- (.recordResult context result))
-
-(defn record-async-failure
- "Records the error of an execution in the given ExecutionContext."
- [^AsyncExecution context ^Throwable error]
- (.recordException context error))
-
(defn execute-get
"Executes the given CheckedSupplier using the Failsafe executor."
[^FailsafeExecutor executor execute-fn]
@@ -91,7 +79,7 @@
(defn execute-get-async
"Executes the given CheckedSupplier using the Failsafe executor."
[^FailsafeExecutor executor execute-fn]
- (.getAsyncExecution executor (->async-runnable execute-fn)))
+ (.getAsync executor (->contextual-supplier execute-fn)))
(defn- check-stateful-map!
"Checks if a single policy is a stateful map and throws if so."
@@ -103,6 +91,13 @@
:policy-type (:type policy)
:stateful-types #{:circuit-breaker :rate-limiter :bulkhead}}))))
+(defn on-cancel-propagate!
+ "Creates a CheckedRunnable that invokes the given handler function when called."
+ [^ExecutionContext context async-result]
+ (.onCancel context (->checked-runnable
+ (fn []
+ (f/async-cancel! async-result)))))
+
(defn validate-not-stateful-map!
"Validates that executor-or-policy is not a stateful policy map.
Returns the input unchanged if valid, throws if invalid."
diff --git a/test/failsage/core_test.clj b/test/failsage/core_test.clj
index f9a7502..121b787 100644
--- a/test/failsage/core_test.clj
+++ b/test/failsage/core_test.clj
@@ -18,7 +18,8 @@
Timeout
TimeoutExceededException]
[java.lang ArithmeticException IllegalArgumentException]
- [java.time Duration]))
+ [java.time Duration]
+ [java.util.concurrent ExecutionException]))
;; Dynamic var for testing
(def ^:dynamic *test-var* nil)
@@ -468,11 +469,34 @@
(is (instance? Timeout policy)))))
(deftest test-timeout-enforces-limit
- (testing "timeout enforces time limit"
+ (testing "timeout enforces time limit - execute"
(let [policy (fs/timeout {:timeout-ms 100})
- executor (fs/executor policy)]
- (is (thrown? TimeoutExceededException
- (fs/execute executor (Thread/sleep 200)))))))
+ executor (fs/executor policy)
+ start-time (System/currentTimeMillis)
+ result (try
+ (fs/execute executor
+ (do
+ (Thread/sleep 10000)
+ ::done)) ;;; is interrupted by timeout
+ (catch TimeoutExceededException e
+ e))
+ end-time (System/currentTimeMillis)]
+ (is (instance? TimeoutExceededException result))
+ (is (< (- end-time start-time) 1000)))) ;;; should timeout before 200ms sleep completes
+ (testing "timeout enforces time limit - execute-async"
+ (let [policy (fs/timeout {:timeout-ms 100})
+ executor (fs/executor policy)
+ start-time (System/currentTimeMillis)
+ result (try
+ @(fs/execute-async executor
+ (future
+ (Thread/sleep 10000)
+ ::done))
+ (catch ExecutionException e
+ (.getCause e)))
+ end-time (System/currentTimeMillis)]
+ (is (instance? TimeoutExceededException result))
+ (is (< (- end-time start-time) 1000))))) ;;; should timeout before 200ms sleep completes
(deftest test-timeout-success-callback
(testing "timeout calls on-success-fn"
diff --git a/test/failsage/impl_test.clj b/test/failsage/impl_test.clj
index e27d7e9..efecffa 100644
--- a/test/failsage/impl_test.clj
+++ b/test/failsage/impl_test.clj
@@ -105,27 +105,6 @@
(let [pred (impl/->checked-predicate (fn [_] (throw (ex-info "test error" {}))))]
(is (thrown? Exception (.test pred nil))))))
-;; AsyncRunnable Converter Tests
-
-(deftest test-async-runnable-creation
- (testing "->async-runnable creates a valid AsyncRunnable"
- (let [called (atom false)
- runnable (impl/->async-runnable (fn [_] (reset! called true)))]
- (is (some? runnable))
- (.run runnable nil)
- (is @called))))
-
-(deftest test-async-runnable-receives-execution
- (testing "AsyncRunnable receives execution context"
- (let [called (atom false)
- received-arg (atom nil)
- runnable (impl/->async-runnable (fn [exec]
- (reset! called true)
- (reset! received-arg exec)))]
- (.run runnable nil)
- (is @called)
- (is (nil? @received-arg)))))
-
;; Thread Pool Tests
(deftest test-get-pool-with-keyword
@@ -191,27 +170,6 @@
(is (= "success" result))
(is (= 2 @counter)))))
-;; record-async-success and record-async-failure Tests
-
-(deftest test-record-async-success
- (testing "record-async-success records result on AsyncExecution"
- (let [recorded-result (atom nil)
- mock-execution (reify dev.failsafe.AsyncExecution
- (recordResult [_ result]
- (reset! recorded-result result)))]
- (impl/record-async-success mock-execution "success-value")
- (is (= "success-value" @recorded-result)))))
-
-(deftest test-record-async-failure
- (testing "record-async-failure records exception on AsyncExecution"
- (let [recorded-exception (atom nil)
- mock-execution (reify dev.failsafe.AsyncExecution
- (recordException [_ exception]
- (reset! recorded-exception exception)))
- test-error (ex-info "test error" {})]
- (impl/record-async-failure mock-execution test-error)
- (is (= test-error @recorded-exception)))))
-
;; ContextualSupplier with ExecutionContext Tests
(deftest test-contextual-supplier-receives-context
@@ -267,21 +225,16 @@
(testing "execute-get-async executes an async function"
(let [executor (.with (Failsafe/none) ^ExecutorService (f/get-pool :io))
result (impl/execute-get-async executor
- (fn [ctx]
- (f/async
- (impl/record-async-success ctx 42))))]
+ (fn [_]
+ 42))]
(is (= 42 @result)))))
(deftest test-execute-get-async-with-exception
(testing "execute-get-async handles exceptions"
(let [executor (.with (Failsafe/none) ^ExecutorService (f/get-pool :io))
result (impl/execute-get-async executor
- (fn [ctx]
- (f/async
- (try
- (throw (ex-info "async error" {}))
- (catch Throwable t
- (impl/record-async-failure ctx t))))))]
+ (fn [_]
+ (throw (ex-info "async error" {}))))]
(is (thrown-with-msg? Exception #"async error"
@result)))))
@@ -292,8 +245,7 @@
result (impl/execute-get-async executor
(fn [ctx]
(reset! received-context ctx)
- (f/async
- (impl/record-async-success ctx 42))))]
+ 42))]
(is (= 42 @result))
(is (some? @received-context))
- (is (instance? dev.failsafe.AsyncExecution @received-context)))))
+ (is (instance? ExecutionContext @received-context)))))