この記事はナイル Advent Calendar 2021 17日目の記事です。


Rust製のWebフレームワークactix-webを使うとき、Redisに接続する方法がよくわからず困った。関連クレートがいくつもあり、特徴もそれぞれ違う。この記事では目についたものを全部紹介し、実装してみる。

Using Redis in a Rust web service - LogRocket Blogという記事はactix-webではなくwarpで同じようなことをやっている。この記事・コードでもだいぶ参考にした。

やりたいこと

  • actix-webでRedisに接続する方法をあるだけ試す
  • RedisでSETとGETの操作をする
    正確にはSETでなくSETEXをする。SETだとデータがたまりまくってしまうので、時間が経ったら消えるようにする。

使いたいRedisクレート

  • redis-rs(crates.iodocs)
    Redisクライアント。同期処理・非同期処理どちらもできる。今回は非同期処理を使う。
    コネクションプールは r2d2 を使うこともでき、その場合は同期的になる。が、使い方に関するドキュメントや例がなさすぎて、かなり悩んだ。このPull Requestを参考にすればよさそう(そのうちマージされるかも)。

コネクションプールたち

以下はすべてredis-rsをコネクションプールとともに使うクレート。

  • bb8-redis(crates.iodocs)
    bb8というコネクションプールを使う。非同期。
  • deadpool-redis(crates.iodocs)
    deadpoolというコネクションプールを使う。非同期。
  • mobc-redis(crates.iodocs)
    mobcというコネクションプールを使う。非同期。
  • r2d2-redis(crates.iodocs)
    r2d2というコネクションプールを使う。同期的。
    このクレートは今後あまり使われなくなっていくかもしれない。なぜならredis-rs自体にr2d2サポートが入るようになったため。r2d2のREADMEには以前はこれがredis用クレートとして参照されていたが、deprecated扱いになり、消えてしまった。(r2d2のPull Request参照)
    開発は続けていくらしいし、このクレートを使っている記事なども多いので、一応実装してみた。crates.ioでのダウンロード数は今も多い。

環境

  • Rust
    1.57.0
  • actix-web
    4.0.0-beta.12
    現在はv3が安定版で、v4はベータ扱い。
    Redis関連のライブラリでtokioの新しいバージョンを使っており、v3だと "there is no reactor running, must be called from the context of a Tokio 1.x runtime" というエラーが出るのでv4を使う。
  • redis-rs
    0.21.4
  • bb8-redis
    0.10.1
  • deadpool-redis
    0.10.1
  • mobc-redis
    0.7.0
  • r2d2-redis
    0.14.0

実装

コードはここ

localhost:8080/direct にアクセスすると、その都度生成されるUUID(v4)が直に返ってくる。このときUUIDはRedisにSETEXされる(5分で消える)。
そのUUIDを使って localhost:8080/direct/{UUID}/ にアクセスすると、RedisからGETして、文字列が直に返ってくる。
他のエンドポイントはREADME参照。レスポンス形式、エラー処理などは適当。

Cargo.toml

[dependencies]
actix-web = "4.0.0-beta.12"

redis = { version = "0.21.4", features = ["r2d2"] }
r2d2 = "0.8.9"

r2d2_redis = "0.14.0"
bb8-redis = "0.10.1"
deadpool-redis = "0.10.1"
mobc = "0.7"
mobc-redis = "0.7.0"

uuid = { version = "0.8", features = ["v4"] }

redis(redis-rs)の r2d2 をfeaturesに書いて有効にしておく。これを書かないと、r2d2と組み合わせられない。

Redis用モジュール

redis-rsをコネクションプールなしで直接使うモジュール(direct.rs)と、コネクションプールを使うモジュール(with_r2d2.rs、with_bb8.rsなど他すべて)の2種類がある。少しだけ書き方が違うので、両方抜粋してみる。

以下、MyError、DirectClientやR2D2Pool、MAX_POOL_SIZEなどは別のところで定義している。長くなるので省く。

direct.rs

create_client() でクライアントを作る。 open() の時点ではコネクションは作られない(とドキュメントに書いてある)。 get_connection() で初めてコネクションが作られる。
そのあと set_exget などの具体的なRedis操作をするときは、 create_connection() でクライアントからコネクションをその都度作って処理をする。

pub fn create_client(host_addr: &str) -> Result<DirectClient, MyError> {
    redis::Client::open(host_addr).map_err(|e| MyError::new_string(e.to_string()))
}

