kevy4.0

非同期クライアント

kevy-client-asyncは、ブロッキング版kevy-clientの非同期ミラーです。API面もURLファサードも同じで、各呼び出しに.awaitが付くだけです。

このドキュメントが必要になるとき

アプリがすでにtokiosmolasync-stdのいずれかのランタイム上で動いていて、awaitのフローを端から端まで通したい(ブロッキングスレッドプールへのホップ、spawn_blockingによるラップ、コネクションごとのスレッドをなくしたい)場合に、非同期クライアントを使ってください。コードパスが通常スレッド上のリクエスト・レスポンスであれば、ブロッキングクライアントのほうがシンプルかつ低遅延です。同期でいることに非同期税はかかりません。

中心となる考え方

Cargoフィーチャでランタイム(tokiosmolasync-std)をちょうど1つ選びます。クレートはそのランタイムのTcpStreamアダプタだけにコンパイルされ、ほかは一切含まれません。公開APIはブロッキングクライアントを1:1でミラーしているため(AsyncConnection::connect(url).await?conn.set(k, v).await?conn.get(k).await?)、ブロッキングからの移植はConnectionAsyncConnectionに替えて各呼び出しに.awaitを付けるだけで済みます。レイテンシが問題になる場面では、pipelineビルダーがNコマンドを1回のTCP往復にまとめます。

動かしてみる例

Tokio

[dependencies]
kevy-client-async = { version = "1", features = ["tokio"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] }
use kevy_client_async::AsyncConnection;

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut conn = AsyncConnection::connect("tcp://127.0.0.1:6004").await?;
    conn.set(b"k", b"v").await?;
    let v = conn.get(b"k").await?;
    assert_eq!(v.as_deref(), Some(&b"v"[..]));
    Ok(())
}

Smol

コードは同じで、ランタイムのフィーチャだけ入れ替えます。

[dependencies]
kevy-client-async = { version = "1", features = ["smol"] }
smol = "2"
use kevy_client_async::AsyncConnection;

fn main() -> std::io::Result<()> {
    smol::block_on(async {
        let mut conn = AsyncConnection::connect("tcp://127.0.0.1:6004").await?;
        conn.set(b"k", b"v").await?;
        let v = conn.get(b"k").await?;
        assert_eq!(v.as_deref(), Some(&b"v"[..]));
        Ok(())
    })
}

Pipelineビルダー

バッチ全体を1往復で送ります。応答はキューに入れた順に返り、コマンド単位の失敗はバッチを壊さず、Vecの中にReply::Error(_)として現れます。

use kevy_client_async::AsyncConnection;

let mut conn = AsyncConnection::connect("tcp://127.0.0.1:6004").await?;
let replies = conn
    .pipeline()
    .set(b"a", b"1")
    .get(b"a")
    .incr(b"hits")
    .run(&mut conn)
    .await?;
// replies.len() == 3; キューしたコマンドごとに1つのReply、順序どおり。

ランタイムフィーチャ

以下のうちちょうど1つを有効にする必要があります。フィーチャなし、あるいは2つ以上の同時有効はコンパイルエラーになります。暗黙のデフォルトはありません。

フィーチャトランスポートアダプタ取り込まれるランタイムクレート
tokiotokio::net::TcpStreamtokio
smolsmol::net::TcpStreamsmol
async-stdasync_std::net::TcpStreamasync-std

各ランタイムクレートはdefault-features = falseに、アダプタに必要な最小限のフィーチャだけを足して取り込まれます。kevyワークスペースのcrates.io依存はこれらだけです。純Rust・ゼロ依存ルールに対する意図的な例外であり、Rustのasyncエコシステムにはstdだけで成立する基盤が存在しないための措置です。

URLバックエンド

AsyncConnection::connectはブロッキングクライアントと同じURLファサードを受け取ります。TCP系のスキームはランタイムの非同期ソケットを通り、プロセス内スキームは拒否されます(プロセス内アクセスはブロッキングクライアントのほうが厳密に速く、エグゼキュータを経由させる意味がないためです)。

スキーム接続先非同期クライアントの対応
tcp://kevyまたはRedis互換サーバー対応
kevy://kevyサーバー(tcp://のエイリアス)対応
redis://RedisまたはRedis互換サーバー対応
mem://プロセス内組み込みストア非対応(ブロッキング版を使用)
file:///オンディスク組み込みストア非対応(ブロッキング版を使用)

mem://file:///のURLをAsyncConnection::connectで開くとErrorKind::Unsupportedが返ります。

トレードオフ

ブロッキングクライアントがデフォルトであり、今後もデフォルトであり続けるのには理由があります:

周囲のアプリがすでにasyncならasyncを使う。独立したコマンドのバッチがあって往復がボトルネックならpipelineビルダーを使う。それ以外はブロッキングのままにしておいてください。

FAQ

なぜランタイムをちょうど1つ選ぶ必要があるのですか? クレートは単一のTcpStreamアダプタとしてコンパイルされます。1つのバイナリにアダプタを2つ入れると、I/Oごとにランタイム非依存の間接層を挟む(オーバーヘッド)か、誰にも保守できない巨大なcfgマトリクスを抱えるかのどちらかになります。アダプタ0個では公開型が未実装のまま残ります。フィーチャ数をコンパイル時に検査することで、設定ミスを早い段階ではっきり検出できます。

同期と非同期のkevyクライアントを1つのプロセスに混在させられますか? はい。kevy-client(ブロッキング)とkevy-client-asyncは独立したクレートで、自由に共存できます。たとえば同じバイナリの中で、組み込みのfile:///ストアにはブロッキング版を、ネットワークシャードにはasync版を、という使い分けが可能です。コネクションは共有されません。

pub/subはどうなりますか? AsyncSubscriberがブロッキングのSubscriberをミラーします。購読状態のRESPコネクションは通常のコマンドを送れないため、AsyncConnectionとは別の型になっています。メッセージ単位のタイムアウトには、ソケットレベルのreadタイムアウトではなく、使っているランタイム自身のプリミティブ(tokio::time::timeoutasync_io::Timerなど)を使ってください。

pipelineビルダーは送信側のバッファリングを強制しますか? はい。それこそが狙いです。pipeline().…run(&mut conn).awaitはバッチ全体を1回のwriteにシリアライズし、N個の応答を順に読み取ります。コマンド単位のバックプレッシャが必要なら、pipelineを組まずにset/getを直接呼んでください。

リポジトリ内のサンプル