Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

Mac OS X から EC2 インスタンス上に存在する JVM に VisualVM を接続する

EC2 インスタンス上に存在する Web サーバーを VisualVM 接続してで見たいと思った際に、どのようにして見たらよいかをまとめておきます。

なお、ローカル環境が Mac OS X での場合です。

手順

  1. java の起動コマンドに引数をいくつか追加する。
  2. SOCKS プロトコルで EC2 インスタンスに接続できるようにしておく。
  3. VisualVM の起動時設定に↑を使用できるようなオプションを追加する。
  4. JMX 接続を使って、リモート接続を開始する。

1. java の起動コマンドに引数をいくつか追加する

今回はポート番号 3333JMX リモート接続用に開放しておき、そこに接続してもらうように設定します。下記の引数を追加し、アプリケーションを起動しておきます。

-Dcom.sun.management.jmxremote.port=3333 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false

2. SOCKS プロトコルで EC2 インスタンスに接続できるようにしておく

下記コマンドを実行し、SOCKS プロトコル *1 を有効にします。

ssh -i {pem_file_path} -fND 10000 ec2-user@{ec2-private-ip}

{pem_file_path} には、自身の PC に保存してある pem ファイルの居場所を指定します。たとえば、~/.ssh/visualvm_con.pem などです。

{ec2-private-ip} には、接続したい EC2 インスタンスのプライベート IP アドレスを入れておきます。

オプションについては、

  • -i: 秘密鍵ファイルを指定します。
  • -f: バックグラウンド実行を意味します。
  • -N: リモートコマンドを実行しません。
  • -D: ローカルホスト側におけるアプリケーションの動的なポート伝送を指定します。

3. VisualVM の起動時設定に↑を使用できるようなオプションを追加する

起動時の設定に SOCKS プロトコルの設定を追加して、jvisualvm を実行します。

jvisualvm -J-DsocksProxyHost=localhost -J-DsocksProxyPort=10000 -J-DsocksNonProxyHosts=

あるいはGUI から設定する場合は、「File > Preferences > Network」の順に開き、下記の画像のように設定値を入力しておきます。 SOCS Proxy 向けの設定の箇所に、localhost10000 を入れておきます。

f:id:yuk1tyd:20200929234137p:plain

4. JMX 接続を使って、リモート接続を開始する

JMX 接続を追加」を押すと、追加用のダイアログが立ち上がるので、そこに先ほどの {ec2-private-ip} と同じ IP アドレスとポートを書きます。ポートは JMX 用のポートのため、3333 を使用することに注意が必要です。

f:id:yuk1tyd:20200307194717p:plain
JMX 接続を追加」を押す

f:id:yuk1tyd:20200307194817p:plain
プライベート IP アドレスとポート番号を書く

上記の状態にして、「了解」ボタンを押すと、EC2 上のサーバーとの接続が開始されます。接続に成功すると、いつも見る VisualVM の画面が立ち上がってくれるはずです。

*1:あるポートにバインドした通信路へのソケットを提供してくれ、TCP/IP の肩代わりをするプロトコルです。ここで SSH を経由して JMX を通します。

Rust の HTTP クライアント surf を試してみる

async/await に対応した HTTP サーバーの tide を先日紹介し、先日も記事を書きました。

同様に async/await に対応した HTTP クライアントの surf というライブラリがあるようなので、それを軽く紹介したいと思います。執筆時点での surf のバージョンは 1.0.3 です。

surf

github.com

async/await に対応した HTTP クライアントです。HTTP サーバー用のライブラリ tide と同様に、async_std を非同期処理ランタイムに使用しています。ちなみに、Rust の HTTP クライアントライブラリで扱いやすいものとしては、 reqwest が有名で、私自身もよく利用します。reqwest は非同期ランタイムに tokio を使用しています。

tide と合わせて「波乗り」になっているのがおもしろいですね。ネットサーフィンの surf から取ってきているのだと思います。

機能

対応している代表的な機能は、ドキュメントによると下記の通りです。

  • もちろんですが一通り HTTP メソッドに対応している。
  • TLS/SSL はデフォルトで対応している。
  • ストリーム処理に対応している。
  • Client インターフェースを介して接続を再利用できる。
  • Logger などのミドルウェアを拡張することができる。
  • HTTP/2 が標準で入っている。

今回は、下記の機能を試してみたいと思います。

準備

Cargo.toml に下記を追加します。

[dependencies]
surf = "1.0.3"
serde = { version = "1.0", features = ["derive"] }
async-std = { version = "1.5.0", features = ["attributes"] }

serde は Rust における JSON シリアライズ/デシリアライズ用のライブラリです。

また、async_std の attributes という features を今回は使用します。詳細は後ほど解説します。

GET リクエストを送る

README にしたがって、GET リクエストを送ってみましょう!

素直に書くと次のようになります。

use async_std::task;

fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
    task::block_on(async {
        let mut res = surf::get("https://httpbin.org/get").await?;
        dbg!(res.body_string().await?);
        Ok(())
    })
}

一方で、task::block_on は少しノイズが多いと感じるかもしれません。私は次のように記述するのが好きなので、以下では、task::block_on をすべて下記のコードのように読み替えていきます。

#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
    let mut res = surf::get("https://httpbin.org/get").await?;
    dbg!(res.body_string().await?);
    Ok(())
}

#[async_std::main] を利用して fn main()async fn main() に変更し、task::block_on ブロックをなくしました。

結果は次のようになりました。{deleted_by_author} は私が手を加えて消したものです。

❯❯❯ cargo run
   Compiling surf_example v0.1.0 (surf_example)
    Finished dev [unoptimized + debuginfo] target(s) in 1.64s
     Running `target/debug/surf_example`
[src/main.rs:4] res.body_string().await? = "{\n  \"args\": {}, \n  \"headers\": {\n    \"Accept\": \"*/*\", \n    \"Accept-Encoding\": \"deflate, gzip\", \n    \"Host\": \"httpbin.org\", \n    \"Transfer-Encoding\": \"chunked\", \n    \"User-Agent\": \"curl/7.64.1 isahc/0.7.6\", \n    \"X-Amzn-Trace-Id\": \"Root={deleted_by_author}\"\n  }, \n  \"origin\": \"{deleted_by_author}\", \n  \"url\": \"https://httpbin.org/get\"\n}\n"

JSON を送る POST リクエス

JSON を送ってみましょう。同様に README から拝借しています。

use serde::{Deserialize, Serialize};

#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
    #[derive(Deserialize, Serialize)]
    struct Ip {
        ip: String,
    }

    let uri = "https://httpbin.org/post";
    let data = &Ip {
        ip: "129.0.0.1".into(),
    };
    let res = surf::post(uri).body_json(data)?.await?;
    assert_eq!(res.status(), 200);

    let uri = "https://api.ipify.org?format=json";
    let Ip { ip } = surf::get(uri).recv_json().await?;
    assert!(ip.len() > 10);
    Ok(())
}

assertion が通れば、無事動作していると言えます。実際に動かしてみると、

~/dev/rust/surf_example via 🦀 v1.41.1
❯❯❯ cargo run
   Compiling surf_example v0.1.0 (surf_example)
    Finished dev [unoptimized + debuginfo] target(s) in 1.89s
     Running `target/debug/surf_example`
