Skip to content

Commit 37bfaee

Browse files
committed
feat: add context binding parameter for execute handlers
1 parent 03d997a commit 37bfaee

7 files changed

Lines changed: 578 additions & 146 deletions

File tree

README.md

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,22 @@ For detailed information on each policy's behavior and configuration options, se
2929
```clj
3030
(require '[failsage.core :as fs])
3131

32-
;; Create a retry policy
32+
;; Simplest form - no executor needed
33+
(fs/execute
34+
(call-unreliable-service))
35+
36+
;; With a retry policy
3337
(def retry-policy
3438
(fs/retry {:max-retries 3
3539
:delay-ms 100
3640
:backoff-delay-factor 2.0}))
3741

38-
;; Create an executor with the policy
39-
(def executor (fs/executor retry-policy))
42+
;; Pass policy directly - no need to create executor
43+
(fs/execute retry-policy
44+
(call-unreliable-service))
4045

41-
;; Execute with automatic retry on failure
46+
;; Or create an executor explicitly
47+
(def executor (fs/executor retry-policy))
4248
(fs/execute executor
4349
(call-unreliable-service))
4450
```
@@ -54,9 +60,8 @@ For detailed information on each policy's behavior and configuration options, se
5460
:backoff-max-delay-ms 5000
5561
:backoff-delay-factor 2.0}))
5662

57-
(def executor (fs/executor retry-policy))
58-
59-
(fs/execute executor
63+
;; Pass policy directly
64+
(fs/execute retry-policy
6065
(http/get "https://api.example.com/data"))
6166
```
6267

@@ -69,9 +74,8 @@ For detailed information on each policy's behavior and configuration options, se
6974
:on-open-fn (fn [e] (log/warn "Circuit breaker opened!"))
7075
:on-close-fn (fn [e] (log/info "Circuit breaker closed!"))}))
7176

72-
(def executor (fs/executor circuit-breaker))
73-
74-
(fs/execute executor
77+
;; Pass policy directly
78+
(fs/execute circuit-breaker
7579
(call-flaky-service))
7680
```
7781

@@ -82,10 +86,8 @@ For detailed information on each policy's behavior and configuration options, se
8286
(fs/fallback {:result {:status :degraded :data []}
8387
:handle-exception Exception}))
8488

85-
(def executor (fs/executor fallback-policy))
86-
8789
;; Returns fallback value on any exception
88-
(fs/execute executor
90+
(fs/execute fallback-policy
8991
(fetch-user-data user-id))
9092
;; => {:status :degraded :data []}
9193
```
@@ -97,9 +99,8 @@ For detailed information on each policy's behavior and configuration options, se
9799
(fs/timeout {:timeout-ms 5000 ;; 5 second timeout
98100
:interrupt true})) ;; Interrupt thread on timeout
99101

100-
(def executor (fs/executor timeout-policy))
101-
102-
(fs/execute executor
102+
;; Pass policy directly
103+
(fs/execute timeout-policy
103104
(slow-database-query))
104105
```
105106

@@ -112,9 +113,8 @@ For detailed information on each policy's behavior and configuration options, se
112113
:period-ms 1000
113114
:burst true}))
114115

