🦀

100日後にRustをちょっと知ってる人になる: [Day 52]Wasm Workers Server と KVS

2022/10/21に公開

Day 52 のテーマ

Day 50 そして Day 51Wasm Workers Server の最もシンプルな基本動作をするアプリケーションについて眺めてみました。

とは言え、基本動作の中でもまだふれていない機能が Wasm Workers Server にはあります。標準機能として、キー/バリューのインメモリストアを提供しています。

今日は、その キー/バリューストアについて見てみたいと思います。

キー/バリューストア

キー/バリューストアとは、文字通りキーとそのキーに対する値からなるデータ構造を保管するためのストレージです。ハッシュテーブルとしてよく知られているデータ構造ですよね。Redis, MemcachedMongoDB などが有名ですね。

さて、Wasm Workers Serverで提供しているキー/バリューストアは、Wasm Wokers Server がまだまだ進化中ということもあり有名な NoSQL のようなリッチな機能は搭載していません。そこは今後に期待ですね。
現在のところは、メモリ上にデータを保管し、再起動するたびにデータ内容はクリーンアップされるようになっています。永続化はまだできません。
制約事項としても予め言われていることがあります:
同時リクエストで同じネームスペースに書き込む場合にデータがオーバーライドされるということです。これは、将来的にはデータ統合の仕組みや別のデータ保存の仕組みによる実装で、大量の書き込みに対しても対応していく予定になっています。

しかし、とは言ってもシンプルながらもこのキー/バリューストアがあることによって、異なるワーカー同士からの読み書きをキー/バリューストア介して行うことができるようになります。

キー/バリューストアを用いたワーカー

それでは、キー/バリューストアを用いたワーカーを作成してみようと思います。チュートリアルに即して作るだけなのですけれどね…

Dependencies

先日の超シンプルなレスポンスをかえすだけのアプリケーションの時と同じく以下を Dependency に追加します。

  • anyhow = "1.0.65"
  • wasm-workers-rs = { git = "https://github.com/vmware-labs/wasm-workers-server/" }

Reply 関数

受け取ったリクエストに対してレスポンスを返すだけの reply 関数を定義します。

use anyhow::Result;
use wasm_workers_rs::{handler, http::{self, Request, Response}, cache::Cache};

#[handler(cache)]
fn reply(req: Request<String>, cache: &mut Cache) -> Result<Response<String>> {
    Ok(http::Response::builder()
        .status(200)
        .header("x-generated-by", "wasm-workers-server")
        .body(String::from("Hello Wasm!").into())?)
}

Day 50 の時と異なるのは次の 1点です。

今回:

#[handler]
fn reply(req: Request<String>)

前回:

#[handler(cache)]
fn reply(req: Request<String>)

ハンドラマクロのパラメータに cache 属性を付けるかどうかです。

Cache

Cache を見てみます。

use std::collections::HashMap;
pub type Cache = HashMap<String, String>;

文字通り、HashMap データ構造を提供しているものでした。

キー/バリューストアを用いたレスポンス

キー/バリューストアを用いてカウントアップを行います。
まず、hashmap のキーとして、counter という文字列を使うことにします。

let count = cache.get("counter");

次に、列挙型な Option 型を利用します。Option 型は値が存在しないかもしれないときに使用する列挙型です。
定義は次のようになっています。

enum Option<T> {
    None,
    Some(T),
}

T 型 (今回の例では String 型) が存在するときには、Some でラップします。
一方で値が存在しない場合は、None になります。

let count_num = match count {
    Some(count_str) => count_str.parse::<u32>().unwrap_or(0),
    None => 0,
};

Some(count_str) => count_str.parse::<u32>().unwrap_or(0) では、カウント数の文字列を u32 な数値にパースし、unwrap して値を取り出しています。ここで、デフォルト値としては、0 を与える unwrap_or() メソッドが使用されています。

取り出した値は、reply 関数が呼ばれるたびにカウントアップしています。

cache.insert("counter".to_string(), (count_num + 1).to_string());
ソースコード全量
use anyhow::Result;
use wasm_workers_rs::{handler, http::{self, Request, Response, response}, cache::Cache};