~/dev/rust/surf_example via 🦀 v1.41.1
❯❯❯

すごくわかりにくいかもしれませんが、assertion が落ちることなく確かに動いていました。

送った後にレスポンスボディを取り出すことも可能で、

use serde::{Deserialize, Serialize};

#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
    #[derive(Deserialize, Serialize, Debug)]
    struct Ip {
        ip: String,
    }

    let uri = "https://httpbin.org/post";
    let data = &Ip {
        ip: "129.0.0.1".into(),
    };
    let mut res = surf::post(uri).body_json(data)?.await?;
    let body = res.body_string().await?;
    println!("{}", body);

    Ok(())
}

上記のように書くと、実際のリクエストボディを取得することができます。今回はしませんでしたが、body_json() も実装されていて、serde を使って構造体に復元することも可能です。

❯❯❯ cargo run
   Compiling surf_example v0.1.0 (surf_example)
    Finished dev [unoptimized + debuginfo] target(s) in 1.83s
     Running `target/debug/surf_example`
{
  "args": {},
  "data": "{\"ip\":\"129.0.0.1\"}",
  "files": {},
  "form": {},
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "deflate, gzip",
    "Content-Type": "application/json",
    "Host": "httpbin.org",
    "Transfer-Encoding": "chunked",
    "User-Agent": "curl/7.64.1 isahc/0.7.6",
    "X-Amzn-Trace-Id": "Root={deleted_by_author}"
  },
  "json": {
    "ip": "129.0.0.1"
  },
  "origin": "{deleted_by_author}",
  "url": "https://httpbin.org/post"
}

ミドルウェア

ミドルウェアは少し大変です。今回は、自分の好きなロギングライブラリを処理の途中にはさむということをやってみたいと思います。

まず、Cargo.toml に下記のクレートを追加します。

[dependencies]
surf = "1.0.3"
serde = { version = "1.0", features = ["derive"] }
async-std = { version = "1.5.0", features = ["attributes"] }
log = "0.4.8" # ロギング関係のもの。追加。
simple_logger = "1.6.0" # ロギング関係のもの。追加。
futures = "0.3" # ミドルウェアを記述するために必要。追加。

Middleware というトレイトに、リクエスト時にログを書いて、リクエストを送って、かかった時間をロギングして返すというミドルウェアの実装を追加したいと思います。Middleware というトレイトは handle という関数が実装可能な状態になっていますね。

/// Middleware that wraps around remaining middleware chain.
pub trait Middleware<C: HttpClient>: 'static + Send + Sync {
    /// Asynchronously handle the request, and return a response.
    fn handle<'a>(
        &'a self,
        req: Request,
        client: C,
        next: Next<'a, C>,
    ) -> BoxFuture<'a, Result<Response, Exception>>;
}

書いてみます。

use futures::future::BoxFuture;
use log::{info, Level};
use std::time::Instant;
use surf::middleware::{HttpClient, Middleware, Next, Request, Response};
use surf::Exception;

struct HttpReqLogger;

impl<C: HttpClient> Middleware<C> for HttpReqLogger {
    fn handle<'a>(
        &'a self,
        req: Request,
        client: C,
        next: Next<'a, C>,
    ) -> BoxFuture<'a, Result<Response, Exception>> {
        Box::pin(async move {
            info!("Request: {}", req.uri());
            let now = Instant::now();
            let res = next.run(req, client).await?;
            info!("Request completed: {:?}", now.elapsed());
            Ok(res)
        })
    }
}

#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
    simple_logger::init_with_level(Level::Info).unwrap();
    surf::get("https://httpbin.org/get")
        .middleware(HttpReqLogger {})
        .recv_string()
        .await?;
    Ok(())
}

Box::pin[*1] の中に、ミドルウェアの処理を書いていきます。今回は simple_logger というクレートにロギングを行わせてみました。中でリクエストを投げてレスポンスを得るまでの時間経過をプリントしています。

これを実行してみました!

❯❯❯ cargo run
   Compiling surf_example v0.1.0 (surf_example)
    Finished dev [unoptimized + debuginfo] target(s) in 2.13s
     Running `target/debug/surf_example`
2020-02-29 15:09:33,558 INFO  [surf::middleware::logger::native] sending request
2020-02-29 15:09:33,559 INFO  [surf_example] Request: https://httpbin.org/get
2020-02-29 15:09:34,486 INFO  [surf_example] Request completed: 927.326608ms
2020-02-29 15:09:34,486 INFO  [surf::middleware::logger::native] request completed

期待通りの挙動を示していますね!

まとめ

  • surf という async_std ベースの HTTP クライアントライブラリがある。
  • HTTP クライアントとして使う際にやりたいことは、ドキュメントを読んだ感じでは一通り揃っている。

*1:Box::pin(std::pin::Pin)について本当に簡単にですが補足しておきましょう。async/await によって Rust にはジェネレータが投下されることになったのですが、その際自己参照する構造体を扱う必要が出てきました。しかし、Rust の move とは相性が悪く、普通に自己参照を move しようとすると、ポインタが古い領域を示してしまい、不正な状態に陥ることになるという問題がありました。それを解決するために導入されたのが、Pin という概念でした。詳しい解説はこちら

tide 0.6.0 を試してみる

tide 0.6.0 がリリースされていたようなので、使ってみます。リリースノートはこちらです。

Cookie、CORS と Route にいくつか新しい関数が足されたというリリースでした。Cookie と CORS はどちらも Web アプリを作る上では必須要件なので、追加されて嬉しいです。

使用する準備

tide を使えるようにしましょう。下記のように Cargo.toml を用意することで利用可能になります。

[package]
name = "tide-example"
version = "0.1.0"
authors = ["Author"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tide = "0.6.0"
serde = { version = "1.0", features = ["derive"] }

[dependencies.async-std]
version = "1.4.0"
features = ["attributes"]

Cookie

cookie クレートを使った Cookie 実装をできるようになりました。

まずは、cookie クレートを追加しましょう。ただ、完全に調べきれていない状態で書きますが、cookie の最新版である 0.13.3 を使用すると、Cookie 構造体のもつライフタイムが不整合になってしまっており、set_cookieremove_cookie といった関数でコンパイルエラーが発生します。したがって、0.12.0 を指定する必要がある点に注意が必要です *1

(...)
[dependencies]
tide = "0.6.0"
serde = { version = "1.0", features = ["derive"] }
+ cookie = { version="0.12.0", features = ["percent-encode"]}
(...)

今回は下記のようなエンドポイントを追加して実験するものとします。

  • GET /set: Cookie をセットするためのエンドポイントです。レスポンスヘッダに set-cookie が入っていることが期待値になります。
  • GET /remove: Cookie を削除するためのエンドポイントです。

たとえば下記のように実装できます。

use cookie::Cookie;
use tide::Response;

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    let mut app = tide::new();

    app.at("/set").get(|_req| async move {
        let mut res = Response::new(200);
        res.set_cookie(Cookie::new("tmp-session", "session-id"));
        res
    });
    app.at("/remove").get(|_req| async move {
        let mut res = Response::new(200);
        res.remove_cookie(Cookie::named("tmp-session"));
        res
    });
    app.listen("127.0.0.1:8080").await?;
    Ok(())
}

/set を実行すると、Cookie に期待通りのものが設定されていることが確認でき、

