CLOVER🍀

That was when it all began.

RustでValkeyにアクセスしてみる

これは、なにをしたくて書いたもの?

RustからValkeyにアクセスしてみようかなということで。

Valkey GLIDE

Valkeyのクライアントといえば、Valkey GLIDEがあります。前に、Valkey GLIDEのPythonラッパーを使ったエントリーを書いたことが
あります。

ValkeyとRedisのクライアントライブラリーValkey GLIDEをPythonラッパーで試す - CLOVER🍀

Valkey GLIDEはRedisのRustクライアントであるredis-rsをフォークしたものをコアとし、各言語向けにラッパーを提供する形態で
実装されています。

https://github.com/valkey-io/valkey-glide/tree/v1.2.1/glide-core/redis-rs

ではRustからも使えるのかというと、現状Valkey GLIDEはRustから直接使用することを想定していないようです。

Rust client · Issue #828 · valkey-io/valkey-glide · GitHub

RustのRedisクライアント

というわけで、使うならフォーク元のredis-rs(redisクレート)になるようです。

GitHub - redis-rs/redis-rs: Redis library for rust

redis - Rust

あとはfredでしょうか。

GitHub - aembke/fred.rs: An async client for Valkey and Redis

fred - Rust

今回はredis-rsを使ってみようと思います。

環境

今回の環境はこちら

