@@ -326,93 +326,146 @@ export const make = <E = unknown>(
326326 const getState = ( strategyId : string ) : Effect . Effect < CircuitBreaker . CircuitBreakerState , never , never > =>
327327 Ref . get ( states ) . pipe ( Effect . map ( ( map ) => map . get ( strategyId ) ?? defaultState ) )
328328
329- const updateState = ( strategyId : string , newState : CircuitBreaker . CircuitBreakerState ) =>
330- Ref . update ( states , ( map ) => new Map ( map ) . set ( strategyId , newState ) )
329+ // Create tripping strategy if provided, otherwise use default failure count
330+ const trippingStrategy = finalConfig . strategy
331+ ? yield * finalConfig . strategy
332+ : yield * failureCount ( finalConfig . maxFailures ?? 5 )
331333
332- const shouldAllowRequest = ( state : CircuitBreaker . CircuitBreakerState ) : Effect . Effect < boolean , never , never > =>
333- Effect . gen ( function * ( ) {
334- const now = yield * Clock . currentTimeMillis
334+ type Admission = 'Allowed' | 'Rejected' | 'HalfOpened'
335+
336+ const tryAdmit = ( strategyId : string , now : number ) : Effect . Effect < Admission , never , never > =>
337+ Ref . modify ( states , ( map ) : [ Admission , Map < string , CircuitBreaker . CircuitBreakerState > ] => {
338+ const state = map . get ( strategyId ) ?? defaultState
335339
336340 switch ( state . state ) {
337341 case 'Closed' :
338- return true
339- case 'Open' :
340- return now - state . lastFailureTime >= Duration . toMillis ( finalConfig . resetTimeout ?? Duration . seconds ( 60 ) )
342+ return [ 'Allowed' , map ]
341343 case 'HalfOpen' :
342- return state . halfOpenCalls < ( finalConfig . halfOpenMaxCalls ?? 3 )
344+ if ( state . halfOpenCalls >= ( finalConfig . halfOpenMaxCalls ?? 3 ) ) {
345+ return [ 'Rejected' , map ]
346+ }
347+
348+ return [
349+ 'Allowed' ,
350+ new Map ( map ) . set ( strategyId , {
351+ ...state ,
352+ halfOpenCalls : state . halfOpenCalls + 1 ,
353+ } ) ,
354+ ]
355+ case 'Open' :
356+ if ( now - state . lastFailureTime < Duration . toMillis ( finalConfig . resetTimeout ?? Duration . seconds ( 60 ) ) ) {
357+ return [ 'Rejected' , map ]
358+ }
359+
360+ return [
361+ 'HalfOpened' ,
362+ new Map ( map ) . set ( strategyId , {
363+ ...state ,
364+ state : 'HalfOpen' ,
365+ halfOpenCalls : 1 ,
366+ } ) ,
367+ ]
343368 }
344369 } )
345370
346- // Create tripping strategy if provided, otherwise use default failure count
347- const trippingStrategy = finalConfig . strategy
348- ? yield * finalConfig . strategy
349- : yield * failureCount ( finalConfig . maxFailures ?? 5 )
350-
351- const onSuccess = ( strategyId : string , state : CircuitBreaker . CircuitBreakerState ) =>
371+ const onSuccess = ( strategyId : string ) =>
352372 Effect . gen ( function * ( ) {
353- if ( state . state === 'HalfOpen' ) {
373+ yield * trippingStrategy . shouldTrip ( true )
374+
375+ const closedCircuit = yield * Ref . modify (
376+ states ,
377+ ( map ) : [ boolean , Map < string , CircuitBreaker . CircuitBreakerState > ] => {
378+ const state = map . get ( strategyId ) ?? defaultState
379+
380+ if ( state . state === 'HalfOpen' ) {
381+ return [
382+ true ,
383+ new Map ( map ) . set ( strategyId , {
384+ ...defaultState ,
385+ state : 'Closed' ,
386+ } ) ,
387+ ]
388+ }
389+
390+ if ( state . failures > 0 ) {
391+ return [
392+ false ,
393+ new Map ( map ) . set ( strategyId , {
394+ ...state ,
395+ failures : 0 ,
396+ } ) ,
397+ ]
398+ }
399+
400+ return [ false , map ]
401+ } ,
402+ )
403+
404+ if ( closedCircuit ) {
354405 // Reset to closed state after successful half-open calls
355406 yield * trippingStrategy . onReset
356- yield * notifyStateChange ( state . state , 'Closed' )
357- yield * updateState ( strategyId , {
358- ...defaultState ,
359- state : 'Closed' ,
360- } )
407+ yield * notifyStateChange ( 'HalfOpen' , 'Closed' )
361408 yield * withMetrics ( ( metrics ) =>
362409 Metric . increment ( metrics . stateChanges ) . pipe (
363410 Effect . zipRight ( Metric . set ( metrics . state , stateToCode ( 'Closed' ) ) ) ,
364411 ) ,
365412 )
366- } else if ( state . failures > 0 ) {
367- // Reset failures on success
368- yield * updateState ( strategyId , {
369- ...state ,
370- failures : 0 ,
371- } )
372413 }
373414 } )
374415
375- const onFailure = ( strategyId : string , state : CircuitBreaker . CircuitBreakerState ) =>
416+ const onFailure = ( strategyId : string ) =>
376417 Effect . gen ( function * ( ) {
377418 const now = yield * Clock . currentTimeMillis
378419 const shouldTrip = yield * trippingStrategy . shouldTrip ( false )
379420
380- if ( state . state === 'HalfOpen' ) {
381- // Failed during half-open, go back to open
382- yield * notifyStateChange ( state . state , 'Open' )
383- yield * updateState ( strategyId , {
384- ...state ,
385- state : 'Open' ,
386- failures : state . failures + 1 ,
387- lastFailureTime : now ,
388- halfOpenCalls : 0 ,
389- } )
421+ const openedFrom = yield * Ref . modify (
422+ states ,
423+ ( map ) : [ CircuitBreaker . State | undefined , Map < string , CircuitBreaker . CircuitBreakerState > ] => {
424+ const state = map . get ( strategyId ) ?? defaultState
425+
426+ if ( state . state === 'HalfOpen' ) {
427+ return [
428+ 'HalfOpen' ,
429+ new Map ( map ) . set ( strategyId , {
430+ ...state ,
431+ state : 'Open' ,
432+ failures : state . failures + 1 ,
433+ lastFailureTime : now ,
434+ halfOpenCalls : 0 ,
435+ } ) ,
436+ ]
437+ }
438+
439+ if ( shouldTrip && state . state === 'Closed' ) {
440+ return [
441+ 'Closed' ,
442+ new Map ( map ) . set ( strategyId , {
443+ ...state ,
444+ state : 'Open' ,
445+ failures : state . failures + 1 ,
446+ lastFailureTime : now ,
447+ } ) ,
448+ ]
449+ }
450+
451+ return [
452+ undefined ,
453+ new Map ( map ) . set ( strategyId , {
454+ ...state ,
455+ failures : state . failures + 1 ,
456+ lastFailureTime : now ,
457+ } ) ,
458+ ]
459+ } ,
460+ )
461+
462+ if ( openedFrom !== undefined ) {
463+ yield * notifyStateChange ( openedFrom , 'Open' )
390464 yield * withMetrics ( ( metrics ) =>
391465 Metric . increment ( metrics . stateChanges ) . pipe (
392466 Effect . zipRight ( Metric . set ( metrics . state , stateToCode ( 'Open' ) ) ) ,
393467 ) ,
394468 )
395- } else if ( shouldTrip && state . state === 'Closed' ) {
396- // Threshold reached, open the circuit
397- yield * notifyStateChange ( state . state , 'Open' )
398- yield * updateState ( strategyId , {
399- ...state ,
400- state : 'Open' ,
401- failures : state . failures + 1 ,
402- lastFailureTime : now ,
403- } )
404- yield * withMetrics ( ( metrics ) =>
405- Metric . increment ( metrics . stateChanges ) . pipe (
406- Effect . zipRight ( Metric . set ( metrics . state , stateToCode ( 'Open' ) ) ) ,
407- ) ,
408- )
409- } else {
410- // Increment failures but keep closed
411- yield * updateState ( strategyId , {
412- ...state ,
413- failures : state . failures + 1 ,
414- lastFailureTime : now ,
415- } )
416469 }
417470 } )
418471
@@ -421,53 +474,41 @@ export const make = <E = unknown>(
421474 effect : Effect . Effect < A , E2 , R > ,
422475 ) : Effect . Effect < A , E2 | CircuitBreaker . OpenError , R > =>
423476 Effect . gen ( function * ( ) {
424- const state = yield * getState ( strategyId )
425- const shouldAllow = yield * shouldAllowRequest ( state )
477+ const now = yield * Clock . currentTimeMillis
478+ const admission = yield * tryAdmit ( strategyId , now )
426479
427- if ( ! shouldAllow ) {
480+ if ( admission === 'Rejected' ) {
428481 yield * withMetrics ( ( metrics ) => Metric . increment ( metrics . rejectedCalls ) )
429482 return yield * Effect . fail ( OpenError ( strategyId ) )
430483 }
431484
432- // Transition to half-open if we're allowing a request from open state
433- if ( state . state === 'Open' ) {
434- yield * notifyStateChange ( state . state , 'HalfOpen' )
435- yield * updateState ( strategyId , {
436- ...state ,
437- state : 'HalfOpen' ,
438- halfOpenCalls : 1 ,
439- } )
485+ if ( admission === 'HalfOpened' ) {
486+ yield * notifyStateChange ( 'Open' , 'HalfOpen' )
440487 yield * withMetrics ( ( metrics ) =>
441488 Metric . increment ( metrics . stateChanges ) . pipe (
442489 Effect . zipRight ( Metric . set ( metrics . state , stateToCode ( 'HalfOpen' ) ) ) ,
443490 ) ,
444491 )
445- } else if ( state . state === 'HalfOpen' ) {
446- yield * updateState ( strategyId , {
447- ...state ,
448- halfOpenCalls : state . halfOpenCalls + 1 ,
449- } )
450492 }
451493
452494 const result = yield * Effect . either ( effect )
453495
454496 if ( Either . isRight ( result ) ) {
455- yield * onSuccess ( strategyId , yield * getState ( strategyId ) )
497+ yield * onSuccess ( strategyId )
456498 yield * withMetrics ( ( metrics ) => Metric . increment ( metrics . successfulCalls ) )
457499 return result . right
458500 } else {
459501 // Check if this failure should be counted based on the isFailure predicate
460502 const shouldCountFailure = finalConfig . isFailure ? finalConfig . isFailure ( result . left ) : true
461503
462504 if ( shouldCountFailure ) {
463- yield * onFailure ( strategyId , yield * getState ( strategyId ) )
505+ yield * onFailure ( strategyId )
464506 yield * withMetrics ( ( metrics ) => Metric . increment ( metrics . failedCalls ) )
465507 }
466508
467509 return yield * Effect . fail ( result . left )
468510 }
469511 } )
470-
471512 const currentState = ( strategyId : string ) : Effect . Effect < CircuitBreaker . State , never , never > =>
472513 getState ( strategyId ) . pipe ( Effect . map ( ( state ) => state . state ) )
473514
0 commit comments