❯❯❯ curl localhost:8080/set -i
HTTP/1.1 200 OK
set-cookie: tmp-session=session-id
transfer-encoding: chunked
date: Sat, 08 Feb 2020 11:18:26 GMT

/remove を実行すると、一旦 200 OK が返ってきていることがわかります。ブラウザで挙動を確認したところ、正しく指定の Cookie が削除されていました。

❯❯❯ curl localhost:8080/remove -i
HTTP/1.1 200 OK
transfer-encoding: chunked
date: Sat, 08 Feb 2020 11:28:49 GMT

CORS

おなじみ CORS も実装できるようになりました。いつも Web をしていると通る道なので、実装が追加されて大変嬉しいです。

use http::header::HeaderValue;
use tide::middleware::{Cors, Origin};
use tide::Response;

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    let mut app = tide::new();

    let rules = Cors::new()
        .allow_methods(HeaderValue::from_static("GET, POST, OPTIONS"))
        .allow_origin(Origin::from("*"))
        .allow_credentials(false);

    app.middleware(rules);
    app.at("/portfolios").post(|_| async {
        Response::new(200)
    });

    app.listen("127.0.0.1:8080").await?;

    Ok(())
}

tide::middleware::Corstide::middleware::Origin を使って設定します。これは他の言語のライブラリとも特に使い心地に差はなく、CORS をすぐに実装できていいですね。

…と思ったんですが、

❯❯❯ cargo build
   Compiling tide-example v0.1.0 (/Users/a14926/dev/rust/tide-example)
error[E0277]: the trait bound `http::header::value::HeaderValue: std::convert::From<http::header::value::HeaderValue>` is not satisfied
  --> src/main.rs:22:10
   |
22 |         .allow_methods(HeaderValue::from_static("GET, POST, OPTIONS"))
   |          ^^^^^^^^^^^^^ the trait `std::convert::From<http::header::value::HeaderValue>` is not implemented for `http::header::value::HeaderValue`
   |
   = help: the following implementations were found:
             <http::header::value::HeaderValue as std::convert::From<&'a http::header::value::HeaderValue>>
             <http::header::value::HeaderValue as std::convert::From<http::header::name::HeaderName>>
             <http::header::value::HeaderValue as std::convert::From<i16>>
             <http::header::value::HeaderValue as std::convert::From<i32>>
           and 6 others
   = note: required because of the requirements on the impl of `std::convert::Into<http::header::value::HeaderValue>` for `http::header::value::HeaderValue`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `tide-example`.

To learn more, run the command again with --verbose.

コンパイルエラー😇 リリースノートに載っていたサンプルコードをそのままコピペしてビルドしてもやはりコンパイルエラーだったため、まずリリースノートのコードは合っていない気がします。これもあとで Issue 上げておこうかな…。もしわかる方いたら教えていただけますと🙏🏻

Nesting

エンドポイントをネストしやすくなりました。たとえば次のように実装すると、/api/v1/portfolios というエンドポイントを定義できます。

use tide::Response;

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    let mut portfolios = tide::new();
    portfolios.at("/portfolios").get(|_| async { Response::new(200) });

    let mut v1 = tide::new();
    v1.at("/v1").nest(portfolios);

    let mut api = tide::new();
    api.at("/api").nest(v1);

    api.listen("127.0.0.1:8080").await?;

    Ok(())
}

curl を投げると、正常に動作していることが確かめられました。

❯❯❯ curl localhost:8080/api/v1/portfolios -i
HTTP/1.1 200 OK
transfer-encoding: chunked
date: Sat, 08 Feb 2020 11:53:50 GMT

不具合と思われる事象については、これから調査してみます!

*1:これは原因がわかっていて、0.12.0 時点では Cookie<'static> を返す new 関数が生えていて、それを前提として set_cookie や remove_cookieCookie<'static> を受け取るように設計されていました。しかし、0.13.3 時点では new 関数は Cookie<'c> のライフタイムを返すようになっていて、ライフタイムの不整合が発生していますね。CookieBuilder を変更した関係でそうなったように読めます→https://github.com/SergioBenitez/cookie-rs/compare/0.12.0...0.13.0

【競プロノート】グラフを Rust で実装してみる

先日の Rust LT 会でグラフを実装してみる話がありました。そこではおそらく隣接リストを説明していたと思うのですが、そういえば Rust で実装してみたことはなかったなと思ったので、実装してみました。

『みんなのデータ構造』という本を参考にして実装しています。12章にグラフの章があります。何言語かで書かれたサンプルコードも存在します。

今回は、本書に掲載されていた下記のような「典型的な操作」を実装しました。今回はグラフ G = (V, E) を考えるものとします。一般的な話通りで、V は頂点で、E は辺です。

  • addEdge(i, j): 辺 (i, j) を E に加える
  • removeEdge(i, j): 辺 (i, j) を E から除く
  • hasEdge(i, j): (i, j) ∈ E かどうかを調べる
  • outEdges(i): (i, j) ∈ E を満たす整数整数 j のリストを返す
  • inEdges(i): (j, i) ∈ E を満たす整数整数 j のリストを返す

隣接行列

いきなりですが、隣接行列の実装は意外と大変です。Rust は、ポインタを使った操作 (参照先を書き換えてゴニョゴニョする操作) をしようとすると少々めんどくさくなるように言語が設計されています。隣接行列は行列 (2次元の配列) をもったデータ構造なのですが、その行列を状態とみなして書き換えていく操作を発生させるため、Rust 的にはとても面倒な実装になります。

今回実装した隣接行列は、(i, j) ∈ E のときに true となり、そうでない場合は false となる要素が含まれている行列です。

速度面などの細かい話を気にせず、RefCell を使用して実装してみました。ちなみに、std::mem::replace でも実装できると思いますし、実際 RefCell#replace はそれを使用して実装されているため、もしかすると RefCell を使用するのは大げさだったかもしれません。

use std::cell::RefCell;

pub struct AdjacencyMatrix {
    dimension: usize,
    matrix: Vec<Vec<RefCell<bool>>>,
}

impl AdjacencyMatrix {
    pub fn new(dimension: usize) -> AdjacencyMatrix {
        let mut matrix = vec![];
        for _ in 0..dimension {
            let mut tmp = vec![];
            for _ in 0..dimension {
                tmp.push(RefCell::new(false));
            }
            matrix.push(tmp);
        }

        AdjacencyMatrix { dimension, matrix }
    }

    pub fn dimension(&self) -> usize {
        self.dimension
    }

    fn get(&self, i: usize, j: usize) -> &RefCell<bool> {
        self.matrix.get(i).unwrap().get(j).unwrap()
    }

    pub fn add_edge(&self, i: usize, j: usize) {
        self.get(i, j).replace(true);
    }

    pub fn remove_edge(&self, i: usize, j: usize) {
        self.get(i, j).replace(false);
    }

    pub fn has_edge(&self, i: usize, j: usize) -> bool {
        *self.get(i, j).borrow()
    }

    pub fn in_edges(&self, i: usize) -> Vec<usize> {
        let mut edges = vec![];
        for j in 0..self.dimension {
            if *self.get(j, i).borrow() {
                edges.push(j);
            }
        }
        edges
    }