$ rustup --version
rustup 1.27.1 (54dd3d00f 2024-04-24)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.84.0 (9fc6b4312 2025-01-07)`

Valkey。

$ bin/valkey-server --version
Valkey server v=8.0.2 sha=00000000:0 malloc=jemalloc-5.3.0 bits=64 build=cada04e4030cf22b

Valkeyの設定は以下とし、172.17.0.2で動作しているものとします。

conf/valkey.conf

bind 0.0.0.0

user default off
user valkey-user on >password +@all ~* &*

準備

まずはCargoパッケージの作成。

$ cargo new --vcs none --lib valkey-getting-started
$ cd valkey-getting-started

テストコードのみの実装にするつもりなので、ライブラリークレートにしました。

redisクレートを依存関係に追加します。

$ cargo add redis

コネクションプールを使う場合は、r2d2フィーチャーを有効にします。

$ cargo add redis --features r2d2
$ cargo add r2d2

今回はコネクションプールも有効にしておきました。

Cargo.toml

[package]
name = "valkey-getting-started"
version = "0.1.0"
edition = "2021"

[dependencies]
r2d2 = "0.8.10"
redis = { version = "0.28.0", features = ["r2d2"] }

テストコードの雛形はこちら。

src/lib.rs

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use r2d2::Pool;
    use redis::{Client, Commands, Connection, RedisResult};

    // ここにテストを書く!!
}

この後は、こちらにテストを書きながらredisクレートを使っていこうと思います。

redisクレートを使う

では、redisクレートを使っていきます。

Redisへの接続。

    #[test]
    fn connect() {
        let client = Client::open("redis://valkey-user:password@172.17.0.2:6379").unwrap();
        let _ = client.get_connection().unwrap();
    }

Crate redis / Connection Handling

接続に関する情報は、URL形式での指定になるようです。

IntoConnectionInfo in redis - Rust

フォーマットとしてはこちら。

The URL format is redis://[][:@][:port][/[][?protocol=]]

Crare redis / Connection Parameters

コネクションプールを使って接続する場合。

    #[test]
    fn pool() {
        let client = Client::open("redis://valkey-user:password@172.17.0.2:6379").unwrap();
        let pool = Pool::builder().max_size(10).build(client).unwrap();

        let _ = pool.get().unwrap();
    }

Crate redis / Connection Pooling

GitHub - sfackler/r2d2: A generic connection pool for Rust

ローレベルでのコマンドの実行。setとgetです。delも入っていますけど。

    #[test]
    fn set_and_get_low_level() {
        let client = Client::open("redis://valkey-user:password@172.17.0.2:6379").unwrap();
        let mut conn = client.get_connection().unwrap();

        redis::cmd("SET")
            .arg("key1")
            .arg("value1")
            .exec(&mut conn)
            .unwrap();

        assert_eq!(
            redis::cmd("GET")
                .arg("key1")
                .query::<String>(&mut conn)
                .unwrap(),
            "value1"
        );

        redis::cmd("DEL").arg("key1").exec(&mut conn).unwrap();

        assert_eq!(
            redis::cmd("GET")
                .arg("key1")
                .query::<String>(&mut conn)
                .is_err(),
            true
        );
    }

Crate redis / Executing Low-Level Commands

ハイレベルでのコマンドの実行。set、get、delです。

    #[test]
    fn set_and_get_high_level() {
        let client = Client::open("redis://valkey-user:password@172.17.0.2:6379").unwrap();
        let mut conn = client.get_connection().unwrap();

        assert_eq!(
            conn.set::<_, _, Option<String>>("key2", "value2")
                .unwrap()
                .unwrap(),
            "OK"
        );

        let value2: String = conn.get("key2").unwrap();
        assert_eq!(value2, "value2");

        conn.del::<_, i32>("key2").unwrap();

        assert_eq!(conn.get::<_, String>("key2").is_err(), true);
    }

Crate redis / Executing High-Level Commands

ローレベルなコマンドの実行に比べてこちらの方が簡単に書けるのですが、まだコマンドの多くは実装されていないようなので注意です。

Note that high-level commands are work in progress and many are still missing!

ハイレベルなコマンドはCommandsトレイトを実装することで実現しているので、ここを見るとハイレベルなコマンドとして
実装されているコマンドが確認できることになりますね。

Commands in redis - Rust

なお、setやdelのコマンドの戻り値はValkeyのコマンドの戻り値の型がそのまま反映されるので必要に応じてコマンドを見ておきましょう。

Valkey Command · SET

Valkey Command · DEL

また、RedisとRustのデータ型の変換についてはこちら。

Crate redis / Type Conversions

どのような型に対してトレイトが実装されているのかは、FromRedisValueを見ます。

FromRedisValue in redis - Rust

ここで、以降のテストでは接続に関する部分を簡略化する関数を作成して使うことにしました。

    fn prepare_test<F>(consumer: F)
    where
        F: Fn(&mut Connection),
    {
        let client = Client::open("redis://valkey-user:password@172.17.0.2:6379").unwrap();
        let mut conn = client.get_connection().unwrap();

        consumer(&mut conn);
    }

Hashの操作。ハイレベルなコマンドを使っています。

#[test]
    fn hset_hget() {
        prepare_test(|conn| {
            conn.hset::<_, _, _, i32>("hkey1", "hfield1", "hvalue1")
                .unwrap();

            let hfield1: String = conn.hget("hkey1", "hfield1").unwrap();
            assert_eq!(hfield1, "hvalue1");

            conn.hdel::<_, _, i32>("hkey1", "hfield1").unwrap();

            assert_eq!(conn.hget::<_, _, String>("hkey1", "hfield1").is_err(), true);

            conn.hset_multiple::<_, _, _, String>(
                "hkey2",
                &[("hfield2-1", "hvalue2-1"), ("hfield2-2", "hvalue2-2")],
            )
            .unwrap();

            let hset: HashMap<String, String> = conn.hgetall("hkey2").unwrap();
            assert_eq!(
                hset,
                HashMap::from([
                    ("hfield2-1".into(), "hvalue2-1".into()),
                    ("hfield2-2".into(), "hvalue2-2".into()),
                ])
            );

            conn.del::<_, i32>("hkey2").unwrap();

            /*
            for f in hset.keys() {
                conn.hdel::<_, _, i32>("hkey2", f).unwrap();
            }
             */

            let hset: HashMap<String, String> = conn.hgetall("hkey2").unwrap();
            assert_eq!(hset, HashMap::new());
        });
    }

HSET全体を扱う時はHashMapになるようです。

Crate redis / Type Conversions

FromRedisValue in redis - Rust

パイプライン。

    #[test]
    fn pipeline() {
        prepare_test(|mut conn| {
            redis::pipe()
                .atomic()
                .set("key4", "value4")
                .ignore()
                .hset_multiple("hkey3", &[("hfield3", "hvalue3")])
                .ignore()
                .exec(&mut conn)
                .unwrap();

            let result: (String, HashMap<String, String>) = redis::pipe()
                .atomic()
                .get("key4")
                .hgetall("hkey3")
                .query(&mut conn)
                .unwrap();

            assert_eq!(result.0, "value4");
            assert_eq!(
                result.1,
                HashMap::from([("hfield3".into(), "hvalue3".into())])
            );

            redis::pipe()
                .atomic()
                .del("key4")
                .ignore()
                .del("hkey3")
                .ignore()
                .exec(&mut conn)
                .unwrap();

            let result: RedisResult<(String, HashMap<String, String>)> = redis::pipe()
                .atomic()
                .get("key4")
                .hgetall("hkey3")
                .query(&mut conn);

            assert_eq!(result.is_err(), true);
        });
    }

redis::pipeで開始します。atomicをつけることで、multi/exec相当になります。

Crate redis / Pipelining

Pipeline in redis - Rust

また結果を取得する必要がないコマンドについてはignoreとしておくと、パイプラインの戻り値に含まれなくなります。

トランザクション

    #[test]
    fn transaction() {
        prepare_test(|mut conn| {
            let keys = ["key5", "hkey4"];

            redis::transaction(&mut conn, &keys, |mut c, pipe| {
                pipe.set("key5", "value5")
                    .ignore()
                    .hset_multiple("hkey4", &[("hfield4-1", "hvalue4-1")])
                    .ignore()
                    .query::<Option<()>>(&mut c)
            })
            .unwrap();

            let result: (String, HashMap<String, String>) =
                redis::transaction(&mut conn, &keys, |mut c, pipe| {
                    pipe.get("key5").hgetall("hkey4").query(&mut c)
                })
                .unwrap();

            assert_eq!(result.0, "value5");
            assert_eq!(
                result.1,
                HashMap::from([("hfield4-1".into(), "hvalue4-1".into())])
            );

            redis::transaction(&mut conn, &keys, |mut c, pipe| {
                pipe.del("key5")
                    .ignore()
                    .del("hkey4")
                    .ignore()
                    .query::<Option<()>>(&mut c)
            })
            .unwrap();

            let result: RedisResult<(String, HashMap<String, String>)> =
                redis::transaction(&mut conn, &keys, |mut c, pipe| {
                    pipe.get("key5").hgetall("hkey4").query(&mut c)
                });

            assert_eq!(result.is_err(), true);
        });
    }

Crate redis / Transactions

transaction in redis - Rust

これは、指定したキーを監視して登録したコマンドが成功するまで繰り返すようです。実行はアトミックなパイプラインで行われます。

こういうことですね。

    loop {
        cmd("WATCH").arg(keys).exec(con)?;
        let mut p = pipe();
        let response: Option<T> = func(con, p.atomic())?;
        match response {
            None => {
                continue;
            }
            Some(response) => {
                // make sure no watch is left in the connection, even if
                // someone forgot to use the pipeline.
                cmd("UNWATCH").exec(con)?;
                return Ok(response);
            }
        }
    }

https://github.com/redis-rs/redis-rs/blob/redis-0.28.0/redis/src/connection.rs#L1918-L1933

こんなところでしょうか。

おわりに

redisクレートを使って、RustでValkeyにアクセスしてみました。

Valkey自体の操作はいいのですが、Rustに慣れないですね…。もうちょっといろいろ試して慣れていきたいところです。