雑記: Circuit Breaker をオープンさせるだけ (resilience4j-spring-boot2)

お気付きの点がありましたらご指摘いただけますと幸いです。

前々回の記事でかいたおにぎりが返るだけの 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) {
        }
    }
}