    pub fn out_edges(&self, i: usize) -> Vec<usize> {
        let mut edges = vec![];
        for j in 0..self.dimension {
            if *self.get(i, j).borrow() {
                edges.push(j);
            }
        }
        edges
    }
}

隣接リスト

逆に、隣接リストはスムーズに実装できます。これは、単純にエッジを追加する際にリストの末尾に値を入れていけばよいためです。

pub struct AdjacencyList {
    dimension: usize,
    list: Vec<Vec<usize>>,
}

impl AdjacencyList {
    pub fn new(dimension: usize) -> AdjacencyList {
        AdjacencyList {
            dimension,
            list: vec![vec![]],
        }
    }

    pub fn dimension(&self) -> usize {
        self.dimension
    }

    pub fn add_edge(&mut self, i: usize, j: usize) {
        if i > self.dimension() {
            panic!("add: greater than dimension");
        }

        match self.list.get_mut(i) {
            Some(v) => v.push(j),
            None => self.list.insert(i, vec![j]),
        }
    }

    pub fn remove_edge(&mut self, i: usize, j: usize) {
        match self.list.get_mut(i) {
            Some(v) => v.retain(|v0| *v0 != j),
            None => (),
        }
    }

    pub fn has_edge(&self, i: usize, j: usize) -> bool {
        match self.list.get(i) {
            Some(v) => v.contains(&j),
            None => false,
        }
    }

    pub fn in_edges(&self, i: usize) -> Vec<usize> {
        let mut edges = vec![];
        for j in 0..self.dimension() {
            if let Some(list) = self.list.get(j) {
                if list.contains(&i) {
                    edges.push(j);
                }
            }
        }
        edges
    }

    pub fn out_edges(&self, i: usize) -> Vec<usize> {
        match self.list.get(i) {
            Some(v) => v.to_vec(),
            None => vec![],
        }
    }
}

ポイントは、隣接リストの場合は初期状態の Vec<Vec<usize>> で、たとえば行を取得したい場合に、行が None として返ってきてしまうパターンが考えられるため、その点について留意しなが実装する必要があったという点です。Vector 関連でもっといい操作があれば知りたいところですが、思いつく限りでは None がどうしても発生してしまうため、都度パターンマッチして慎重に取り出す操作を行っています。

もっとも単純なグラフの実装がこれでわかりました。

2020年の Rust

2020年、Rust をどうしていくかというロードマップが例年通り策定されているようです。まだ草案なようなので変わる可能性がありそうですが、現時点での情報をまとめておこうかなと思います。

github.com

まとめると

  • Rust 2021 Edition への準備の年。
  • 現在進行中の作業をやりきる。
  • プロジェクトのガバナンスや可視性を改善する。

Rust 2021 Edition への準備

Rust では3年単位で「Edition」を変えることにしています。2018年に行われた Rust 2018 Edition が記憶に新しいかもしれません。2020年はその前年に当たるため、10月くらいに 2021 Edition で何をやるかについては決めきっておきたいようです。

2021 Edition ではどういった機能の変更が想定されているのでしょうか?草案に書いてある限りでは、

  • エラーハンドリング: 実は、Rust のエラーハンドリングにはデファクトに近い存在はあるものの、ユーザーは何を使うべきかを決めかねている状態が続いています。標準ライブラリでおそらくよいエラーハンドリングを選定し、実装することになるはずです。
  • 詳細は未定ですが trait に関する修正も入れたいようです: 個人的には async trait を有効にしていただけると大変幅が広がります。
  • &raw をはじめとする unsafe 周りの機能の改善: &raw&mut <place> as *mut&<place> as *const の対極に位置する予定の文法で、これが導入されると生ポインタを直接生成することができるようになります。

現在進行中の作業をやりきる

Rust のチーム的には、「ほとんど終わっているけれど完了ではない」作業が長く放置されていることを問題視しており、それを今年は、1つ1つ確実に完了させていきたいと思っているようです。たしかに、何年か前にステータスが変わらなくなり放置されている Issue などをたまに見かける気がします…。今年で多く片付くと嬉しいですね。

プロジェクトのガバナンスや可視性を改善する

3つくらいやりたいことがあげられていました。

  • 設計作業の状態の可視性とイニシアチブの取り方を改善したい。
  • メンタリングやリーダーシップ、それらに対する組織的な手助けの幅を広げたい。
  • 設計に関するディスカッションをより生産的にし、その作業によって消耗することのないようにしたい。

現在進行中の作業について、「どれが自分は手伝えそうか」といった話や、「X という機能の現在の進捗状態を知りたい」というのがとても把握しにくいといった問題があるようです。これは新規参画者だけでなく、結構コントリビュートしている人にとっても難しいらしく、そのあたりをまずは改善していきたいと考えているようです。ステータスを文章化してまとめたり、あるいは更新をもっと投稿するように作業時間を増やしていこうと考えているようです。

あとは単純にプロジェクトをリードする人や、新規のコントリビュータをメンタリングする人が足りていないようで、そのあたりの補充をしたいと考えているようです。

Rust は、RFC がユーザーに開かれています。誰でも議論に参加可能です。この形式は他言語にもだんだん浸透していっています。すばらしいものです。ただ、たとえば大規模で議論の余地がたくさんある設計に関する議論の際には、この仕組みがうまく機能しなくなりはじめているのではないかと見ているようです。今年は、RFC の議論周りの仕組みを改善していけるように、重きをおいて取り組んでいくことにしたようです。

直近の例だと async/await 導入あたりでの RFC の議論は、結構カオスになっていたように見えました。Rust のオープンな RFC システムはすばらしいとは思いますが、たしかにこのあたりは改善が必要かもしれませんね。期待。

余談: 2020年での日本の Rust コミュニティ

今年も Rust.Tokyo をやります。準備がんばります。今年も多くの人にお越しいただけますと嬉しいです。

感想

2018 年は 2018 Edition で忙しく、2019 年は async/await の導入でとても忙しかったようで、たしかに大量の「やりかけ RFC」が残っているようにも思います (しかも終わったのかステータスがわかりにくいのはたしかにそう…)。今年はそのあたりの機能がいくつか消化されるたり、あるいは RFC 管理が直感的にステータスを把握できるように改善されると期待できそうですね!

もし、英語の解釈が間違っているようでしたら教えていただけますと幸いです🙏🏻

作って学ぶ、cats.effect.IO モナドのしくみ

今回は cats.effect.IO (以下、 IO ) を理解する上で最低限必要となる*1実装を選定して、cats を参考に IO を実装してみました。具体的には、下記の機能を実装してみます。

  • IO#pure
  • IO#delay or IO#apply
  • IO#raiseError
  • IO#map
  • IO#flatMap
  • IO#unsafeRunSync

この記事のサンプルを実装し切ると、最終的にはたとえば次のようなコードが動きます。

object Main extends App {

  val io = for {
    num <- IO.pure(123)
    numStr <- IO.pure(num.toString)
    withHello <- IO.pure(s"${numStr}, Hello!")
    _ <- IO(println(withHello))
  } yield ()

    // "123, Hello!"
  io.unsafeRunSync()
}

逆に、紹介するにとどめて終わりそうな機能は下記です。

  • IO#async: 非同期処理を開始することができます。
  • IO#start: これを使用するとグリーンスレッドを扱うことができるようになります。実装的には、上記の async と、ExecutionContext の切り替え機能 (IO#contextShift) と、Fiber を実装すれば実現できます。

