• 作成:

Actix web(1.0)でグローバルな状態を共有する方法

試行錯誤を書いても役に立たないし色々な無駄な思考を書くのも面倒なので省いて短く書きます.

まずActix web 1.0では0.7でstateと呼ばれていたものはdataと呼ばれるようになりました.

dataのドキュメントは以下です.

actix_web::web::Data - Rust

そしてここに書かれているサンプルコードは間違っており, 素直に設定していくと

App data is not configured, to configure use App::data().というエラーメッセージを見て困惑することになります. これはdataと型が一致しないことを示すエラーです. web::Data::newを使って型を合わせる方法はわかりません. 元コードでもあってる気がするんですが.

というかそういうのはコンパイル時に弾いてほしい… 複数のdataを設定出来る(のか?)から仕方がないのか? Rustの型システムだと無理な感じですかね.

使う方法がわからなかったのでとりあえず私は型を直接突っ込む形式を試してみました. するとデータは確かに入っているはずなのに時間が経つとリセットされるという怪現象が起きました.

その原因はHttpServer::newの内部のコードはマルチスレッドで並列実行されるということで, それに私は気がついてませんでした. スレッドが切り替わったからデータも切り替わったのですね.

困り果てた私は最小再現コードを書いてTwitterとrust-jp Slackに質問を投げて, そういう旨の回答を頂けました.

https://rust-jp.slack.com/archives/C8FLSR5F1/p1559201687007200

data内部でArcを使ってるのでスレッド共有は問題ないと思っていたのですが, もう1つ必要だったようですね.

if your data implements Send + Sync traits you can use web::Data::new() and avoid double Arc.

とか書いてますけどこれは気にしないことにします. web::Data::new()を使って型合わせる方法わからないですし.

それで教えてもらった情報を元にしたコードがこちらになります.

/*
~~~Cargo.toml
[package]
name = "actix-data-example"
version = "0.1.0"
authors = ["ncaq <ncaq@ncaq.net>"]
edition = "2018"

[dependencies]
actix-web = "1.0.0-rc"
env_logger = "0.6.0"
~~~
 */

use actix_web::*;
use std::sync::*;

fn main() -> std::io::Result<()> {
    std::env::set_var("RUST_LOG", "actix_web=trace");
    env_logger::init();

    let data = Arc::new(Mutex::new(ActixData::default()));
    HttpServer::new(move || {
        App::new()
            .wrap(middleware::Logger::default())
            .data(data.clone())
            .service(web::resource("/index/").route(web::get().to(index)))
            .service(web::resource("/create/").route(web::get().to(create)))
    })
    .bind("0.0.0.0:3000")?
    .run()
}

fn index(actix_data: web::Data<Arc<Mutex<ActixData>>>) -> HttpResponse {
    println!("actix_data: {:?}", actix_data);
    HttpResponse::Ok().body(format!("{:?}", actix_data))
}

fn create(actix_data: web::Data<Arc<Mutex<ActixData>>>) -> HttpResponse {
    println!("actix_data: {:?}", actix_data);
    actix_data.lock().unwrap().counter += 1;
    HttpResponse::Ok().body(format!("{:?}", actix_data))
}

/// actix-webが保持する状態
#[derive(Debug, Default)]
struct ActixData {
    counter: usize,
}

グローバルな状態共有が出来て本当に良かったです. サーバの中のクロージャが並列実行されることに気が付かなかったのが主な敗因っぽいですね…

とりあえずサンプルコードが間違ってるっぽいことは上流に報告しました. Official document data cause App data is not configured, to configure use App::data() · Issue #874 · actix/actix-web 他にも困ってる人居たようですしね. rust - Actix-Web reports "App data is not configured" when processing a file upload - Stack Overflow

他のフレームワークでも事情は一緒なんでしょうけど, フルスタック系のはこういうのを事前にお膳立てしてくれるわけですね. Rustはゼロオーバーヘッドの言語なのでフレームワークもグローバル状態共有が必要に鳴るまでは用意しないと言うわけですかね.