115-
(def executor (fs/executor rate-limiter))
116-
117-
(fs/execute executor
116+
;; Pass policy directly
117+
(fs/execute rate-limiter
118118
(process-request request))
119119
```
120120

@@ -126,9 +126,8 @@ For detailed information on each policy's behavior and configuration options, se
126126
(fs/bulkhead {:max-concurrency 10
127127
:max-wait-time-ms 1000})) ;; Wait up to 1 second for permit
128128

129-
(def executor (fs/executor bulkhead-policy))
130-
131-
(fs/execute executor
129+
;; Pass policy directly
130+
(fs/execute bulkhead-policy
132131
(process-task task))
133132
```
134133

@@ -183,16 +182,54 @@ Failsage integrates with [futurama](https://github.com/k13labs/futurama) for asy
183182
(require '[failsage.core :as fs])
184183
(require '[futurama.core :as f])
185184

185+
;; Simplest form - uses default thread pool
186+
(fs/execute-async
187+
(f/async
188+
(let [result (f/<!< (async-http-call))]
189+
(process-result result))))
190+
191+
;; With a policy
186192
(def retry-policy (fs/retry {:max-retries 3}))
187-
(def executor (fs/executor :io retry-policy)) ;; Use :io thread pool
193+
(fs/execute-async retry-policy
194+
(f/async
195+
(let [result (f/<!< (async-http-call))]
196+
(process-result result))))
188197

189-
;; Returns immediately, executes asynchronously
198+
;; With explicit executor and thread pool
199+
(def executor (fs/executor :io retry-policy)) ;; Use :io thread pool
190200
(fs/execute-async executor
191201
(f/async
192202
(let [result (f/<!< (async-http-call))]
193203
(process-result result))))
194204
```
195205

206+
## Accessing Execution Context
207+
208+
You can access execution context information (like attempt count, start time, etc.) by providing a context binding:
209+
210+
```clj
211+
;; Synchronous execution with context
212+
(def retry-policy (fs/retry {:max-retries 3}))
213+
214+
(fs/execute retry-policy ctx
215+
(let [attempt (.getAttemptCount ctx)
216+
start-time (.getStartTime ctx)]
217+
(log/info "Attempt" attempt "started at" start-time)
218+
(call-service)))
219+
220+
;; Asynchronous execution with context
221+
(fs/execute-async retry-policy ctx
222+
(f/async
223+
(log/info "Async attempt" (.getAttemptCount ctx))
224+
(f/<!< (async-call-service))))
225+
```
226+
227+
The context object provides access to:
228+
- `.getAttemptCount` - Current attempt number (0-indexed)
229+
- `.getStartTime` - When execution started
230+
- `.getElapsedTime` - Time elapsed since execution started
231+
- And more - see [ExecutionContext](https://failsafe.dev/javadoc/core/dev/failsafe/ExecutionContext.html) / [AsyncExecution](https://failsafe.dev/javadoc/core/dev/failsafe/AsyncExecution.html) docs
232+
196233
## Event Callbacks
197234

198235
All policies support event callbacks for observability:
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
{}
1+
{:hooks {:analyze-call {failsage.core/execute
2+
hooks.failsage/analyze-execute-macro
3+
4+
failsage.core/execute-async
5+
hooks.failsage/analyze-execute-macro}}}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
(ns hooks.failsage
2+
(:require [clj-kondo.hooks-api :as api]))
3+
4+
(defn analyze-execute-macro
5+
"analyze execute macro to support optional context binding"
6+
[{:keys [:node]}]
7+
(let [execute-args (rest (:children node))
8+
[executor-or-policy context-binding body] (when (= 3 (count execute-args))
9+
execute-args)]
10+
(if context-binding
11+
{:node (api/list-node
12+
(list
13+
(api/token-node 'let)
14+
(api/vector-node
15+
(vector context-binding executor-or-policy))
16+
body))}
17+
{:node node})))

src/failsage/core.clj

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
(ns failsage.core
22
"Defines failsafe policies for handling failures, retries, etc."
33
(:require
4-
[failsage.impl :as impl])
4+
[failsage.impl :as impl]
5+
[futurama.core :refer [!<! async]])
56
(:import
67
[dev.failsafe
8+
AsyncExecution
79
Bulkhead
810
CircuitBreaker
9-
Failsafe
11+
ExecutionContext
1012
FailsafeExecutor
1113
Fallback
1214
RateLimiter
@@ -499,7 +501,7 @@
499501
builder)))
500502

501503
(defn executor
502-
"Creates a Failsafe instance with the provided policies.
504+
"Creates a FailsafeExecutor instance with the provided policies.
503505
504506
Docs: https://failsafe.dev/javadoc/core/dev/failsafe/FailsafeExecutor.html
505507
@@ -508,41 +510,53 @@
508510
If not provided, it will use the currently bound `futurama.core/*thread-pool*` or `:io` pool.
509511
- `:policies`, `:policy-args` (optional): The policy or policies to use with the FailsafeExecutor.
510512
If not provided, the executor will treat any exceptions as a failure without any additional error handling."
511-
([]
512-
(executor nil nil))
513-
([policies]
514-
(executor nil policies))
513+
(^FailsafeExecutor []
514+
(impl/->executor nil nil))
515+
(^FailsafeExecutor [policies]
516+
(impl/->executor nil policies))
515517
(^FailsafeExecutor [pool policies]
516-
(let [pool (impl/get-pool pool)
517-
policies (impl/get-policy-list policies)]
518-
(if (empty? policies)
519-
(.with (Failsafe/none) pool)
520-
(.with (Failsafe/with policies) pool)))))
518+
(impl/->executor pool policies)))
521519

522520
(defmacro execute
523-
"Executes the given function with the provided Failsafe executor.
521+
"Executes the given function with the provided Failsafe executor or policy.
524522
525523
Docs: https://failsafe.dev/javadoc/core/dev/failsafe/FailsafeExecutor.html
526524
527525
Parameters:
528-
- `executor`: The FailsafeExecutor to use for execution.
526+
- `executor-or-policy`: The FailsafeExecutor or Policies to use for execution.
527+
- `context-binding`: the binding to use for the ExecutionContext context.
529528
- `body`: the body of code to execute.
530529
531530
Returns the result or throws an exception."
532-
[executor & body]
533-
`(impl/execute-get ~executor (bound-fn []
534-
~@body)))
531+
([body]
532+
`(execute [] context# ~body))
533+
([executor-or-policy body]
534+
`(execute ~executor-or-policy context# ~body))
535+
([executor-or-policy context-binding body]
536+
`(impl/execute-get ~executor-or-policy
537+
(bound-fn [~(vary-meta context-binding assoc :tag ExecutionContext)]
538+
~body))))
535539

536540
(defmacro execute-async
537-
"Executes the given async function with the provided Failsafe executor.
541+
"Executes the given async function with the provided Failsafe executor or policy.
538542
539543
Docs: https://failsafe.dev/javadoc/core/dev/failsafe/FailsafeExecutor.html
540544
541545
Parameters:
542-
- `executor`: The FailsafeExecutor to use for execution.
546+
- `executor-or-policy`: The FailsafeExecutor or Policies to use for async execution.
547+
- `context-binding`: the binding to use for the AsyncExecution context.
543548
- `body`: the body of code to execute.
544549
545550
Returns a future representing the result or exception."
546-
[executor & body]
547-
`(impl/execute-get-async ~executor (bound-fn []
548-
~@body)))
551+
([body]
552+
`(execute-async [] context# ~body))
553+
([executor-or-policy body]
554+
`(execute-async ~executor-or-policy context# ~body))
555+
([executor-or-policy context-binding body]
556+
`(impl/execute-get-async ~executor-or-policy
557+
(bound-fn [~context-binding]
558+
(async
559+
(try
560+
(impl/record-async-success ~context-binding (!<! ~body))
561+
(catch Throwable ~'t
562+
(impl/record-async-failure ~context-binding ~'t))))))))

src/failsage/impl.clj

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
(ns failsage.impl
22
(:require
3-
[futurama.core :refer [async !<!] :as f])
3+
[futurama.core :as f])
44
(:import
55
[dev.failsafe
66
AsyncExecution
77
BulkheadBuilder
88
CircuitBreakerBuilder
9+
Failsafe
910
FailsafeExecutor
1011
FallbackBuilder
1112
Policy
@@ -17,7 +18,7 @@
1718
AsyncRunnable
1819
CheckedFunction
1920
CheckedPredicate
20-
CheckedSupplier]
21+
ContextualSupplier]
2122
[java.util List]
2223
[java.util.concurrent ExecutorService]))
2324

@@ -63,16 +64,6 @@
6364
^EventListener [f]
6465
(FailsafeEventListener. f))
6566

66-
(deftype FailsafeCheckedSupplier [f]
67-
CheckedSupplier
68-
(get [_]
69-
(f)))
70-
71-
(defn ->checked-supplier
72-
"Converts a Clojure function to a Failsafe CheckedSupplier."
73-
^CheckedSupplier [f]
74-
(FailsafeCheckedSupplier. f))
75-
7667
(deftype FailsafeCheckedFunction [f]
7768
CheckedFunction
7869
(apply [_ args]
@@ -103,6 +94,16 @@
10394
^AsyncRunnable [f]
10495
(FailsafeAsyncRunnable. f))
10596

97+
(deftype FailsafeContextualSupplier [f]
98+
ContextualSupplier
99+
(get [_ context]
100+
(f context)))
101+
102+
(defn ->contextual-supplier
103+
"Converts a Clojure function to a Failsafe ContextualSupplier."
104+
^ContextualSupplier [f]
105+
(FailsafeContextualSupplier. f))
106+
106107
(defn get-pool
107108
"Returns a Failsafe-compatible thread pool. If `pool` is a keyword, looks up the pool using `futurama.core/get-pool`.
108109
Otherwise, returns the provided pool, a default thread pool, or falls back to `futurama.core/get-pool :io`."
@@ -127,20 +128,41 @@
127128
:else (throw (ex-info "Invalid policy type" {:policy policy}))))
128129
vec))
129130

131+
(defn record-async-success
132+
"Records the result of an execution in the given ExecutionContext."
133+
[^AsyncExecution context ^Object result]
134+
(.recordResult context result))
135+
136+
(defn record-async-failure
137+
"Records the error of an execution in the given ExecutionContext."
138+
[^AsyncExecution context ^Throwable error]
139+
(.recordException context error))
140+
141+
(defn ->executor
142+
"Creates a FailsafeExecutor with the given thread pool and policies.
143+
If no policies are provided, uses Failsafe.none()
144+
If no pool is provided, uses a default thread pool."
145+
(^FailsafeExecutor []
146+
(->executor nil nil))
147+
(^FailsafeExecutor [executor-or-policies]
148+
(->executor nil executor-or-policies))
149+
(^FailsafeExecutor [pool executor-or-policies]
150+
(let [pool (get-pool pool)]
151+
(if (instance? FailsafeExecutor executor-or-policies)
152+
(.with ^FailsafeExecutor executor-or-policies pool)
153+
(let [policies (get-policy-list executor-or-policies)]
154+
(if (empty? policies)
155+
(.with (Failsafe/none) pool)
156+
(.with (Failsafe/with policies) pool)))))))
157+
130158
(defn execute-get
131159
"Executes the given CheckedSupplier using the Failsafe executor."
132-
[^FailsafeExecutor executor execute-fn]
133-
(.get executor (->checked-supplier execute-fn)))
160+
[executor-or-policies execute-fn]
161+
(.get (->executor executor-or-policies)
162+
(->contextual-supplier execute-fn)))
134163

135164
(defn execute-get-async
136165
"Executes the given CheckedSupplier using the Failsafe executor."
137-
[^FailsafeExecutor executor execute-fn]
138-
(.getAsyncExecution executor
139-
(->async-runnable
140-
(fn [^AsyncExecution execution]
141-
(async
142-
(try
143-
(let [result (!<! (execute-fn))]
144-
(.recordResult execution result))
145-
(catch Throwable t
146-
(.recordException execution t))))))))
166+
[executor-or-policies execute-fn]
167+
(.getAsyncExecution (->executor executor-or-policies)
168+
(->async-runnable execute-fn)))

0 commit comments

Comments
 (0)