From c546895d094d66341b37d13017c2adb10ecab24c Mon Sep 17 00:00:00 2001 From: Jose Gomez Date: Wed, 22 Oct 2025 10:53:11 -0500 Subject: [PATCH] feat: add the ability to build policies from a map with a :type --- .cljfmt.edn | 6 + CHANGELOG.md | 18 + README.md | 134 ++++ pom.xml | 4 +- .../failsage/hooks/failsage.clj_kondo | 6 +- src/failsage/core.clj | 240 ++++--- src/failsage/impl.clj | 106 +-- test/failsage/core_test.clj | 614 +++++++++++++++--- test/failsage/impl_test.clj | 167 +---- 9 files changed, 865 insertions(+), 430 deletions(-) create mode 100644 .cljfmt.edn diff --git a/.cljfmt.edn b/.cljfmt.edn new file mode 100644 index 0000000..9a6a915 --- /dev/null +++ b/.cljfmt.edn @@ -0,0 +1,6 @@ +{:legacy/merge-indents? true + :indents {cond [[:inner 0]] + futurama.core/async [[:inner 0]] + futurama.core/thread [[:inner 0]] + failsage.core/execute [[:inner 0]] + failsage.core/execute-async [[:inner 0]]}} diff --git a/CHANGELOG.md b/CHANGELOG.md index 661c235..8273247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ This is a history of changes to k13labs/failsage +# 0.2.0 - 2025-10-22 + +### Features + +* **Map-based Policy Building**: Policies can now be defined using plain Clojure maps with a `:type` key to support policies as data. + * All policy types support map format: `:retry`, `:circuit-breaker`, `:fallback`, `:timeout`, `:rate-limiter`, `:bulkhead` + * Maps can be used directly with `execute`, `execute-async`, and `executor` functions + * Policy maps are automatically built via the `IPolicyBuilder` protocol. + * Mix map-based and function-based policies seamlessly in policy composition + * Example: `(fs/execute {:type :retry :max-retries 3} (call-service))` + * Stateful policy maps (circuit breaker, rate limiter, bulkhead) throw exceptions when used directly to prevent state loss +* Added `policies` function to explicitly create policy lists from regular policies, maps, or mixed combinations + +### Changes + +* Removed some redundant functions from the `failsage.impl` namespace, and moved the `IPolicyBuilder protocol to the `failsage.core` namespace. +* Extended `IPolicyBuilder` protocol to support `IPersistentMap` interface for automatic policy building + # 0.1.0 - 2025-10-21 Initial release providing idiomatic Clojure wrappers for [Failsafe.dev](https://failsafe.dev/). diff --git a/README.md b/README.md index b2e78e8..dd1425e 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Rather than reinventing the resilience patterns, Failsage wisely wraps Failsafe - **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 +- **Data-Driven Policies**: Define policies as plain Clojure maps for configuration-driven setups Failsage supports all core Failsafe policies: - **Retry**: Automatic retry with configurable backoff strategies @@ -137,6 +138,98 @@ For detailed information on each policy's behavior and configuration options, se (process-task task)) ``` +## Policy Maps + +Policies can also be defined using plain Clojure maps with a `:type` key. This is useful for configuration-driven setups or when you want to serialize/deserialize policy definitions: + +```clj +;; Define policies as maps +(def retry-map {:type :retry + :max-retries 3 + :delay-ms 100}) + +(def circuit-breaker-map {:type :circuit-breaker + :delay-ms 60000 + :failure-threshold 5}) + +(def fallback-map {:type :fallback + :result {:status :degraded}}) + +;; IMPORTANT: Stateful policies (circuit breakers, rate limiters, bulkheads) maintain +;; state across executions. Using their maps directly with execute/execute-async will +;; throw an exception to prevent accidentally losing state. + +;; ❌ THROWS EXCEPTION - stateful policy map used directly +(fs/execute circuit-breaker-map (api-call-1)) +;; => ExceptionInfo: Cannot execute with stateful policy map directly + +;; ✅ CORRECT - create executor once, reuse it +(def executor (fs/executor circuit-breaker-map)) +(fs/execute executor (api-call-1)) +(fs/execute executor (api-call-2)) + +;; ✅ ALSO CORRECT - use policies function +(def policy-list (fs/policies circuit-breaker-map)) +(fs/execute policy-list (api-call-1)) +(fs/execute policy-list (api-call-2)) + +;; Stateless policies (retry, timeout, fallback) can be used directly as maps +(fs/execute retry-map + (call-unreliable-service)) +``` + +All policy types support the map format: + +```clj +;; Retry +{:type :retry :max-retries 3 :backoff-delay-ms 100 :backoff-max-delay-ms 5000} + +;; Circuit Breaker +{:type :circuit-breaker :delay-ms 60000 :failure-threshold 5} + +;; Fallback +{:type :fallback :result "default-value"} +{:type :fallback :result-fn (fn [event] "computed-value")} + +;; Timeout +{:type :timeout :timeout-ms 5000 :interrupt true} + +;; Rate Limiter +{:type :rate-limiter :max-executions 100 :period-ms 1000 :burst true} + +;; Bulkhead +{:type :bulkhead :max-concurrency 10 :max-wait-time-ms 1000} +``` + +Policy maps work seamlessly with all execution modes: + +```clj +;; With executor (executor is created once and can be reused) +(def executor (fs/executor {:type :retry :max-retries 3})) +(fs/execute executor (operation-1)) +(fs/execute executor (operation-2)) + +;; Direct with execute (only works for stateless policies like fallback, retry, timeout) +(fs/execute {:type :fallback :result :default} + (risky-operation)) + +;; With execute-async (only works for stateless policies) +(fs/execute-async {:type :retry :max-retries 3} + (f/async (async-operation))) + +;; Stateful policy maps throw exceptions when used directly +;; (fs/execute {:type :circuit-breaker :failure-threshold 5} (call)) +;; => ExceptionInfo: Cannot execute with stateful policy map directly + +;; Mix maps with regular policies +(def retry-map {:type :retry :max-retries 3}) +(def fallback-policy (fs/fallback {:result :default})) +;; Build executor once with both policies +(def mixed-executor (fs/executor [fallback-policy retry-map])) +(fs/execute mixed-executor (operation-1)) +(fs/execute mixed-executor (operation-2)) +``` + ## Policy Composition Policies can be composed together. They are applied in order from innermost to outermost: @@ -180,6 +273,47 @@ Policies can be composed together. They are applied in order from innermost to o (def executor (fs/executor [fallback cb bulkhead])) ``` +**Configuration-driven with Policy Maps**: Load policies from config +```clj +;; Load from config file/env +(def policy-config + [{:type :fallback :result {:status :degraded}} + {:type :circuit-breaker :failure-threshold 10 :delay-ms 30000} + {:type :retry :max-retries 3 :delay-ms 100}]) + +;; Build executor once from map config - policies are built automatically +(def executor (fs/executor policy-config)) + +;; Reuse the executor for multiple calls - circuit breaker state is maintained +(fs/execute executor (api-call-1)) +(fs/execute executor (api-call-2)) +(fs/execute executor (api-call-3)) +``` + +**Creating Reusable Policy Lists**: Use `policies` to build policy lists independently +```clj +;; Create a reusable policy list - policies are built from maps +;; Note: the policies function builds all policies immediately +(def standard-policies + (fs/policies + (fs/fallback {:result {:status :degraded}}) + {:type :circuit-breaker :failure-threshold 5 :delay-ms 30000} + (fs/retry {:max-retries 3}))) + +;; Create executors with the same policy list +;; Each executor gets the SAME policy instances (e.g., same circuit breaker) +(def executor-1 (fs/executor standard-policies)) +(def executor-2 (fs/executor :io standard-policies)) + +;; Both executors share the same circuit breaker state +(fs/execute executor-1 (api-call)) +(fs/execute executor-2 (api-call)) + +;; Or use directly with execute (creates a new executor each time) +;; Note: this still reuses the same policy instances from standard-policies +(fs/execute standard-policies (api-call)) +``` + ## Asynchronous Execution Failsage integrates with [futurama](https://github.com/k13labs/futurama) for async execution: diff --git a/pom.xml b/pom.xml index 494a99d..aea0507 100644 --- a/pom.xml +++ b/pom.xml @@ -5,9 +5,9 @@ com.github.k13labs failsage failsage - 0.1.0 + 0.2.0 - 0.1.0 + 0.2.0 https://github.com/k13labs/failsage scm:git:git://github.com/k13labs/failsage.git scm:git:ssh://git@github.com/k13labs/failsage.git diff --git a/resources/clj-kondo.exports/failsage/failsage/hooks/failsage.clj_kondo b/resources/clj-kondo.exports/failsage/failsage/hooks/failsage.clj_kondo index 4554220..f6a4981 100644 --- a/resources/clj-kondo.exports/failsage/failsage/hooks/failsage.clj_kondo +++ b/resources/clj-kondo.exports/failsage/failsage/hooks/failsage.clj_kondo @@ -5,11 +5,11 @@ "analyze execute macro to support optional context binding" [{:keys [:node]}] (let [execute-args (rest (:children node)) - [executor-or-policy context-binding body] (when (= 3 (count execute-args)) - execute-args)] + [executor-or-policy context-binding & body] (when (>= (count execute-args) 3) + execute-args)] (if context-binding {:node (api/list-node - (list + (list* (api/token-node 'let) (api/vector-node (vector context-binding executor-or-policy)) diff --git a/src/failsage/core.clj b/src/failsage/core.clj index 6771426..4d85eca 100644 --- a/src/failsage/core.clj +++ b/src/failsage/core.clj @@ -4,30 +4,122 @@ [failsage.impl :as impl] [futurama.core :refer [!> (for [policy (flatten policy-args) + :when (some? policy)] + (cond + (instance? Policy policy) + policy + + (satisfies? IPolicyBuilder policy) + (build-policy policy) + + :else (throw (ex-info "Invalid policy type" {:policy policy})))) + vec)) + +(defn executor + "Creates a FailsafeExecutor instance with the provided policies. + + Docs: https://failsafe.dev/javadoc/core/dev/failsafe/FailsafeExecutor.html + + Options: + - `:pool` (optional): An executor service to use for async executions. + If not provided, it will use the currently bound `futurama.core/*thread-pool*` or `:io` pool. + - `:policies` (optional): The policy or policies to use with the FailsafeExecutor. + If not provided, the executor will treat any exceptions as a failure without any additional error handling." + (^FailsafeExecutor [] + (executor nil nil)) + (^FailsafeExecutor [executor-or-policies] + (executor nil executor-or-policies)) + (^FailsafeExecutor [pool executor-or-policies] + (let [pool (impl/get-pool pool)] + (if (instance? FailsafeExecutor executor-or-policies) + (.with ^FailsafeExecutor executor-or-policies pool) + (let [policies (policies executor-or-policies)] + (if (empty? policies) + (.with (Failsafe/none) pool) + (.with (Failsafe/with policies) pool))))))) + +(defmacro execute + "Executes the given function with the provided Failsafe executor or policy. + + Docs: https://failsafe.dev/javadoc/core/dev/failsafe/FailsafeExecutor.html + + Parameters: + - `executor-or-policy`: The FailsafeExecutor or Policies to use for execution. + - `context-binding`: the binding to use for the ExecutionContext context. + - `body`: the body of code to execute. + + Returns the result or throws an exception." + ([body] + `(execute [] context# ~body)) + ([executor-or-policy body] + `(execute ~executor-or-policy context# ~body)) + ([executor-or-policy context-binding & body] + `(impl/execute-get (executor (impl/validate-not-stateful-map! ~executor-or-policy)) + (bound-fn [~(vary-meta context-binding assoc :tag ExecutionContext)] + ~@body)))) -(defn build-policy - "Builds and returns the Failsafe Policy instance from a builder." - [builder] - (impl/build-policy builder)) +(defmacro execute-async + "Executes the given async function with the provided Failsafe executor or policy. + + Docs: https://failsafe.dev/javadoc/core/dev/failsafe/FailsafeExecutor.html + + Parameters: + - `executor-or-policy`: The FailsafeExecutor or Policies to use for async execution. + - `context-binding`: the binding to use for the AsyncExecution context. + - `body`: the body of code to execute. + + Returns a future representing the result or exception." + ([body] + `(execute-async [] context# ~body)) + ([executor-or-policy body] + `(execute-async ~executor-or-policy context# ~body)) + ([executor-or-policy context-binding & body] + `(impl/execute-get-async (executor (impl/validate-not-stateful-map! ~executor-or-policy)) + (bound-fn [~(vary-meta context-binding assoc :tag AsyncExecution)] + (async + (try + (impl/record-async-success ~context-binding (!executor nil nil)) - (^FailsafeExecutor [policies] - (impl/->executor nil policies)) - (^FailsafeExecutor [pool policies] - (impl/->executor pool policies))) - -(defmacro execute - "Executes the given function with the provided Failsafe executor or policy. - - Docs: https://failsafe.dev/javadoc/core/dev/failsafe/FailsafeExecutor.html - - Parameters: - - `executor-or-policy`: The FailsafeExecutor or Policies to use for execution. - - `context-binding`: the binding to use for the ExecutionContext context. - - `body`: the body of code to execute. - - Returns the result or throws an exception." - ([body] - `(execute [] context# ~body)) - ([executor-or-policy body] - `(execute ~executor-or-policy context# ~body)) - ([executor-or-policy context-binding body] - `(impl/execute-get ~executor-or-policy - (bound-fn [~(vary-meta context-binding assoc :tag ExecutionContext)] - ~body)))) - -(defmacro execute-async - "Executes the given async function with the provided Failsafe executor or policy. - - Docs: https://failsafe.dev/javadoc/core/dev/failsafe/FailsafeExecutor.html - - Parameters: - - `executor-or-policy`: The FailsafeExecutor or Policies to use for async execution. - - `context-binding`: the binding to use for the AsyncExecution context. - - `body`: the body of code to execute. - - Returns a future representing the result or exception." - ([body] - `(execute-async [] context# ~body)) - ([executor-or-policy body] - `(execute-async ~executor-or-policy context# ~body)) - ([executor-or-policy context-binding body] - `(impl/execute-get-async ~executor-or-policy - (bound-fn [~context-binding] - (async - (try - (impl/record-async-success ~context-binding (!> (for [policy (flatten policy-args) - :when (some? policy)] - (cond - (instance? Policy policy) - policy - - (satisfies? IPolicyBuilder policy) - (build-policy policy) - - :else (throw (ex-info "Invalid policy type" {:policy policy})))) - vec)) - (defn record-async-success "Records the result of an execution in the given ExecutionContext." [^AsyncExecution context ^Object result] @@ -138,31 +83,32 @@ [^AsyncExecution context ^Throwable error] (.recordException context error)) -(defn ->executor - "Creates a FailsafeExecutor with the given thread pool and policies. - If no policies are provided, uses Failsafe.none() - If no pool is provided, uses a default thread pool." - (^FailsafeExecutor [] - (->executor nil nil)) - (^FailsafeExecutor [executor-or-policies] - (->executor nil executor-or-policies)) - (^FailsafeExecutor [pool executor-or-policies] - (let [pool (get-pool pool)] - (if (instance? FailsafeExecutor executor-or-policies) - (.with ^FailsafeExecutor executor-or-policies pool) - (let [policies (get-policy-list executor-or-policies)] - (if (empty? policies) - (.with (Failsafe/none) pool) - (.with (Failsafe/with policies) pool))))))) - (defn execute-get "Executes the given CheckedSupplier using the Failsafe executor." - [executor-or-policies execute-fn] - (.get (->executor executor-or-policies) - (->contextual-supplier execute-fn))) + [^FailsafeExecutor executor execute-fn] + (.get executor (->contextual-supplier execute-fn))) (defn execute-get-async "Executes the given CheckedSupplier using the Failsafe executor." - [executor-or-policies execute-fn] - (.getAsyncExecution (->executor executor-or-policies) - (->async-runnable execute-fn))) + [^FailsafeExecutor executor execute-fn] + (.getAsyncExecution executor (->async-runnable execute-fn))) + +(defn- check-stateful-map! + "Checks if a single policy is a stateful map and throws if so." + [policy] + (when (and (map? policy) + (#{:circuit-breaker :rate-limiter :bulkhead} (:type policy))) + (throw (ex-info "Cannot execute with stateful policy map directly" + {:policy-map policy + :policy-type (:type policy) + :stateful-types #{:circuit-breaker :rate-limiter :bulkhead}})))) + +(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." + [executor-or-policy] + (if (sequential? executor-or-policy) + (doseq [policy (flatten executor-or-policy)] + (check-stateful-map! policy)) + (check-stateful-map! executor-or-policy)) + executor-or-policy) diff --git a/test/failsage/core_test.clj b/test/failsage/core_test.clj index 03fcaf6..f9a7502 100644 --- a/test/failsage/core_test.clj +++ b/test/failsage/core_test.clj @@ -17,11 +17,59 @@ RetryPolicy Timeout TimeoutExceededException] - [java.lang ArithmeticException IllegalArgumentException])) + [java.lang ArithmeticException IllegalArgumentException] + [java.time Duration])) ;; Dynamic var for testing (def ^:dynamic *test-var* nil) +;; IPolicyBuilder Protocol Tests + +(deftest test-build-policy-bulkhead-builder + (testing "build-policy builds a Bulkhead from BulkheadBuilder" + (let [builder (-> (Bulkhead/builder 5) + (.withMaxWaitTime (Duration/ofMillis 100))) + policy (fs/build-policy builder)] + (is (instance? Bulkhead policy)) + (is (instance? Policy policy))))) + +(deftest test-build-policy-circuit-breaker-builder + (testing "build-policy builds a CircuitBreaker from CircuitBreakerBuilder" + (let [builder (-> (CircuitBreaker/builder) + (.withDelay (Duration/ofMillis 100))) + policy (fs/build-policy builder)] + (is (instance? CircuitBreaker policy)) + (is (instance? Policy policy))))) + +(deftest test-build-policy-fallback-builder + (testing "build-policy builds a Fallback from FallbackBuilder" + (let [builder (Fallback/builder "default-value") + policy (fs/build-policy builder)] + (is (instance? Fallback policy)) + (is (instance? Policy policy))))) + +(deftest test-build-policy-rate-limiter-builder + (testing "build-policy builds a RateLimiter from RateLimiterBuilder" + (let [builder (RateLimiter/smoothBuilder 10 (Duration/ofSeconds 1)) + policy (fs/build-policy builder)] + (is (instance? RateLimiter policy)) + (is (instance? Policy policy))))) + +(deftest test-build-policy-retry-policy-builder + (testing "build-policy builds a RetryPolicy from RetryPolicyBuilder" + (let [builder (-> (RetryPolicy/builder) + (.withMaxRetries 3)) + policy (fs/build-policy builder)] + (is (instance? RetryPolicy policy)) + (is (instance? Policy policy))))) + +(deftest test-build-policy-timeout-builder + (testing "build-policy builds a Timeout from TimeoutBuilder" + (let [builder (Timeout/builder (Duration/ofSeconds 5)) + policy (fs/build-policy builder)] + (is (instance? Timeout policy)) + (is (instance? Policy policy))))) + ;; Bulkhead Tests (deftest test-bulkhead-creation @@ -298,11 +346,11 @@ counter (atom 0)] (try (fs/execute executor - (do - (swap! counter inc) - (when (<= @counter 2) - (throw (ex-info "retry" {}))) - "success")) + (do + (swap! counter inc) + (when (<= @counter 2) + (throw (ex-info "retry" {}))) + "success")) (catch Exception _)) ;; Should have tried 3 times (initial + 2 retries) (is (= 3 @counter))))) @@ -337,9 +385,9 @@ counter (atom 0)] (try (fs/execute executor - (do - (swap! counter inc) - (throw (IllegalArgumentException. "abort")))) + (do + (swap! counter inc) + (throw (IllegalArgumentException. "abort")))) (catch IllegalArgumentException _)) ;; Should only try once, no retries (is (= 1 @counter))))) @@ -350,9 +398,9 @@ executor (fs/executor policy) counter (atom 0) result (fs/execute executor - (do - (swap! counter inc) - :abort))] + (do + (swap! counter inc) + :abort))] (is (= :abort result)) ;; Should only try once, no retries (is (= 1 @counter))))) @@ -372,11 +420,11 @@ executor (fs/executor policy) counter (atom 0) result (fs/execute executor - (do - (swap! counter inc) - (if (< @counter 2) - :retry - :success)))] + (do + (swap! counter inc) + (if (< @counter 2) + :retry + :success)))] (is (= :success result)) (is (= 2 @counter))))) @@ -392,11 +440,11 @@ executor (fs/executor policy) counter (atom 0)] (fs/execute executor - (do - (swap! counter inc) - (when (< @counter 2) - (throw (ex-info "retry" {}))) - "success")) + (do + (swap! counter inc) + (when (< @counter 2) + (throw (ex-info "retry" {}))) + "success")) (is (= 1 @failed-attempt-count)) (is (= 1 @retry-count)) (is @success-called)))) @@ -435,6 +483,67 @@ (fs/execute executor "success") (is @success-called)))) +;; Policies Tests + +(deftest test-policies-creation + (testing "policies function creates a policy list" + (testing "with single policy" + (let [policy (fs/retry {:max-retries 3}) + policy-list (fs/policies policy)] + (is (instance? java.util.List policy-list)) + (is (= 1 (count policy-list))) + (is (instance? RetryPolicy (first policy-list))))) + + (testing "with multiple policies" + (let [retry-policy (fs/retry {:max-retries 3}) + timeout-policy (fs/timeout {:timeout-ms 1000}) + policy-list (fs/policies retry-policy timeout-policy)] + (is (instance? java.util.List policy-list)) + (is (= 2 (count policy-list))) + (is (instance? RetryPolicy (first policy-list))) + (is (instance? Timeout (second policy-list))))) + + (testing "with map-based policies" + (let [retry-map {:type :retry :max-retries 3} + fallback-map {:type :fallback :result "default"} + policy-list (fs/policies retry-map fallback-map)] + (is (instance? java.util.List policy-list)) + (is (= 2 (count policy-list))) + (is (instance? RetryPolicy (first policy-list))) + (is (instance? Fallback (second policy-list))))) + + (testing "with mixed policy types" + (let [retry-policy (fs/retry {:max-retries 3}) + fallback-map {:type :fallback :result "default"} + policy-list (fs/policies retry-policy fallback-map)] + (is (instance? java.util.List policy-list)) + (is (= 2 (count policy-list))) + (is (instance? RetryPolicy (first policy-list))) + (is (instance? Fallback (second policy-list))))) + + (testing "with nested collections" + (let [retry-policy (fs/retry {:max-retries 3}) + timeout-policy (fs/timeout {:timeout-ms 1000}) + policy-list (fs/policies [retry-policy timeout-policy])] + (is (instance? java.util.List policy-list)) + (is (= 2 (count policy-list))))) + + (testing "with nil values filtered out" + (let [retry-policy (fs/retry {:max-retries 3}) + policy-list (fs/policies retry-policy nil)] + (is (instance? java.util.List policy-list)) + (is (= 1 (count policy-list))) + (is (instance? RetryPolicy (first policy-list))))) + + (testing "empty list with no policies" + (let [policy-list (fs/policies)] + (is (instance? java.util.List policy-list)) + (is (empty? policy-list)))) + + (testing "throws exception for invalid policy type" + (is (thrown? Exception + (fs/policies "invalid-policy")))))) + ;; Executor Tests (deftest test-executor-creation-no-policies @@ -479,9 +588,9 @@ (let [executor (fs/executor) counter (atom 0) result (fs/execute executor - (do - (swap! counter inc) - @counter))] + (do + (swap! counter inc) + @counter))] (is (= 1 result)) (is (= 1 @counter))))) @@ -497,11 +606,11 @@ executor (fs/executor policy) counter (atom 0)] (fs/execute executor - (do - (swap! counter inc) - (when (< @counter 2) - (throw (ex-info "retry" {}))) - "success")) + (do + (swap! counter inc) + (when (< @counter 2) + (throw (ex-info "retry" {}))) + "success")) (is (= 2 @counter))))) (deftest test-execute-with-fallback-policy @@ -520,9 +629,9 @@ executor (fs/executor [fallback-policy retry-policy]) counter (atom 0) result (fs/execute executor - (do - (swap! counter inc) - (throw (ex-info "fail" {}))))] + (do + (swap! counter inc) + (throw (ex-info "fail" {}))))] (is (= "fallback" result)) ;; Should have tried 3 times (initial + 2 retries) (is (= 3 @counter))))) @@ -562,9 +671,9 @@ (let [executor (fs/executor) received-context (atom nil) result (fs/execute executor ctx - (do - (reset! received-context ctx) - 42))] + (do + (reset! received-context ctx) + 42))] (is (= 42 result)) (is (some? @received-context)) (is (instance? dev.failsafe.ExecutionContext @received-context))))) @@ -576,12 +685,12 @@ counter (atom 0) attempt-counts (atom [])] (fs/execute executor ctx - (do - (swap! attempt-counts conj (.getAttemptCount ^ExecutionContext ctx)) - (swap! counter inc) - (when (< @counter 2) - (throw (ex-info "retry" {}))) - "success")) + (do + (swap! attempt-counts conj (.getAttemptCount ^ExecutionContext ctx)) + (swap! counter inc) + (when (< @counter 2) + (throw (ex-info "retry" {}))) + "success")) (is (= [0 1] @attempt-counts))))) (deftest test-execute-context-has-start-time @@ -589,9 +698,9 @@ (let [executor (fs/executor) start-time (atom nil)] (fs/execute executor ctx - (do - (reset! start-time (.getStartTime ^ExecutionContext ctx)) - 42)) + (do + (reset! start-time (.getStartTime ^ExecutionContext ctx)) + 42)) (is (some? @start-time))))) ;; Execute with Policy (not Executor) Tests @@ -601,11 +710,11 @@ (let [policy (fs/retry {:max-retries 2}) counter (atom 0) result (fs/execute policy - (do - (swap! counter inc) - (when (< @counter 2) - (throw (ex-info "retry" {}))) - "success"))] + (do + (swap! counter inc) + (when (< @counter 2) + (throw (ex-info "retry" {}))) + "success"))] (is (= "success" result)) (is (= 2 @counter))))) @@ -615,9 +724,9 @@ fallback-policy (fs/fallback {:result "fallback"}) counter (atom 0) result (fs/execute [fallback-policy retry-policy] - (do - (swap! counter inc) - (throw (ex-info "fail" {}))))] + (do + (swap! counter inc) + (throw (ex-info "fail" {}))))] (is (= "fallback" result)) (is (= 3 @counter))))) @@ -626,11 +735,11 @@ (let [policy (fs/retry {:max-retries 1}) attempt-counts (atom [])] (fs/execute policy ctx - (do - (swap! attempt-counts conj (.getAttemptCount ^ExecutionContext ctx)) - (when (= 1 (count @attempt-counts)) - (throw (ex-info "retry" {}))) - "success")) + (do + (swap! attempt-counts conj (.getAttemptCount ^ExecutionContext ctx)) + (when (= 1 (count @attempt-counts)) + (throw (ex-info "retry" {}))) + "success")) (is (= [0 1] @attempt-counts))))) ;; Execute Async Tests @@ -640,7 +749,7 @@ (f/with-pool :io (let [executor (fs/executor :io nil) result (fs/execute-async executor - (f/async 42))] + (f/async 42))] (is (= 42 @result)))))) (deftest test-execute-async-with-retry @@ -650,11 +759,11 @@ executor (fs/executor :io policy) counter (atom 0) result (fs/execute-async executor - (f/async - (swap! counter inc) - (when (< @counter 2) - (throw (ex-info "retry" {}))) - "success"))] + (f/async + (swap! counter inc) + (when (< @counter 2) + (throw (ex-info "retry" {}))) + "success"))] (is (= "success" @result)) (is (>= @counter 2)))))) @@ -664,7 +773,7 @@ (let [executor (fs/executor :io nil)] (binding [*test-var* "async-bound-value"] (let [result (fs/execute-async executor - (f/async *test-var*))] + (f/async *test-var*))] (is (= "async-bound-value" @result)))))))) ;; Execute Async with 1-arity (no executor) Tests @@ -680,8 +789,8 @@ (f/with-pool :io (let [counter (atom 0) result (fs/execute-async (f/async - (swap! counter inc) - @counter))] + (swap! counter inc) + @counter))] (is (= 1 @result)) (is (= 1 @counter)))))) @@ -689,7 +798,7 @@ (testing "execute-async with no executor propagates exceptions" (f/with-pool :io (let [future-result (fs/execute-async (f/async - (throw (ex-info "async error" {}))))] + (throw (ex-info "async error" {}))))] ;; Exception is thrown when dereferencing the future (is (thrown-with-msg? Exception #"async error" @future-result)))))) @@ -702,9 +811,9 @@ (let [executor (fs/executor :io nil) received-context (atom nil) result (fs/execute-async executor ctx - (f/async - (reset! received-context ctx) - 42))] + (f/async + (reset! received-context ctx) + 42))] (is (= 42 @result)) (is (some? @received-context)) (is (instance? dev.failsafe.AsyncExecution @received-context)))))) @@ -717,12 +826,12 @@ counter (atom 0) attempt-counts (atom []) result (fs/execute-async executor ctx - (f/async - (swap! attempt-counts conj (.getAttemptCount ^ExecutionContext ctx)) - (swap! counter inc) - (when (< @counter 2) - (throw (ex-info "retry" {}))) - "success"))] + (f/async + (swap! attempt-counts conj (.getAttemptCount ^ExecutionContext ctx)) + (swap! counter inc) + (when (< @counter 2) + (throw (ex-info "retry" {}))) + "success"))] (is (= "success" @result)) (is (= [0 1] @attempt-counts)))))) @@ -731,9 +840,9 @@ (f/with-pool :io (let [executor (fs/executor :io nil) result (fs/execute-async executor ctx - (f/async + (f/async ;; The macro handles recording, but we can verify context methods exist - (.getAttemptCount ^ExecutionContext ctx)))] + (.getAttemptCount ^ExecutionContext ctx)))] (is (= 0 @result)))))) ;; Execute Async with Policy (not Executor) Tests @@ -744,11 +853,11 @@ (let [policy (fs/retry {:max-retries 2}) counter (atom 0) result (fs/execute-async policy - (f/async - (swap! counter inc) - (when (< @counter 2) - (throw (ex-info "retry" {}))) - "success"))] + (f/async + (swap! counter inc) + (when (< @counter 2) + (throw (ex-info "retry" {}))) + "success"))] (is (= "success" @result)) (is (= 2 @counter)))))) @@ -759,9 +868,9 @@ fallback-policy (fs/fallback {:result "fallback"}) counter (atom 0) result (fs/execute-async [fallback-policy retry-policy] - (f/async - (swap! counter inc) - (throw (ex-info "fail" {}))))] + (f/async + (swap! counter inc) + (throw (ex-info "fail" {}))))] (is (= "fallback" @result)) (is (= 3 @counter)))))) @@ -771,11 +880,11 @@ (let [policy (fs/retry {:max-retries 1}) attempt-counts (atom []) result (fs/execute-async policy ctx - (f/async - (swap! attempt-counts conj (.getAttemptCount ^ExecutionContext ctx)) - (when (= 1 (count @attempt-counts)) - (throw (ex-info "retry" {}))) - "success"))] + (f/async + (swap! attempt-counts conj (.getAttemptCount ^ExecutionContext ctx)) + (when (= 1 (count @attempt-counts)) + (throw (ex-info "retry" {}))) + "success"))] (is (= "success" @result)) (is (= [0 1] @attempt-counts)))))) @@ -805,9 +914,9 @@ executor (fs/executor [timeout-policy retry-policy])] (is (thrown? TimeoutExceededException (fs/execute executor - (do - (Thread/sleep 50) - (throw (ex-info "retry" {}))))))))) + (do + (Thread/sleep 50) + (throw (ex-info "retry" {}))))))))) (deftest test-integration-bulkhead-with-retry (testing "bulkhead with retry policy" @@ -816,10 +925,315 @@ executor (fs/executor [bulkhead-policy retry-policy]) counter (atom 0) result (fs/execute executor - (do - (swap! counter inc) - (when (< @counter 2) - (throw (ex-info "retry" {}))) - "success"))] + (do + (swap! counter inc) + (when (< @counter 2) + (throw (ex-info "retry" {}))) + "success"))] (is (= "success" result)) (is (= 2 @counter))))) + +;; Map-based Policy Building Tests + +(deftest test-map-based-policy-building + (testing "Map-based policy building with :type key" + + (testing "bulkhead policy creation" + (testing "basic bulkhead from map" + (let [policy (fs/build-policy {:type :bulkhead :max-concurrency 5})] + (is (instance? Bulkhead policy)) + (is (instance? Policy policy)))) + + (testing "bulkhead with additional options" + (let [success-called (atom false) + policy (fs/build-policy {:type :bulkhead + :max-concurrency 3 + :max-wait-time-ms 100 + :on-success-fn (fn [_] (reset! success-called true))}) + executor (fs/executor policy)] + (is (instance? Bulkhead policy)) + (fs/execute executor "success") + (is @success-called)))) + + (testing "circuit-breaker policy creation" + (testing "basic circuit-breaker from map" + (let [policy (fs/build-policy {:type :circuit-breaker + :delay-ms 1000 + :failure-threshold 3})] + (is (instance? CircuitBreaker policy)) + (is (instance? Policy policy)))) + + (testing "circuit-breaker with state callbacks" + (let [opened (atom false) + policy (fs/build-policy {:type :circuit-breaker + :delay-ms 100 + :failure-threshold 2 + :on-open-fn (fn [_] (reset! opened true))}) + executor (fs/executor policy)] + (is (instance? CircuitBreaker policy)) + (try (fs/execute executor (throw (ex-info "fail1" {}))) (catch Exception _)) + (try (fs/execute executor (throw (ex-info "fail2" {}))) (catch Exception _)) + (is @opened)))) + + (testing "fallback policy creation" + (testing "basic fallback with static result" + (let [policy (fs/build-policy {:type :fallback :result "default"})] + (is (instance? Fallback policy)) + (is (instance? Policy policy)))) + + (testing "fallback with result function" + (let [policy (fs/build-policy {:type :fallback + :result-fn (fn [_] "computed")}) + executor (fs/executor policy) + result (fs/execute executor (throw (ex-info "fail" {})))] + (is (instance? Fallback policy)) + (is (= "computed" result))))) + + (testing "rate-limiter policy creation" + (testing "basic smooth rate-limiter from map" + (let [policy (fs/build-policy {:type :rate-limiter + :max-executions 10 + :period-ms 1000})] + (is (instance? RateLimiter policy)) + (is (instance? Policy policy)))) + + (testing "bursty rate-limiter enforces limits" + (let [policy (fs/build-policy {:type :rate-limiter + :max-executions 2 + :period-ms 10000 + :burst true + :max-wait-time-ms 0}) + executor (fs/executor policy)] + (is (instance? RateLimiter policy)) + (is (= 1 (fs/execute executor 1))) + (is (= 2 (fs/execute executor 2))) + (is (thrown? RateLimitExceededException + (fs/execute executor 3)))))) + + (testing "retry policy creation" + (testing "basic retry from map" + (let [policy (fs/build-policy {:type :retry :max-retries 3})] + (is (instance? RetryPolicy policy)) + (is (instance? Policy policy)))) + + (testing "retry with backoff options" + (let [policy (fs/build-policy {:type :retry + :max-retries 3 + :backoff-delay-ms 10 + :backoff-max-delay-ms 100}) + executor (fs/executor policy) + counter (atom 0)] + (is (instance? RetryPolicy policy)) + (fs/execute executor + (do + (swap! counter inc) + (when (< @counter 2) + (throw (ex-info "retry" {}))) + "success")) + (is (= 2 @counter))))) + + (testing "timeout policy creation" + (testing "basic timeout from map" + (let [policy (fs/build-policy {:type :timeout :timeout-ms 1000})] + (is (instance? Timeout policy)) + (is (instance? Policy policy)))) + + (testing "timeout enforces time limit" + (let [policy (fs/build-policy {:type :timeout + :timeout-ms 100 + :interrupt true}) + executor (fs/executor policy)] + (is (instance? Timeout policy)) + (is (thrown? TimeoutExceededException + (fs/execute executor (Thread/sleep 200))))))) + + (testing "error handling" + (testing "unknown policy type throws exception" + (is (thrown? IllegalArgumentException + (fs/build-policy {:type :unknown-policy}))))) + + (testing "usage with executor" + (testing "single map-based policy with executor" + (let [policy-map {:type :retry :max-retries 2} + executor (fs/executor policy-map) + counter (atom 0) + result (fs/execute executor + (do + (swap! counter inc) + (when (< @counter 2) + (throw (ex-info "retry" {}))) + "success"))] + (is (= "success" result)) + (is (= 2 @counter)))) + + (testing "map-based policy directly with execute" + (let [policy-map {:type :fallback :result "fallback-value"} + result (fs/execute policy-map (throw (ex-info "fail" {})))] + (is (= "fallback-value" result)))) + + (testing "multiple map-based policies" + (let [retry-map {:type :retry :max-retries 2} + fallback-map {:type :fallback :result "fallback"} + executor (fs/executor [fallback-map retry-map]) + counter (atom 0) + result (fs/execute executor + (do + (swap! counter inc) + (throw (ex-info "fail" {}))))] + (is (= "fallback" result)) + (is (= 3 @counter)))) + + (testing "mixed map and regular policies" + (let [retry-map {:type :retry :max-retries 2} + fallback-policy (fs/fallback {:result "fallback"}) + executor (fs/executor [fallback-policy retry-map]) + counter (atom 0) + result (fs/execute executor + (do + (swap! counter inc) + (throw (ex-info "fail" {}))))] + (is (= "fallback" result)) + (is (= 3 @counter))))) + + (testing "integration scenarios" + (testing "circuit breaker with fallback" + (let [cb-map {:type :circuit-breaker :delay-ms 1000 :failure-threshold 2} + fb-map {:type :fallback :result "fallback"} + executor (fs/executor [fb-map cb-map])] + (is (= "fallback" (fs/execute executor (throw (ex-info "fail1" {}))))) + (is (= "fallback" (fs/execute executor (throw (ex-info "fail2" {}))))) + (is (= "fallback" (fs/execute executor "should not execute"))))) + + (testing "retry with timeout" + (let [retry-map {:type :retry :max-retries 5 :delay-ms 100} + timeout-map {:type :timeout :timeout-ms 250} + executor (fs/executor [timeout-map retry-map])] + (is (thrown? TimeoutExceededException + (fs/execute executor + (do + (Thread/sleep 50) + (throw (ex-info "retry" {}))))))))) + + (testing "async execution" + (testing "map-based policy with execute-async" + (f/with-pool :io + (let [retry-map {:type :retry :max-retries 2} + counter (atom 0) + result (fs/execute-async retry-map + (f/async + (swap! counter inc) + (when (< @counter 2) + (throw (ex-info "retry" {}))) + "success"))] + (is (= "success" @result)) + (is (>= @counter 2)))))) + + (testing "callback support" + (testing "all callback options work with map-based policies" + (let [success-called (atom false) + policy-map {:type :retry + :max-retries 2 + :on-success-fn (fn [_] (reset! success-called true))} + counter (atom 0)] + (fs/execute policy-map + (do + (swap! counter inc) + (when (< @counter 2) + (throw (ex-info "retry" {}))) + "success")) + (is (= 2 @counter)) + (is @success-called)))) + + (testing "build option behavior" + (testing "map-based policy always builds (build:false is overridden)" + (let [policy-map {:type :retry :max-retries 3 :build false} + policy (fs/build-policy policy-map)] + (is (instance? RetryPolicy policy)) + (is (instance? Policy policy))))))) + +;; Stateful Policy Validation Tests + +(deftest test-stateful-policy-map-validation + (testing "execute throws when using stateful policy maps directly" + (testing "circuit-breaker map throws" + (is (thrown-with-msg? + clojure.lang.ExceptionInfo + #"Cannot execute with stateful policy map directly" + (fs/execute {:type :circuit-breaker :failure-threshold 5} + "result")))) + + (testing "rate-limiter map throws" + (is (thrown-with-msg? + clojure.lang.ExceptionInfo + #"Cannot execute with stateful policy map directly" + (fs/execute {:type :rate-limiter :max-executions 10 :period-ms 1000} + "result")))) + + (testing "bulkhead map throws" + (is (thrown-with-msg? + clojure.lang.ExceptionInfo + #"Cannot execute with stateful policy map directly" + (fs/execute {:type :bulkhead :max-concurrency 5} + "result"))))) + + (testing "execute-async throws when using stateful policy maps directly" + (testing "circuit-breaker map throws" + (is (thrown-with-msg? + clojure.lang.ExceptionInfo + #"Cannot execute with stateful policy map directly" + (fs/execute-async {:type :circuit-breaker :failure-threshold 5} + (f/async "result"))))) + + (testing "rate-limiter map throws" + (is (thrown-with-msg? + clojure.lang.ExceptionInfo + #"Cannot execute with stateful policy map directly" + (fs/execute-async {:type :rate-limiter :max-executions 10 :period-ms 1000} + (f/async "result"))))) + + (testing "bulkhead map throws" + (is (thrown-with-msg? + clojure.lang.ExceptionInfo + #"Cannot execute with stateful policy map directly" + (fs/execute-async {:type :bulkhead :max-concurrency 5} + (f/async "result")))))) + + (testing "execute throws when stateful map is in list of policies" + (testing "list with circuit-breaker map throws" + (is (thrown-with-msg? + clojure.lang.ExceptionInfo + #"Cannot execute with stateful policy map directly" + (fs/execute [{:type :retry :max-retries 3} + {:type :circuit-breaker :failure-threshold 5}] + "result")))) + + (testing "nested list with bulkhead map throws" + (is (thrown-with-msg? + clojure.lang.ExceptionInfo + #"Cannot execute with stateful policy map directly" + (fs/execute [[{:type :fallback :result "default"}] + [{:type :bulkhead :max-concurrency 5}]] + "result"))))) + + (testing "stateless policy maps work correctly" + (testing "retry map works" + (is (= "result" (fs/execute {:type :retry :max-retries 3} "result")))) + + (testing "timeout map works" + (is (= "result" (fs/execute {:type :timeout :timeout-ms 1000} "result")))) + + (testing "fallback map works" + (is (= "result" (fs/execute {:type :fallback :result "default"} "result"))))) + + (testing "built policies and executors work correctly" + (testing "executor with stateful policy map works" + (let [executor (fs/executor {:type :circuit-breaker :failure-threshold 5})] + (is (= "result" (fs/execute executor "result"))))) + + (testing "policies function with stateful policy map works" + (let [policy-list (fs/policies {:type :circuit-breaker :failure-threshold 5})] + (is (= "result" (fs/execute policy-list "result"))))) + + (testing "explicit policy works" + (let [policy (fs/circuit-breaker {:failure-threshold 5})] + (is (= "result" (fs/execute policy "result"))))))) diff --git a/test/failsage/impl_test.clj b/test/failsage/impl_test.clj index ec0b889..e27d7e9 100644 --- a/test/failsage/impl_test.clj +++ b/test/failsage/impl_test.clj @@ -1,69 +1,16 @@ (ns failsage.impl-test (:require [clojure.test :refer [deftest is testing]] + [failsage.core] [failsage.impl :as impl] [futurama.core :as f]) (:import [dev.failsafe - Bulkhead - CircuitBreaker ExecutionContext Failsafe - Fallback - Policy - RateLimiter - RetryPolicy - Timeout] - [java.time Duration] + RetryPolicy] [java.util.concurrent ExecutorService])) -;; IPolicyBuilder Protocol Tests - -(deftest test-build-policy-bulkhead-builder - (testing "build-policy builds a Bulkhead from BulkheadBuilder" - (let [builder (-> (Bulkhead/builder 5) - (.withMaxWaitTime (Duration/ofMillis 100))) - policy (impl/build-policy builder)] - (is (instance? Bulkhead policy)) - (is (instance? Policy policy))))) - -(deftest test-build-policy-circuit-breaker-builder - (testing "build-policy builds a CircuitBreaker from CircuitBreakerBuilder" - (let [builder (-> (CircuitBreaker/builder) - (.withDelay (Duration/ofMillis 100))) - policy (impl/build-policy builder)] - (is (instance? CircuitBreaker policy)) - (is (instance? Policy policy))))) - -(deftest test-build-policy-fallback-builder - (testing "build-policy builds a Fallback from FallbackBuilder" - (let [builder (Fallback/builder "default-value") - policy (impl/build-policy builder)] - (is (instance? Fallback policy)) - (is (instance? Policy policy))))) - -(deftest test-build-policy-rate-limiter-builder - (testing "build-policy builds a RateLimiter from RateLimiterBuilder" - (let [builder (RateLimiter/smoothBuilder 10 (Duration/ofSeconds 1)) - policy (impl/build-policy builder)] - (is (instance? RateLimiter policy)) - (is (instance? Policy policy))))) - -(deftest test-build-policy-retry-policy-builder - (testing "build-policy builds a RetryPolicy from RetryPolicyBuilder" - (let [builder (-> (RetryPolicy/builder) - (.withMaxRetries 3)) - policy (impl/build-policy builder)] - (is (instance? RetryPolicy policy)) - (is (instance? Policy policy))))) - -(deftest test-build-policy-timeout-builder - (testing "build-policy builds a Timeout from TimeoutBuilder" - (let [builder (Timeout/builder (Duration/ofSeconds 5)) - policy (impl/build-policy builder)] - (is (instance? Timeout policy)) - (is (instance? Policy policy))))) - ;; EventListener Converter Tests (deftest test-event-listener-creation @@ -203,62 +150,6 @@ (let [pool (impl/get-pool nil)] (is (instance? ExecutorService pool)))))) -;; Policy List Tests - -(deftest test-get-policy-list-with-single-policy - (testing "get-policy-list handles a single policy" - (let [policy (.build (RetryPolicy/builder)) - result (impl/get-policy-list policy)] - (is (= 1 (count result))) - (is (instance? Policy (first result)))))) - -(deftest test-get-policy-list-with-multiple-policies - (testing "get-policy-list handles multiple policies" - (let [policy1 (.build (RetryPolicy/builder)) - policy2 (.build (Timeout/builder (Duration/ofSeconds 5))) - result (impl/get-policy-list policy1 policy2)] - (is (= 2 (count result))) - (is (every? #(instance? Policy %) result))))) - -(deftest test-get-policy-list-with-builder - (testing "get-policy-list builds policies from builders" - (let [builder (RetryPolicy/builder) - result (impl/get-policy-list builder)] - (is (= 1 (count result))) - (is (instance? RetryPolicy (first result)))))) - -(deftest test-get-policy-list-with-mixed-types - (testing "get-policy-list handles mixed policies and builders" - (let [policy (.build (RetryPolicy/builder)) - builder (Timeout/builder (Duration/ofSeconds 5)) - result (impl/get-policy-list policy builder)] - (is (= 2 (count result))) - (is (instance? RetryPolicy (first result))) - (is (instance? Timeout (second result)))))) - -(deftest test-get-policy-list-filters-nil - (testing "get-policy-list filters out nil values" - (let [policy (.build (RetryPolicy/builder)) - result (impl/get-policy-list policy nil nil)] - (is (= 1 (count result)))))) - -(deftest test-get-policy-list-flattens-nested - (testing "get-policy-list flattens nested collections" - (let [policy1 (.build (RetryPolicy/builder)) - policy2 (.build (Timeout/builder (Duration/ofSeconds 5))) - result (impl/get-policy-list [policy1 policy2])] - (is (= 2 (count result)))))) - -(deftest test-get-policy-list-empty - (testing "get-policy-list handles empty input" - (let [result (impl/get-policy-list)] - (is (empty? result))))) - -(deftest test-get-policy-list-invalid-type - (testing "get-policy-list throws on invalid policy type" - (is (thrown-with-msg? Exception #"Invalid policy type" - (impl/get-policy-list "not-a-policy"))))) - ;; Execute Tests (deftest test-execute-get-basic @@ -300,48 +191,6 @@ (is (= "success" result)) (is (= 2 @counter))))) -;; ->executor Tests - -(deftest test-executor-creation-no-args - (testing "->executor creates an executor with no arguments" - (let [executor (impl/->executor)] - (is (some? executor)) - (is (instance? dev.failsafe.FailsafeExecutor executor))))) - -(deftest test-executor-creation-with-policies - (testing "->executor creates an executor with policies" - (let [retry-policy (.build (RetryPolicy/builder)) - executor (impl/->executor [retry-policy])] - (is (some? executor)) - (is (instance? dev.failsafe.FailsafeExecutor executor))))) - -(deftest test-executor-creation-with-pool-and-policies - (testing "->executor creates an executor with pool and policies" - (let [retry-policy (.build (RetryPolicy/builder)) - executor (impl/->executor :io [retry-policy])] - (is (some? executor)) - (is (instance? dev.failsafe.FailsafeExecutor executor))))) - -(deftest test-executor-creation-from-executor - (testing "->executor accepts a FailsafeExecutor and returns a new one with pool" - (let [base-executor (Failsafe/none) - executor (impl/->executor :io base-executor)] - (is (some? executor)) - (is (instance? dev.failsafe.FailsafeExecutor executor))))) - -(deftest test-executor-creation-with-empty-policies - (testing "->executor with empty policies uses Failsafe.none" - (let [executor (impl/->executor [])] - (is (some? executor)) - (is (instance? dev.failsafe.FailsafeExecutor executor))))) - -(deftest test-executor-creation-with-single-policy - (testing "->executor with single policy (not in collection)" - (let [retry-policy (.build (RetryPolicy/builder)) - executor (impl/->executor retry-policy)] - (is (some? executor)) - (is (instance? dev.failsafe.FailsafeExecutor executor))))) - ;; record-async-success and record-async-failure Tests (deftest test-record-async-success @@ -420,7 +269,7 @@ result (impl/execute-get-async executor (fn [ctx] (f/async - (impl/record-async-success ctx 42))))] + (impl/record-async-success ctx 42))))] (is (= 42 @result))))) (deftest test-execute-get-async-with-exception @@ -429,10 +278,10 @@ 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))))))] + (try + (throw (ex-info "async error" {})) + (catch Throwable t + (impl/record-async-failure ctx t))))))] (is (thrown-with-msg? Exception #"async error" @result))))) @@ -444,7 +293,7 @@ (fn [ctx] (reset! received-context ctx) (f/async - (impl/record-async-success ctx 42))))] + (impl/record-async-success ctx 42))))] (is (= 42 @result)) (is (some? @received-context)) (is (instance? dev.failsafe.AsyncExecution @received-context)))))