もちろん、IO では、ポーリング時に保有することになるスタックの大きさと深さの細かい管理が必要です。今回は、パフォーマンス面はまったく追求せず、単に IO の中身のコンセプトを理解するという点に主眼を置いています。

IO とは

遅延評価つき Future

IO は、もともとは Haskell にある概念です。IO[A] は、評価されるときに、A 型の値を返す前に副作用のある振る舞いをする計算であることを表現する型です。IO の値は純粋でイミュータブルであり、そのため参照透過です。

IO を使うことで、同期計算あるいは非同期計算を記述できます。for-yield でつないで書いていくことができますし、また内部処理をキャンセル可能にすることもできます。

もし、一般的に Scala で使用される概念である Future をご存知でしたら、IO は遅延評価つき Future といった対応関係にあります*2

ステートマシンである

実は、IO ( Future も) は、ステートマシンなのです*3。IO は、中のステート = 状態が受理になるまで遷移していき、受理になった時点で unsafeRunSync などの値を取り出す動作を行うと、値を取り出せるという仕組みになっています。

事実、cats の IO は下記のようなデータ構造をもっています。実際の IO.scalahttps://github.com/typelevel/cats-effect/blob/master/core/shared/src/main/scala/cats/effect/IO.scala#L1483

  private[effect] final case class Pure[+A](a: A)
    extends IO[A]

  private[effect] final case class Delay[+A](thunk: () => A)
    extends IO[A]

  private[effect] final case class RaiseError(e: Throwable)
    extends IO[Nothing]

  private[effect] final case class Suspend[+A](thunk: () => IO[A])
    extends IO[A]

  private[effect] final case class Bind[E, +A](source: IO[E], f: E => IO[A])
    extends IO[A]

  private[effect] final case class Map[E, +A](source: IO[E], f: E => A, index: Int)
    extends IO[A] with (E => IO[A]) {

    override def apply(value: E): IO[A] =
      new Pure(f(value))
  }

  private[effect] final case class Async[+A](
    k: (IOConnection, Either[Throwable, A]  => Unit) => Unit,
    trampolineAfter: Boolean = false)
    extends IO[A]

  private[effect] final case class ContextSwitch[A](
    source: IO[A],
    modify: IOConnection => IOConnection,
    restore: (A, Throwable, IOConnection, IOConnection) => IOConnection)
    extends IO[A]

多くの代数データがあります。cats では、同期処理であれば IORunLoop#step、非同期処理であれば IORunLoop#loop を使って、これらの代数データを遷移させていきます。これが、IO がステートマシンである所以なのです。

実装してみる

実装していきます。この IO は並行性を獲得できてはいませんが、IO の挙動の学習用には同期処理側を学ぶのが複雑性が少なく最適だと思うので、あえて実装していません。

用意した代数データ

今回は Pure, Delay, FlatMap, Map, そして RaiseError を切り出して用意しました。

object IO {
  final case class Pure[+A](a: A) extends IO[A]
  final case class Delay[+A](thunk: () => A) extends IO[A]
  final case class RaiseError(err: Throwable) extends IO[Nothing]
  final case class FlatMap[E, +A](original: IO[E], f: E => IO[A]) extends IO[A]
  final case class Map[E, +A](original: IO[E], f: E => A, index: Int)
      extends IO[A]
      with (E => IO[A]) {
    override def apply(value: E): IO[A] = Pure(f(value))
  }
}

https://github.com/yuk1ty/scratch-io-monad/blob/master/src/main/scala/effect/IO.scala

状態遷移を見てみる

今回は cats の IO を参考に、IOExecutor というクラスを実装してみました。というか、まるっと切り出しただけです。このクラスは、IO の状態遷移をかけつつ、状態に応じた関数適用や値の取り出しを逐次行っていくクラスです。

実装はこちらにあります→ https://github.com/yuk1ty/scratch-io-monad/blob/master/src/main/scala/effect/internal/IOExecutor.scala

IOExecutor#step と代数データを見比べながら、処理を少し理解してみましょう*4

登場人物を整理する

step 関数内の登場人物を少し整理します。

object IOExecutor {

  private[this] type Current = IO[Any]

  private[this] type Bind = Any => IO[Any]

  private[this] type CallBack = Either[Throwable, Any] => Unit

  private[this] type CallStack = mutable.ArrayStack[Bind]

