これは、なにをしたくて書いたもの?
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
あとはfredでしょうか。
GitHub - aembke/fred.rs: An async client for Valkey and Redis
今回は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トレイトを実装することで実現しているので、ここを見るとハイレベルなコマンドとして
実装されているコマンドが確認できることになりますね。
なお、setやdelのコマンドの戻り値はValkeyのコマンドの戻り値の型がそのまま反映されるので必要に応じてコマンドを見ておきましょう。
また、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相当になります。
また結果を取得する必要がないコマンドについては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); }); }
これは、指定したキーを監視して登録したコマンドが成功するまで繰り返すようです。実行はアトミックなパイプラインで行われます。
こういうことですね。
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に慣れないですね…。もうちょっといろいろ試して慣れていきたいところです。