pub async fn create_connection(client: &DirectClient) -> Result<Connection, MyError> {
    client
        .get_async_connection()
        .await
        .map_err(|e| MyError::new_string(e.to_string()))
}

pub async fn set(client: &DirectClient, key: &str, value: &str) -> Result<String, MyError> {
    let mut con = create_connection(client).await?;
    let redis_key = get_key(key);
    con.set_ex(redis_key, value, TTL)
        .await
        .map_err(|e| MyError::new_string(e.to_string()))
}

with_r2d2.rs

コネクションプールを作る。 コネクションプールを使う他のモジュールもほぼ同じ。

r2d2は同期的なので、asyncがない。
create_pool() でコネクションプールを作り、setなどのときは create_connection() でコネクションを取得する。

pub fn create_pool(host_addr: &str) -> Result<R2D2Pool, MyError> {
    let client = redis::Client::open(host_addr).map_err(|e| MyError::new_string(e.to_string()))?;
    r2d2::Pool::builder()
        .max_size(MAX_POOL_SIZE)
        .connection_timeout(CONNECTION_TIMEOUT)
        .build(client)
        .map_err(|e| MyError::new_string(e.to_string()))
}

fn create_connection(pool: &R2D2Pool) -> Result<R2d2PooledCon, MyError> {
    pool.get().map_err(|e| MyError::new_string(e.to_string()))
}

pub fn set(pool: &R2D2Pool, key: &str, value: &str) -> Result<(), MyError> {
    let mut con = create_connection(pool)?;
    let redis_key = get_key(key);
    con.set_ex(redis_key, value, TTL)
        .map_err(|e| MyError::new_string(e.to_string()))
}

actix-web側

main.rsの話。

redis-rsを直接使うときはクライアントを使いまわすことにする。それ以外はコネクションプールを使うので、それぞれのクレートで作ったコネクションプールを引き回す。
このように引き回すときは App::new() のところで app_data() を使って設定すればいい。
(この例はドキュメントにあり、そこでは data() というメソッドが使われているが、手元でやるとdeprecatedになった。新しいactix-webではこのメソッドが変わっているらしい。)

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let host = "redis://127.0.0.1/";
    let direct_client = direct::create_client(host).unwrap();
    let r2d2_pool = with_r2d2::create_pool(host).unwrap();
    // ...

    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(direct_client.clone()))
            .app_data(web::Data::new(r2d2_pool.clone()))
            // ...

各エンドポイントではこれを使って処理ができる。

#[get("/direct")]
async fn set_direct(client: web::Data<direct::DirectClient>) -> impl Responder {
    let id = Uuid::new_v4();
    let key = format!("{}", id);
    let value = "hi";
    let result = direct::set(&client, &key, value).await;
    match result {
        Ok(_) => HttpResponse::Ok().body(key),
        Err(e) => HttpResponse::InternalServerError().body(e.msg),
    }
}

#[get("/r2d2")]
async fn set_with_r2d2(pool: web::Data<with_r2d2::R2D2Pool>) -> impl Responder {
    let id = Uuid::new_v4();
    let key = format!("{}", id);
    let value = "hi";
    let result = with_r2d2::set(&pool, &key, value);
    match result {
        Ok(_) => HttpResponse::Ok().body(key),
        Err(e) => HttpResponse::InternalServerError().body(e.msg),
    }
}

おわりに

  • サンプルがけっこういろいろあってとっつきやすい
    参考にいくつか挙げた。ただしシンプルな例が多いので、プールを作るときにタイムアウトを設定する方法を調べるときなどはドキュメントを読むしかない。(タイムアウトについては今回書いた方法でいいのか自信がない。)
  • クレート多すぎ
    何を使えばベストなのかよくわからない。あとで性能も調べたい。
    非同期でコネクションプールを使うのが良いと思うので、bb8かdeadpool、あるいはmobcだろうか。mobcはGitHubでの更新がやや少ないのが気になる。(枯れているということかもしれない。)
  • この記事はこの練習用コードがきっかけで書いた。Redis周りがよくわからなかったので、あとでまとめようと思った。いろいろ改善が必要なコード。
  • ライフタイムがわからない
    with_bb8.rsでコネクションの型名にエイリアスをつけようと思った。
    type BB8Connetion = bb8_redis::bb8::PooledConnection<RedisConnectionManager>;
    
    としたところ、エラーになった。 "expected named lifetime parameter" とのこと。 PooledConnection 構造体の定義にライフタイム指定子があるため…なのか? type で別名をつけるだけなのにこんなエラーが出るというのが衝撃だった。Rust何もわからない。

参考