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)))))