@@ -13,8 +13,7 @@ import (
1313// stops calling the wrapped function until it closes again, returning [ErrCircuitOpen] in the meantime.
1414//
1515// A zero Circuit will panic, analogous to calling a nil function variable. Initialize with [NewCircuit].
16- type Circuit [IN , OUT any ] struct {
17- f WrappedFunc [IN , OUT ]
16+ type Circuit struct {
1817 options
1918
2019 // State
@@ -64,8 +63,8 @@ func (f BreakerMiddlewareFunc) Wrap(of ObserverFactory) (ObserverFactory, error)
6463 return f (of )
6564}
6665
67- // WrappedFunc is the type of the function wrapped by a Breaker .
68- type WrappedFunc [IN , OUT any ] func (context.Context , IN ) (OUT , error )
66+ // WrappableFunc is the type of the function wrapped by a [Circuit] .
67+ type WrappableFunc [IN , OUT any ] func (context.Context , IN ) (OUT , error )
6968
7069// dedupObservableCall wraps an [Observer] ensuring it can only be observed a single time.
7170func dedupObservableCall (obs Observer ) Observer {
@@ -83,12 +82,10 @@ func (d *dedupedObserver) Observe(failure bool) {
8382 })
8483}
8584
86- // NewCircuit instantiates a new [Circuit] that wraps the provided function. See [Circuit.Call] for calling semantics.
87- // A Circuit with a nil breaker is a noop wrapper around the provided function and will never open.
88- func NewCircuit [IN , OUT any ](f WrappedFunc [IN , OUT ], breaker Breaker , opts ... Option ) (* Circuit [IN , OUT ], error ) {
89- c := & Circuit [IN , OUT ]{
90- f : f ,
91- }
85+ // NewCircuit instantiates a new [Circuit]. See [Wrap] for further usage.
86+ // A [Circuit] with a nil breaker is a noop and will never open for any of its wrapped functions.
87+ func NewCircuit (breaker Breaker , opts ... Option ) (* Circuit , error ) {
88+ c := & Circuit {}
9289
9390 o := options {
9491 isFailure : defaultFailureCondition ,
@@ -115,10 +112,10 @@ func NewCircuit[IN, OUT any](f WrappedFunc[IN, OUT], breaker Breaker, opts ...Op
115112 return c , nil
116113}
117114
118- // State reports the current [State] of the circuit .
115+ // State reports the current [State] of the [Circuit] .
119116// It should only be used for informational purposes. To minimize race conditions, the circuit should be called directly
120117// instead of checking its state first.
121- func (c * Circuit [ IN , OUT ] ) State () State {
118+ func (c * Circuit ) State () State {
122119 oa := c .openedAt .Load ()
123120
124121 if oa == 0 {
@@ -137,7 +134,7 @@ func (c *Circuit[IN, OUT]) State() State {
137134
138135// stateForCall returns the state of the circuit meant for the next call.
139136// It wraps [State] to keep the mutable part outside of the external API.
140- func (c * Circuit [ IN , OUT ] ) stateForCall () State {
137+ func (c * Circuit ) stateForCall () State {
141138 state := c .State ()
142139
143140 if state == StateHalfOpen {
@@ -152,18 +149,18 @@ func (c *Circuit[IN, OUT]) stateForCall() State {
152149
153150// open marks the circuit as open, if it not already.
154151// It is safe for concurrent calls and only the first one will actually set opening time.
155- func (c * Circuit [ IN , OUT ] ) open () {
152+ func (c * Circuit ) open () {
156153 // CompareAndSwap is needed to avoid clobbering another goroutine's openedAt value.
157154 c .openedAt .CompareAndSwap (0 , time .Now ().UnixMicro ())
158155}
159156
160157// reopen forcefully (re)marks the circuit as open, resetting the half-open time.
161- func (c * Circuit [ IN , OUT ] ) reopen () {
158+ func (c * Circuit ) reopen () {
162159 c .openedAt .Store (time .Now ().UnixMicro ())
163160}
164161
165162// close closes the circuit.
166- func (c * Circuit [ IN , OUT ] ) close () {
163+ func (c * Circuit ) close () {
167164 c .openedAt .Store (0 )
168165}
169166
@@ -173,22 +170,22 @@ func (c *Circuit[IN, OUT]) close() {
173170// If the breaker is closed, it returns a non-nil [Observer] that will be used to observe the result of the call.
174171//
175172// It implements [ObserverFactory], so that the [Circuit] can act as the base for [BreakerMiddleware].
176- func (c * Circuit [ IN , OUT ] ) ObserverForCall (_ context.Context , state State ) (Observer , error ) {
173+ func (c * Circuit ) ObserverForCall (_ context.Context , state State ) (Observer , error ) {
177174 if state == StateOpen {
178175 return nil , ErrCircuitOpen
179176 }
180- return stateObserver [ IN , OUT ] {
177+ return stateObserver {
181178 circuit : c ,
182179 state : state ,
183180 }, nil
184181}
185182
186- type stateObserver [ IN , OUT any ] struct {
187- circuit * Circuit [ IN , OUT ]
183+ type stateObserver struct {
184+ circuit * Circuit
188185 state State
189186}
190187
191- func (s stateObserver [ IN , OUT ] ) Observe (failure bool ) {
188+ func (s stateObserver ) Observe (failure bool ) {
192189 switch s .circuit .breaker .observe (s .state == StateHalfOpen , failure ) {
193190 case stateChangeNone :
194191 return // noop
@@ -199,59 +196,59 @@ func (s stateObserver[IN, OUT]) Observe(failure bool) {
199196 }
200197}
201198
202- // Call calls the wrapped function if the circuit is closed and returns its result. If the circuit is open, it returns
203- // [ErrCircuitOpen].
199+ // Wrap wraps the provided function with the given [Circuit].
200+ //
201+ // Calling the returned function if the circuit is closed and returns the result of the wrapped function.
202+ // If the circuit is open, it returns [ErrCircuitOpen].
204203//
205204// The wrapped function is called synchronously, but possible context errors are recorded as soon as they occur. This
206205// ensures the circuit opens quickly, even if the wrapped function blocks.
207206//
208207// By default, all errors are considered failures (including [context.Canceled]), but this can be customized via
209- // [WithFailureCondition] and [IgnoreContextCanceled].
208+ // [WithFailureCondition] and [IgnoreContextCanceled] on the provided [Circuit] .
210209//
211210// Panics are observed as failures, but are not recovered (i.e.: they are "repanicked" instead).
212- func (c * Circuit [IN , OUT ]) Call (ctx context.Context , in IN ) (out OUT , err error ) {
213- if c .f == nil {
214- return out , nil
215- }
216-
217- obs , err := c .observerFactory .ObserverForCall (ctx , c .stateForCall ())
218- if err != nil {
219- // Note: any errors here are not "observed" and do not count towards the breaker's failure rate.
220- // This includes:
221- // - ErrCircuitOpen
222- // - ErrConcurrencyLimit (for blocking limited circuits)
223- // - context timeouts while blocked on concurrency limit
224- // And any other errors that may be returned by optional breaker wrappers.
225- return out , err
226- }
211+ func Wrap [IN , OUT any ](c * Circuit , f WrappableFunc [IN , OUT ]) WrappableFunc [IN , OUT ] {
212+ return func (ctx context.Context , in IN ) (out OUT , err error ) {
213+ obs , err := c .observerFactory .ObserverForCall (ctx , c .stateForCall ())
214+ if err != nil {
215+ // Note: any errors here are not "observed" and do not count towards the breaker's failure rate.
216+ // This includes:
217+ // - ErrCircuitOpen
218+ // - ErrConcurrencyLimit (for blocking limited circuits)
219+ // - context timeouts while blocked on concurrency limit
220+ // And any other errors that may be returned by optional breaker wrappers.
221+ return out , err
222+ }
227223
228- // ensure we dedup the final - potentially wrapped - observer.
229- obs = dedupObservableCall (obs )
224+ // ensure we dedup the final - potentially wrapped - observer.
225+ obs = dedupObservableCall (obs )
230226
231- obsCtx , cancel := context .WithCancelCause (ctx )
232- defer cancel (errWrappedFunctionDone )
227+ obsCtx , cancel := context .WithCancelCause (ctx )
228+ defer cancel (errWrappedFunctionDone )
233229
234- // TODO: we could skip this if we could ensure the original context has neither cancellation nor deadline
235- go c .observeCtx (obs , obsCtx )
230+ // TODO: we could skip this if we could ensure the original context has neither cancellation nor deadline
231+ go c .observeCtx (obs , obsCtx )
236232
237- defer func () {
238- // ensure we also open the breaker on panics
239- if err := recover (); err != nil {
240- obs .Observe (true )
241- panic (err ) // let the caller deal with panics
242- }
243- obs .Observe (c .options .isFailure (err ))
244- }()
233+ defer func () {
234+ // ensure we also open the breaker on panics
235+ if err := recover (); err != nil {
236+ obs .Observe (true )
237+ panic (err ) // let the caller deal with panics
238+ }
239+ obs .Observe (c .options .isFailure (err ))
240+ }()
245241
246- return c .f (ctx , in )
242+ return f (ctx , in )
243+ }
247244}
248245
249246// errWrappedFunctionDone is used to distinguish between internal and external (to the lib) context cancellations.
250247var errWrappedFunctionDone = errors .New ("wrapped function done" )
251248
252249// observeCtx observes the given context for cancellation and records it as a failure.
253250// It assumes [Observer] is idempotent and deduplicates calls itself.
254- func (c * Circuit [ IN , OUT ] ) observeCtx (obs Observer , ctx context.Context ) {
251+ func (c * Circuit ) observeCtx (obs Observer , ctx context.Context ) {
255252 // We want to observe a context error as soon as possible to open the breaker, but at the same time we want to
256253 // keep the call to the wrapped function synchronous to avoid all pitfalls that come with asynchronicity.
257254 <- ctx .Done ()
0 commit comments