お気付きの点がありましたらご指摘いただけますと幸いです。
前々回の記事でかいたおにぎりが返るだけの REST API を流用して Resilience4j の Circuit Breaker をオープンさせて Micrometer で検知したいと思います。参考文献 2, 3 によると以下の依存性(// これ)が要るとのことなので build.gradle に追記してプロジェクトを開き直します。
plugins { id 'org.springframework.boot' version '2.6.1' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' implementation 'io.github.resilience4j:resilience4j-spring-boot2:1.7.0' // これ implementation 'org.springframework.boot:spring-boot-starter-actuator' // これ implementation 'org.springframework.boot:spring-boot-starter-aop' // これ implementation 'io.github.resilience4j:resilience4j-micrometer:1.7.0' // これ } test { useJUnitPlatform() }
application.yml に Circuit Breaker のデフォルト設定を追記します。今回は Circuit Breaker をすぐにオープンさせたいので直近の 4 回の試行のうち 50% が失敗したらオープンするようにします。
spring.main.allow-bean-definition-overriding: true resilience4j.circuitbreaker: configs: default: slidingWindowSize: 4 minimumNumberOfCalls: 4 failureRateThreshold: 50
BentouApplication.java に MeterRegistry と RestTemplate を登録します。
package com.example.bentou; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @SpringBootApplication public class BentouApplication { @Configuration public static class BentouApplicationConfiguration { @Bean MeterRegistry meterRegistry() { return new SimpleMeterRegistry(); } @Bean RestTemplate restTemplate() { return new RestTemplateBuilder().build(); } } public static void main(String[] args) { SpringApplication.run(BentouApplication.class, args); } }
どのコンポーネントでもいいですがツナおにぎりサービスに Circuit Breaker を使用するメソッドを追加します。get() というメソッドで外部にアクセスするが失敗したら fallback するようにします。
package com.example.bentou; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; @Service @Qualifier("tuna") public class OnigiriServiceTuna implements OnigiriService { private final RestTemplate restTemplate; public OnigiriServiceTuna(RestTemplate restTemplate) { this.restTemplate = restTemplate; } @Override public Onigiri provideOnigiri() { return new Onigiri("ツナ"); } @CircuitBreaker(name = "hoge", fallbackMethod = "fallback") public String get() { return restTemplate.getForObject("abc", String.class); } private String fallback(RestClientException e) { return "NG"; } }
ここまで準備したら以下のように RestTemplate を最初だけ成功するようにモックすると、直近 4 回の失敗率が 50% になった時点で Circuit Breaker がオープンすることがわかります。
package com.example.bentou; import io.github.resilience4j.circuitbreaker.CallNotPermittedException; import io.micrometer.core.instrument.MeterRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.when; @SpringBootTest class OnigiriServiceTunaTest { @Autowired OnigiriServiceTuna service; @Autowired MeterRegistry meterRegistry; @MockBean RestTemplate restTemplate; // RestTemplate から /abc にアクセスすると最初の3回だけ成功して後は失敗するようにモック @BeforeEach public void setUp() { when(restTemplate.getForObject("abc", String.class)) .thenReturn("OK", "OK", "OK") .thenThrow(new RestClientException("ERROR")); } @Test public void test() { assertEquals("OK", service.get()); // 1 assertEquals("OK", service.get()); // 2 assertEquals("OK", service.get()); // 3 assertEquals("NG", service.get()); // 4 // この時点ではサーキットブレーカーはオープンではない assertEquals(0, meterRegistry.get("resilience4j.circuitbreaker.state") .tags("name", "hoge", "state", "open").gauge().value(), 1e-6); assertEquals("NG", service.get()); // 5 // この時点でサーキットブレーカーはオープンになる assertEquals(1, meterRegistry.get("resilience4j.circuitbreaker.state") .tags("name", "hoge", "state", "open").gauge().value(), 1e-6); // オープンになったので CallNotPermittedException が送出される try { assertEquals("NG", service.get()); // 6 fail(); } catch (CallNotPermittedException exception) { } } }