  def step[A](source: Current): IO[A] = {
    var currentIO = source
    var bindFirst: Bind = null
    var bindRest: CallStack = null
    var hasUnboxed = false
    var unboxed: AnyRef = null
(...)
  • currentIO: 現在処理中の IO を指し示します。
  • bindFirst: 次にやってくる予定の処理を一時的に保管しておくための変数です。
  • bindRest: 2個、3個と bindFirst が積み上がることは容易に想像されます。それを保管するためのスタックを用意しています。
  • hasUnboxed: 中身が評価されたかどうかを制御しています。Delay か Pure に到達すると true になります。後続の処理で使用するためのフラグ。
  • unboxed: 評価された値を一時保存しておきます。

Pure に到達すると受理状態になる

IO は、状態遷移した後に Pure あるいは RaiseError に到達すると受理状態になります。つまり、この step 関数のなかで行っている処理は、再帰的に代数データの中身をたどっていき、Pure 状態 (例外時は RaiseError に寄せられます) に到達するまで状態遷移を繰り返していく処理です。

たとえば次のような IO の処理を組み立てたとしましょう。

for {
    num <- IO.pure(123)
    plusOne <- IO.pure(num + 1)
    plusThree <- IO.pure(plusOne + 3)
} yield plusThree

生成される代数データは次のようになっています。

FlatMap(Pure(123),Main$$$Lambda$2/892529689@6b9651f3)

中身について少し見ていきましょう。FlatMap と Delay で囲まれていることがわかります。FlatMap の代数データは、先ほどの代数データ一覧であったように、元のデータが左側に入っていて、これから連結したい対象の処理が右側に入っているという構図です。Main$$Lambda... については、左は「num + 1」、右は「plusOne + 3」が入っています。for-yield は flatMap のシンタックスシュガーなためです。flatMap の中身は Function ですよね。

この代数データに対して step 関数を適用して処理を実行していくと、次のような遷移をします。

// IO.pure(123) と IO.pure(num + 1) が入っている。
FlatMap(Pure(123),Main$$$Lambda$2/892529689@6b9651f3) 
// IO.pure(123) をたどっている。
Pure(123)
 // num + 1 適用後値と、IO.pure(plusOne + 3) が入っている。
FlatMap(Pure(124),Main$$$Lambda$6/1728790703@12f41634)
// IO.pure(123) をたどっている。
Pure(124) 
// plusOne + 3 をたどっている。
<function1>
// ↑が適用された 
Pure(127)
// 受理状態になった
Pure(127) 

Delay の挙動

公式ドキュメントに習って、IO は、遅延評価つきの IO であると先ほど紹介しました。その根幹を担うのがこの Delay という代数データです。IO#apply をすると基本的に Delay に状態遷移するようにできています。

すこしわかりにくいですが、Delay と Pure を織り交ぜて見ると、下記のような動きをします。

for {
    num <- IO.pure(123)
    plusOne <- IO(num + 1)
    plus3 <- IO(plusOne + 3)
} yield plus3
FlatMap(Pure(123),Main$$$Lambda$2/892529689@6b9651f3)
Pure(123)
FlatMap(Delay(Main$$$Lambda$6/1415157681@16f7c8c1),Main$$$Lambda$7/641853239@2f0a87b3)
Delay(Main$$$Lambda$6/1415157681@16f7c8c1)
<function1>
Delay(Main$$$Lambda$9/1338841523@b3d7190)
Pure(127)

Delay は、保持する値 (thunk) を評価せずに、一度遅延状態で保持する状態です。Delay は、step 関数に入れられてはじめて thunk の評価がかけられ、値が評価されていきます。

case Delay(thunk) => {
    Try {
       unboxed = thunk().asInstanceOf[AnyRef]
       hasUnboxed = true
       currentIO = null
    }.recover {
        case NonFatal(err) =>
          currentIO = RaiseError(err)
    }.get
}

unboxed に標準出力関数を噛ませて、中身の状態を見ていきましょう。

FlatMap(Pure(123),Main$$$Lambda$2/892529689@6b9651f3)
Pure(123)
FlatMap(Delay(Main$$$Lambda$6/1415157681@16f7c8c1),Main$$$Lambda$7/641853239@2f0a87b3)
Delay(Main$$$Lambda$6/1415157681@16f7c8c1)
delay unboxed: 124
<function1>
Delay(Main$$$Lambda$9/1338841523@b3d7190)
delay unboxed: 127
Pure(127)

きちんと関数適用が行われていたことがわかりました。

つなげる FlatMap

IO#flatMap をすると、FlatMap 状態に遷移します。この代数データの step 関数側の処理を見てみると、次のようなことをしています。

    do {
      currentIO match {
        case FlatMap(fa, bindNext) => {
          if (bindFirst != null) {
            if (bindRest == null) {
              bindRest = new mutable.ArrayStack()
            }
            bindRest.push(bindFirst)
          }

          bindFirst = bindNext.asInstanceOf[Bind]
          currentIO = fa
        }
(...)

FlatMap は一度パターンマッチに入ってくると、まず次に評価予定の内容をスタックの一番上に詰め込んでおきます。その後、現在の評価中 IO を自身のもともと評価予定だった IO に変え、次のループでその中身を処理していきます。これが、flatMap によって IO モナドがまるでひとつであるかのように連結されていくように見える所以です*5

flatMap を二重に重ねてしまう→「発火しない!」

実際にプロダクションで IO を使っていて、不意に実装中に flatMap が2回重なってしまって、IO が発火されずに困っているケースが散見されました。なぜこれが起きるかも、ステートマシンの動きを見ていくとわかります。

for {
    num <- IO.pure(123)
    plusOne <- IO(num + 1)
    plus3 <- IO(IO(plusOne + 3)) // 2回重ねてみた
} yield plus3

さて、ステートマシンの動きはどうなっているかというと…

FlatMap(Pure(123),Main$$$Lambda$2/892529689@6b9651f3)
Pure(123)
FlatMap(Delay(Main$$$Lambda$6/1415157681@16f7c8c1),Main$$$Lambda$7/641853239@2f0a87b3)
Delay(Main$$$Lambda$6/1415157681@16f7c8c1)
<function1>
Delay(Main$$$Lambda$9/1338841523@5d11346a)
Delay(Main$$$Lambda$11/2050404090@17211155)
Pure(Delay(Main$$$Lambda$11/2050404090@17211155))

Pure(Delay(...)) となってしまっていることがわかります。内側の Delay は関数適用が行われていないために評価されておらず、結果値を取り出せていません。

「そんなケースあるのか?」という話なのですが、プロダクションで見たケースとしては、たとえば IO(num + 1) の間に KVS への保存処理などを含んでいたとします。下記のように書いたとしましょう。

// num は外で定義されているものとします

// KvsRepository.scala
object KvsRepository {
    def insert(i: Int): IO[Unit] = IO(kvs.set(i)) // kvs への保存処理
}

// Main.scala
IO {
    KvsRepository.insert(num)
    num + 1
}.unsafeRunSync()

この場合は発火しませんね。なぜなら、KvsRepository 内の処理は Delay になっているからです。したがって、この insert は発火されず、num + 1 の結果だけが返ります。

IO の使い所の注意としては、きちんと最終的に Pure 状態になるようにコンビネータをつなげ続ける必要があるという点です。先ほどの KVS への保存の例のコードを修正するとしたら、次のようになるでしょう。

// num は外で定義されているものとします
val task = for {
    _ <- KVSRepository.insert(num)
    n <- num + 1
} yield n

task.unsafeRunSync()

発火しない問題には注意が必要です。が、かといって、副作用を含む処理を IO#pure に入れるのは、たしかに即時評価を起こせますが IO の本来の使い方や目的とは違っています。なので、面倒臭がらずに、きちんとコンビネータをつなげることを私はおすすめします。発火しない問題が起きた際には、一度 IO モナドを print するなどして、現在の代数データの状況がどうなっているかを確認するとよいです。

まとめ

  • IO はステートマシンである。
  • このステートマシンは、Pure か RaiseError になると受理状態になる。

リポジトリ

今回記事の長さの都合 (とわたしの気力の都合) でこの記事では解説しきれなかった、例外処理側や step 関数の実装全体もこのリポジトリに置いてあります。本当は RaiseError 時に復旧手段として用意されている IOFrame も解説するべきだったかもしれません…。

実装そのものはそこまでハードでもないですし、IO モナドを深く理解したい方には一度フルスクラッチしてみるのはとてもおすすめだと私は思いました。もともとは会社のチームメンバーのみなさんに使ってもらえるようにリポジトリを用意しましたが、2020年のチャレンジにどうぞ。

github.com

参考

*1:というか、私がよく使っているだけとも言うけれど

*2:ちなみに Future パターンに関する解説はこちらに詳しく載っています。

*3:この話については、この記事がとてもわかりやすいです→ステートマシン抽象化としてのFuture | κeenのHappy Hacκing Blog

*4:実装を見ると、再代入をかなり繰り返しますし、かつ Any や AnyRef などを多用していて型安全性も少し低めです。再代入を繰り返しているのは、パフォーマンス面を考慮してのことではないかと思いました。また、Any や AnyRef を全廃しようとすると、多くの型に対して個別実装を生やす必要が出てきて、実装の手間が膨大になりそうだなと思いました。それに加えて、Any(Ref) の範囲はこの IO 内に限られていて外には露出していませんので、問題ではないのかもしれませんね。

*5:このスタックの深さは、効率よく制御しないとパフォーマンスネックになる可能性があると想像できます。今回の実装では Scala の標準でついている ArrayStack を使用しましたが、cats では独自に Stack 実装を用意してありました。

async/await 時代の新しい HTTP サーバーフレームワーク tide を試す

Rust Advent Calendar 2019 25日目の記事です。

tide は現在開発途中の、 Rust の async/await に対応した HTTP サーバーを構築するフレームワークです。not ready for production yet なので本番にこれを使用するのは難しいかもしれませんが、いろいろな例を見てみた感じとても使いやすそうで、注目に値するフレームワークの一つです。

記事を少し読んでみたのですが、どうやら 2018 年に Rust の Network Service Working Group が開発に着手したフレームワークのようですね。現在のステータスを追いかけていないので詳しくはわかりませんが、Rust チームの方々が何かしら関わっているフレームワークということで、少し安心感がもてるかなと私は思っています。async/await が今年無事安定化されたので、一層開発が進んでくれると嬉しい…そんなフレームワークです。

GitHubリポジトリはこちら。

github.com

また、開発者の方の Twitter はこちら。時々 tide に関する最新情報が流れてくるので、tide がどういう状況かを逐次キャッチアップしたい方はフォローしておくとよいと思います🙂

twitter.com

今回はそんな tide を少し触ってみたので、解説記事を書いておきたいと思います*1

実行 OS は macOS version 10.14 です。また、テンプレ用のリポジトリも用意しました→GitHub - yuk1ty/tide-example: A build template for tide

Hello, World してみる

まずは GitHub のサンプルを写したらいけるだろうということで、README.md のものをそのままローカルに落としてきて Hello, World してくれる API を用意してみましょう。Cargo.toml に tide の依存を追加します。その後、下記のように書きます。

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    let mut app = tide::new();
    app.at("/").get(|_| async move { "Hello, World!" });
    app.listen("127.0.0.1:8080").await?;
    Ok(())
}

ただ…このまま実行すると、次のようなコンパイルエラーに見舞われました。

$ cargo build
   Compiling tide-example v0.1.0 (/Users/xxx/dev/rust/tide-example)
error[E0433]: failed to resolve: use of undeclared type or module `async_std`
 --> src/main.rs:1:3
  |
1 | #[async_std::main]
  |   ^^^^^^^^^ use of undeclared type or module `async_std`

error[E0277]: `main` has invalid return type `impl std::future::Future`
 --> src/main.rs:2:20
  |
2 | async fn main() -> Result<(), std::io::Error> {
  |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^ `main` can only return types that implement `std::process::Termination`
  |
  = help: consider using `()`, or a `Result`

error: aborting due to 2 previous errors

どうやら、#[async_std::main] が存在しないと怒られてしまっています。それに伴って、async fn main() が返す結果が impl std::future::Future となってしまっており、エラーがもうひとつ発生しています。しかし、おそらく #[async_std::main] が存在しないために起きている事象なはずですので、そちらを解決することに専念しましょう。

async-std とは?

#[async_std::main] ですが、 Rust チームが鋭意開発中の非同期処理基盤 async-std に入っています。

async-std は、Go のランタイムのタスクスケジューリングアルゴリズムやブロッキング戦略を Rust に導入したライブラリです。非同期処理のランタイムには tokio をはじめいくつか種類がありますが、そのうちのひとつが async-std です*2。余談ですが Rust.Tokyo でキーノートをしてくれた Florian が開発に携わっていますね!

この crate に含まれる #[async_std::main] アトリビュートを追加すると、async fn main() -> Result<...> と宣言できるようになり、アプリケーションを非同期処理のランタイムに乗せられます。つまり、tide は async-std 上に乗って動いているということでもあります。

github.com

Cargo.toml に設定を追加する

なお、この #[async_std::main] ですが、async-std の attribute feature を有効にしてライブラリとして追加する必要があるようです。したがって、使用したい場合には自身で下記の記述を追加する必要があります。

[dependencies.async-std]
version = "1.4.0"
features = ["attributes"]

HTTP サーバーが起動する

この状態で走らせると…

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.33s
     Running `target/debug/tide-example`
Server is listening on: http://127.0.0.1:8080

無事にサーバーが起動しました!curl で GET リクエストを送ってみましょう。すると…

$ curl localhost:8080
Hello, World!

Hello, World! と返ってきます。これでようやく動作確認が完了しました。

ルーティングをちょこっと紹介

私が気になった機能をピックアップして試していきます。

/hc に GET を投げると 200 OK を返す

よく実装するヘルスチェック機構を実装しましょう。/hc に対して GET リクエストを送ると 200 OK を返させます。これには tide::Response を利用します。

use tide::Response;

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    let mut app = tide::new();
    app.at("/hc").get(|_| async move {
        Response::new(200)
    });
    app.listen("127.0.0.1:8080").await?;
    Ok(())
}

curl を投げて確認してみましょう。

$ curl --dump-header - localhost:8080/hc
HTTP/1.1 200 OK
transfer-encoding: chunked
date: Tue, 24 Dec 2019 08:01:19 GMT

想定通り、200 OK が返ってきていますね。Response の実装を少し読んでみましたが、Web アプリを作る際に欲しい機能は一通り用意されているようでした。

複数エンドポイントを作ってみる――罠。

後ほど使用するために、/json というエンドポイントを用意してみましょう。

use tide::Response;

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    let mut app = tide::new();
    app.at("/hc").get(|_| async move { Response::new(200) });
    app.at("json").get(|_| async move { "OK" });
    app.listen("127.0.0.1:8080").await?;
    Ok(())
}

これで、curl を叩いてみます。

$ curl localhost:8080/json
OK

大丈夫ですね!👏 ただ、ちょっとハマったポイントがありました。普通はやらないのかもしれませんが、app.at(...).get(...).at(...).get(...) といった形で、実はメソッドチェーンが可能です。

use tide::Response;

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    let mut app = tide::new();
    app.at("/hc")
        .get(|_| async move { Response::new(200) })
        .at("/json")
        .get(|_| async move { "OK" });
    app.listen("127.0.0.1:8080").await?;
    Ok(())
}

よさそうに見えます。コンパイルも通りました。/hc というエンドポイントと、/json というエンドポイントが作られるだろうと期待しています。curl を叩いてみます。

$ curl --dump-header - localhost:8080/json
HTTP/1.1 404 Not Found
transfer-encoding: chunked
date: Tue, 24 Dec 2019 09:04:21 GMT

えっ…消えました…。もしかして、と思って次のような curl を叩いてみました。

$ curl --dump-header - localhost:8080/hc/json
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
transfer-encoding: chunked
date: Tue, 24 Dec 2019 09:04:55 GMT

OK

なるほど、つまりメソッドチェーンをすると、チェーンした分だけ配下にどんどんパスが切られていってしまうのでしょう。これはちょっと微妙なデザインだと思いました。メソッドチェーンをしたとしても、第一階層にパスが追加され続けるというデザインが直感的なように私には思えました。あるいは、メソッドチェーン自体を禁止される形式がよいのかもしれません。

nest

ちなみに、もしルートをグループ化して使用したい場合には nest という関数が使えます。RubySinatra や Go の echo などのご存知の方は、ああいった namespaceGroup 関数のようなものが使えるイメージです。たとえば、/api/v1 という親ルートの中に、/hc/endpoint という子ルートを用意したい場合には、次のように記述できます。

use tide::Response;

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    let mut app = tide::new();
    app.at("/api/v1").nest(|router| {
        router.at("/hc").get(|_| async move { Response::new(200) });
        router
            .at("/endpoint")
            .get(|_| async move { "nested endpoint" });
    });
    app.listen("127.0.0.1:8080").await?;
    Ok(())
}

