import grails.spring.* import breaker.* class CircuitBreakerAspectTests extends GroovyTestCase { void testAroundAdvice() { def bb = new BeanBuilder() bb.beans { xmlns aop:"http://www.springframework.org/schema/aop" msgService(ExternalMessageService) msgServiceCircuitBreakerAspect(CircuitBreakerAspect) { id = 'msgServiceAspect' failureThresholdCount = 3 // trip breaker after three failures timeoutMillis = 5000 // fail fast for 5 seconds (giving the service a chance to heal itself), then try again } configService(ExternalConfigService) configServiceCircuitBreakerAspect(CircuitBreakerAspect) { id = 'configServiceAspect' failureThresholdCount = 4 // trip breaker after three failures timeoutMillis = 4000 // fail fast for 4 seconds (giving the service a chance to heal itself), then try again } aop.config("proxy-target-class":true) { aspect( id:"msgSvcAspectId", ref:"msgServiceCircuitBreakerAspect" ) { around method:"invoke", pointcut: "execution(* MessageServiceInterface.getMessage(..))" } aspect( id:"confSvcAspectId", ref:"configServiceCircuitBreakerAspect" ) { around method:"invoke", pointcut: "execution(* ConfigServiceInterface.getConfig(..))" } } } def appCtx = bb.createApplicationContext() // check message service def messageService = appCtx.getBean("msgService") CircuitBreakerAspect msgAspect = appCtx.getBean('msgServiceCircuitBreakerAspect') assertEquals 'msgServiceAspect', msgAspect.id assertEquals CircuitBreakerClosedState, msgAspect.state.class assertEquals 3, msgAspect.state.failureThresholdCount assertEquals 0, msgAspect.state.failureCount assertEquals 5000, msgAspect.openState.timeoutMillis def result = messageService.getMessage(false) assertEquals 'Hello world', result assertEquals CircuitBreakerClosedState, msgAspect.state.class shouldFail { messageService.getMessage(true) } assertEquals CircuitBreakerClosedState, msgAspect.state.class assertEquals 1, msgAspect.state.failureCount shouldFail { messageService.getMessage(true) } assertEquals CircuitBreakerClosedState, msgAspect.state.class assertEquals 2, msgAspect.state.failureCount // as configured above, third failure should trip the breaker shouldFail { messageService.getMessage(true) } assertEquals CircuitBreakerOpenState, msgAspect.state.class Thread.currentThread().sleep(1000) // still open assertEquals CircuitBreakerOpenState, msgAspect.state.class Thread.currentThread().sleep(4000) // now we should be ready to attempt to reset the breaker // another error should throw a failed reset exception shouldFail(CircuitBreakerFailedResetException) { messageService.getMessage(true) } assertEquals CircuitBreakerOpenState, msgAspect.state.class Thread.currentThread().sleep(5001) // now we should be ready to attempt to reset the breaker // and a successful attempt should fully reset to a closed state result = messageService.getMessage(false) assertEquals 'Hello world', result // everything is back to normal when the first call succeeds in a reset attempt assertEquals CircuitBreakerClosedState, msgAspect.state.class assertEquals 0, msgAspect.state.failureCount // same checks on the config service, show that we can configure them separately def configService = appCtx.getBean("configService") CircuitBreakerAspect configAspect = appCtx.getBean('configServiceCircuitBreakerAspect') assertEquals 'configServiceAspect', configAspect.id assertEquals CircuitBreakerClosedState, configAspect.state.class assertEquals 4, configAspect.state.failureThresholdCount assertEquals 0, configAspect.state.failureCount assertEquals 4000, configAspect.openState.timeoutMillis result = configService.getConfig(false) assertEquals 'Hello world', result assertEquals CircuitBreakerClosedState, configAspect.state.class shouldFail { configService.getConfig(true) } assertEquals CircuitBreakerClosedState, configAspect.state.class assertEquals 1, configAspect.state.failureCount shouldFail { configService.getConfig(true) } assertEquals CircuitBreakerClosedState, configAspect.state.class assertEquals 2, configAspect.state.failureCount shouldFail { configService.getConfig(true) } assertEquals CircuitBreakerClosedState, configAspect.state.class assertEquals 3, configAspect.state.failureCount // as configured above, third failure should trip the breaker shouldFail { configService.getConfig(true) } assertEquals CircuitBreakerOpenState, configAspect.state.class Thread.currentThread().sleep(1000) // still open assertEquals CircuitBreakerOpenState, configAspect.state.class Thread.currentThread().sleep(3000) // now we should be ready to attempt to reset the breaker // another error should throw a failed reset exception shouldFail { configService.getConfig(true) } assertEquals CircuitBreakerOpenState, configAspect.state.class Thread.currentThread().sleep(4001) // now we should be ready to attempt to reset the breaker // and a successful attempt should fully reset to a closed state result = configService.getConfig(false) assertEquals 'Hello world', result // everything is back to normal when the first call succeeds in a reset attempt assertEquals CircuitBreakerClosedState, configAspect.state.class assertEquals 0, configAspect.state.failureCount } } interface MessageServiceInterface { def getMessage(Boolean shouldFail) } class ExternalMessageService implements MessageServiceInterface { def getMessage(Boolean shouldFail) { if(shouldFail) { throw new IllegalArgumentException('Error') } return 'Hello world' } } interface ConfigServiceInterface { def getConfig(Boolean shouldFail) } class ExternalConfigService implements ConfigServiceInterface { def getConfig(Boolean shouldFail) { if(shouldFail) { throw new IllegalArgumentException('Error') } return 'Hello world' } }