hisamounaのブログ

アウトプットを習慣化するためのブログ

Rust製WebサーバにCircuitBreakerを入れてみる

Rust製WebサーバにCircuitBreakerを導入してみました。

最近だと、proxy(e.g. envoy)にCircuitBreakerを入れるパターンもあるかと思いますが、今回はアプリケーションに入れるパターンです。

使用するライブラリ

Rocket: RustでWebFrameworkといえば定番になるかと思います。

github.com

failsafe-rs: Rust circuitbreaker で検索したときに一番最初に出てきました。

github.com

実装

コード

CircuitBreakerのstateに応じてレスポンスを分ける。

match state.circuit.call(|| hello(query.name)) {
    Err(Error::Inner(_)) => {
        eprintln!("fail");
        return Err(Status::InternalServerError)
    },
    Err(Error::Rejected) => {
        eprintln!("rejected");
        return Err(Status::ServiceUnavailable)
    },
    Ok(x) => {
        return Ok(x)
    }
}

CircuitBreakerの設定をどこで定義するかに悩みました。

Golangであれば、Globalの変数を定義してStatefulに管理しようとしましたが、Rustは変数のscopeを厳密にする必要があるため同じことができませんでした。

軽く調べた限り(issue)、 Heapへのallocationは開発者が直接操作できず、runtimeでしか行われない。

NGパターン

static mut bbo :Exponential = backoff::exponential(Duration::from_secs(3), Duration::from_secs(30));
static mut pl :ConsecutiveFailures<failsafe::backoff::Exponential> = failure_policy::consecutive_failures(3, bbo);
static mut cb :StateMachine<failsafe::failure_policy::ConsecutiveFailures<failsafe::backoff::Exponential>, ()>= Config::new()
    .failure_policy(pl)
    .build();

エラーメッセージ:

calls in statics are limited to constant functions, tuple structs and tuple variants

至極まっとうなメッセージでした。

rocketのStateを使って解決

struct RocketState {
    circuit : StateMachine<ConsecutiveFailures<Exponential>, ()>
}

#[launch]
fn rocket() -> _ {
    let back_off = backoff::exponential(Duration::from_secs(30), Duration::from_secs(60));
    let policy = failure_policy::consecutive_failures(3, back_off);
    let circuit_breaker = Config::new()
        .failure_policy(policy)
        .build();
    let hystrix_conf = RocketState{circuit: circuit_breaker};
    rocket::build()
        .manage(hystrix_conf)
    .
    .
    .

動作確認

│rocket_circuitbreaker_trial on  main
└─> cargo run
.
.
.

🛰  Routes:
   >> (api_hello) GET /hello?<query..>
📡 Fairings:

# 別ターミナルで
└─> curl -I -XGET 'http://localhost:8000/hello?name=hello'
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
server: Rocket
permissions-policy: interest-cohort=()
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
content-length: 5
date: Tue, 26 Oct 2021 05:35:59 GMT

┌───────────────────>
│~
└─> curl -I -XGET 'http://localhost:8000/hello?name=error'
HTTP/1.1 500 Internal Server Error
content-type: text/html; charset=utf-8
server: Rocket
permissions-policy: interest-cohort=()
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
content-length: 436
date: Tue, 26 Oct 2021 05:36:06 GMT

┌───────────────────>
│~
└─> curl -I -XGET 'http://localhost:8000/hello?name=error'
HTTP/1.1 500 Internal Server Error
content-type: text/html; charset=utf-8
server: Rocket
permissions-policy: interest-cohort=()
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
content-length: 436
date: Tue, 26 Oct 2021 05:36:06 GMT

┌───────────────────>
│~
└─> curl -I -XGET 'http://localhost:8000/hello?name=error'
HTTP/1.1 500 Internal Server Error
content-type: text/html; charset=utf-8
server: Rocket
permissions-policy: interest-cohort=()
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
content-length: 436
date: Tue, 26 Oct 2021 05:36:09 GMT

# num_failures(3)を超えたので 503エラーに
┌───────────────────>
│~
└─> curl -I -XGET 'http://localhost:8000/hello?name=error'
HTTP/1.1 503 Service Unavailable
content-type: text/html; charset=utf-8
server: Rocket
permissions-policy: interest-cohort=()
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
content-length: 397
date: Tue, 26 Oct 2021 05:36:10 GMT

# stateがclosedとなっているので、503エラーに
┌───────────────────>
│~
└─> curl -I -XGET 'http://localhost:8000/hello?name=hello'
HTTP/1.1 503 Service Unavailable
content-type: text/html; charset=utf-8
server: Rocket
permissions-policy: interest-cohort=()
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
content-length: 397
date: Tue, 26 Oct 2021 05:36:17 GMT

# stateがhalf openとなったことで、200 OKに
┌───────────────────>
│~
└─> curl -I -XGET 'http://localhost:8000/hello?name=hello'
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
server: Rocket
permissions-policy: interest-cohort=()
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
content-length: 5
date: Tue, 26 Oct 2021 05:36:24 GMT

今のままだと、全APIでひとつのCircuitBreakerのStateに左右されてしまうので、 RocketStateAPIごとのCircuitBreakerの設定フィールドを用意するなど工夫が必要かと思います。

ご指摘あれば、細かいところでも教えていただけると助かります。