#[handler(cache)]
fn reply(req: Request<String>, cache: &mut Cache) -> Result<Response<String>> {

    let count = cache.get("counter");
    let count_num = match count {
        Some(count_str) => count_str.parse::<u32>().unwrap_or(0),
        None => 0,
    };
    let response = format!(
        "<!DOCTYPE html>
<body>
<h1>キー/バリューストア in Rust</h1>
<p>カウンター: {}</p>
<p>This page was generated by a Wasm modules built from Rust.</p>
</body>",
        count_num
    );

    cache.insert("counter".to_string(), (count_num + 1).to_string());

    Ok(http::Response::builder()
        .status(200)
        .header("x-generated-by", "wasm-workers-server")
        .body(String::from("Hello Wasm!").into())?)
}

Wasm のコンパイル

Wasm イメージを以下の cargo コマンドでコンパイルします。

cargo build --target wasm32-wasi --release

コンパイルが終了すると、target/wasm32-wasi/release/ ディレクトリの配下に Wasm イメージが出力されています。

ls -l target/wasm32-wasi/release/
drwxr-xr-x   5 yanagiharas  staff      160 Oct 21 13:02 build/
-rw-r--r--   1 yanagiharas  staff      207 Oct 21 13:02 day_52_wasm-worker-kv.d
-rwxr-xr-x   1 yanagiharas  staff  2216591 Oct 21 13:02 day_52_wasm-worker-kv.wasm*
drwxr-xr-x  31 yanagiharas  staff      992 Oct 21 13:02 deps/
drwxr-xr-x   2 yanagiharas  staff       64 Oct 21 13:02 examples/
drwxr-xr-x   2 yanagiharas  staff       64 Oct 21 13:02 incremental/

この、target/wasm32-wasi/release/ ディレクトリの配下に TOML ファイルを作成します。

target/wasm32-wasi/release/day_52_wasm-worker-kv.toml

name = "day_52_wasm-worker-kv"
version = "1"

[data]
[data.kv]
namespace = "day_52_wasm-worker-kv"

この TOML ファイル名はハンドラファイル名と同じにしておく必要があるようです。

Wasm Workers Server の起動

生成された Wasm イメージのディレクトリまで移動し、Wasm Workers Server を起動します。

 cd target/wasm32-wasi/release
 wws .
⚙️  Loading routes from: .
🗺  Detected routes:
    - http://127.0.0.1:8080/day_52_wasm-worker-kv
      => day_52_wasm-worker-kv.wasm (handler: day_52_wasm-worker-kv)
    - http://127.0.0.1:8080/deps/day_52_wasm_worker_kv-78cdb3ea12732eda
      => deps/day_52_wasm_worker_kv-78cdb3ea12732eda.wasm (handler: default)
🚀 Start serving requests at http://127.0.0.1:8080

検知されたエンドポイント (http://127.0.0.1:8080/day_52_wasm-worker-kv) に対してアクセスを複数回してみます。

curl http://127.0.0.1:8080/target/wasm32-wasi/release/day_52_wasm-worker-kv
<!DOCTYPE html>
<body>
<h1>キー/バリューストア in Rust</h1>
<p>カウンター: 1</p>
<p>This page was generated by a Wasm modules built from Rust.</p>
</body>
<!DOCTYPE html>
<body>
<h1>キー/バリューストア in Rust</h1>
<p>カウンター: 2</p>
<p>This page was generated by a Wasm modules built from Rust.</p>
</body>
<!DOCTYPE html>
<body>
<h1>キー/バリューストア in Rust</h1>
<p>カウンター: 3</p>
<p>This page was generated by a Wasm modules built from Rust.</p>
</body>

:
:

期待通りカウントアップされていることが分かりました。

Day 52 のまとめ

Wasm Workers Server で標準で提供している機能のキー/バリューストアについてサンプルアプリケーションを作りながら見てみました。ユースケースを妄想すると、Wasm Workers Server のようなイベント・ドリブンで動作するサーバーレスタイプのアプリケーションの場合、処理結果を一時的にどこかに保管しておくことが出来ると便利だというのは容易に想像できると思います。
Wasm のアプリケーションプラットフォームでもそれが出来るように、まず最初に対応させて来たのは今後の方向性を少し想像と妄想ができるかなと感じました。
とは言え、まだまだ発展途上な OSS なのでキー/バリューストアの機能に関してもさらなる進化を期待したいかなと思いました。

GitHubで編集を提案

Discussion