curl で確認してみると、しっかり指定したルートで登録されていました!

curl -i localhost:8080/api/v1/hc
HTTP/1.1 200 OK
transfer-encoding: chunked
date: Tue, 24 Dec 2019 09:21:56 GMT
curl -i localhost:8080/api/v1/endpoint
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
transfer-encoding: chunked
date: Tue, 24 Dec 2019 09:23:25 GMT

nested endpoint

JSON を含むリクエストを投げる

公式ドキュメントに載っているコードを拝借して、JSON をボディに含むリクエストを受け取り、結果を同様に JSON で返す処理を書き足してみます。ここで、Cargo.toml に serde への依存を追加しつつ…

use serde::{Deserialize, Serialize};
use tide::{Request, Response};

#[derive(Debug, Deserialize, Serialize)]
struct Counter {
    count: usize,
}

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    let mut app = tide::new();
    app.at("/hc").get(|_| async move { Response::new(200) });
    app.at("/json").get(|mut req: Request<()>| {
        async move {
            let mut counter: Counter = req.body_json().await.unwrap();
            println!("count is {}", counter.count);
            counter.count += 1;
            tide::Response::new(200).body_json(&counter).unwrap()
        }
    });
    app.listen("127.0.0.1:8080").await?;
    Ok(())
}

curl を投げてみます。カウントが1のリクエストを投げたので、カウントが2になって JSON 形式で返ってきてくれれば成功です。

$ curl -i -H "Accept: application/json" -H "Content-type: application/json" -d '{ "count": 1 }' -X GET localhost:8080/json
HTTP/1.1 200 OK
content-type: application/json
transfer-encoding: chunked
date: Tue, 24 Dec 2019 09:11:42 GMT

{"count":2}

成功しました。

ちょっと小話

Request-Response

tide のデザインの特徴として、Request-Response 方式を採用している点があげられています。関数のシグネチャは非常にシンプルな構成で、Request を引数に受け取り、Response (を含む Result 型) を返すという構成になっています。

async fn endpoint(req: Request) -> Result<Response>;

これまでは Request と Response のライフサイクルの管理は Context を用いて行っていましたContext を受け取り、その中からリクエストの実体を取り出す構成でしたが、バージョン 0.4.0 になって変更が加えられました

State

State というのはミドルウェアがエンドポイント間で値をシェアするために使用されるものです。actix-web や Rocket でも確かあった機能だったかなと記憶しています。State 付きでサーバーを起動する際には、先ほどのように tide::new() するのではなく、 tide::with_state() する必要があります。サンプルコードを載せておきます。

struct State {
    name: String,
}

async fn main() -> Result<(), std::io::Error> {
    let state = State {
        name: "state_test".to_string(),
    };
    let mut app = tide::with_state(state);
    app.at("/hc").get(|req: Request<State>| {
        async move {
            tide::Response::new(200)
        }
    });
}

重要なことは、

  • app の型はもともと Server<()> だったものから、Server<State> に変わっている。
  • get の部分については、Request<()> だったものから、 Request<State> に変わっている。

点です。

Extension Traits

Request や Response といった構造体を型クラスを用いて拡張することもできます。ちょっとした処理を付け足したい際に便利ですね。たとえば、次のようにヘルスチェックすると「OK」と body に入れて返すエンドポイントを、Extension Traits を用いて実装してみます。

use tide::{Request, Response};

trait RequestExt {
    fn health_check(&self) -> String;
}

impl<State> RequestExt for Request<State> {
    fn health_check(&self) -> String {
        "OK".to_string()
    }
}

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    let mut app = tide::new();
    app.at("/hcext")
        .get(|req: Request<()>| async move { req.health_check() });
    app.listen("127.0.0.1:8080").await?;
    Ok(())
}

curl を投げると、上手に動作していることがわかります。

$ curl -i localhost:8080/hcext
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
transfer-encoding: chunked
date: Tue, 24 Dec 2019 09:32:16 GMT

OK

ルーティング応用編

最後に、実アプリケーションであれば必須な機能2つを試してみましょう。パラメータとクエリパラメータです。

パラメータを取得する

たとえばよくやる手として、id = 1 の user というリソースから1つ、user を取得したいというユースケースがあります。これももちろん実装されていました。/users/:id というエンドポイントを用意したくなったら、次のように実装すれば実現できます。

use tide::{Request, Response};

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    let mut app = tide::new();
    app.at("/users/:id").get(|req: Request<()>| {
        async move {
            // 型注釈は必須の模様。つけないと、推論に失敗する。
            let user_id: String = req.param("id").client_err().unwrap();
            Response::new(200).body_string(format!("user_id: {}", user_id))
        }
    });
    app.listen("127.0.0.1:8080").await?;
    Ok(())
}

curl を投げてみましょう。

$ curl -i localhost:8080/users/1
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
transfer-encoding: chunked
date: Tue, 24 Dec 2019 13:12:47 GMT

user_id: 1

期待したとおり、:id に含まれていた 1 という値を返してくれています。正常に動作しているとわかりました。

クエリパラメータを扱う

パラメータが使えるのならば、きっとクエリパラメータも使えてほしいはずです。あります。クエリの方は、パース先の構造体を用意しておいて (サンプルコードの QueryObj)、serde の Deserialize を derive しておくと、あとは勝手に値をパースして構造体に埋めてくれる機能が備わっています。

今回期待する内容は、/users?id=1&name=helloyuki というリクエストを投げると、id と name を返してくれるエンドポイントができていることです。

use serde::Deserialize;
use tide::{Request, Response};

#[derive(Debug, Deserialize)]
struct QueryObj {
    id: String,
    name: String,
}

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    let mut app = tide::new();
    app.at("/users").get(|req: Request<()>| {
        async move {
            let user_id = req.query::<QueryObj>().unwrap();
            Response::new(200).body_string(format!(
                "user_id: {}, user_name: {}",
                user_id.id, user_id.name
            ))
        }
    });
    app.listen("127.0.0.1:8080").await?;
    Ok(())
}

curl を同様に投げてみましょう。

curl -i 'localhost:8080/users?id=1&name=helloyuki'
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
transfer-encoding: chunked
date: Tue, 24 Dec 2019 13:54:29 GMT

user_id: 1, user_name: helloyuki

よさそうです!一通り、ルーティングに欲しい機能が備わっていましたね。

その他

その他、actix-web や Rocket のように、ミドルウェアを注入する機能も存在しています。今日は入門にしては記事が長くなってしまうのでここにとどめておきますが、詳しいサンプルは下記のリポジトリにあります。

github.com

まとめ

  • tide という async-std ベースの HTTP サーバーフレームワークがあります。
  • 今回はルーティングに限ってご紹介しましたが、ルーティングについては必要最低限の機能が揃っていそうです。
  • まだまだ開発途中なので、プロダクションに使うのは難しいかもしれません。
  • 今後の開発の進捗に期待!
  • また、ビルド用のテンプレートを用意したリポジトリも作っておきましたので、ぜひ上のコードをコピペして遊んでみてください!github.com
  • みなさま良いお年を!

*1:ちなみに、この記事は2019年12月25日時点での tide の概況についてのものであり、今後 tide のデザインは大きく変わっている可能性があります。直近でもまずまず大きな変更が加えられているなど、開発は活発であるもののまだまだ安定していない状態といった感じでしょうか。

*2:Rust の非同期処理基盤について詳しく知りたい方は、こちらの記事がおすすめです: https://tech-blog.optim.co.jp/entry/2019/11/08/163000