24 days of Rust - redis

Important note: this article is outdated! Go to http://zsiciarz.github.io/24daysofrust/ for a recent version of all of 24 days of Rust articles. The blogpost here is kept as it is for historical reasons.

Today I'm revisiting the topic of database access in Rust. I mentioned PostgreSQL client library a week ago. This time we'll move from SQL to NoSQL land. Our focus for today will be Redis - a data structure server. The redis crate is a client library to access Redis from Rust.

Connecting

The redis crate provides a Client type that is used to connect to the Redis server. The get_connection() method returns a Connection object which execute Redis commands.

extern crate redis;

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

fn main() {
    let client = Client::open("redis://127.0.0.1/").unwrap();
    let conn = client.get_connection().unwrap();
    let _: () = conn.set("answer", 42i).unwrap();
    let answer: int = conn.get("answer").unwrap();
    println!("Answer: {}", answer);
}

Most of the Redis commands translate directly to Connection methods. But if you encounter an error similar to Type ``redis::connection::Connection`` does not implement any method in scope named ``set``, you probably forgot to import the Commands trait.

(I accidentally used the same example number as Armin did in the readme; not surprising since it is the Answer to the Ultimate Question of Life, the Universe, and Everything.)

Friends in common

When viewing someone's profile page on most of the social networking sites, you can see the number (or even a full list) of friends that you both have in common. This is very easy to achieve in Redis using sets.

In case someone accepts your friendship request, a function similar to the one below will be called.

extern crate redis;

use redis::{Client, Commands, Connection, RedisResult};
use std::collections::HashSet;

type UserId = u64;

fn add_friend(conn: &Connection, my_id: UserId, their_id: UserId) -> RedisResult<()> {
    let my_key = format!("friends:{}", my_id);
    let their_key = format!("friends:{}", their_id);
    let _: () = try!(conn.sadd(my_key, their_id));
    let _: () = try!(conn.sadd(their_key, my_id));
    Ok(())
}

I'm assuming here that the friendship relation is mutual. That's why there are two sadd calls - one to add yourself to their set of friends and the other one is symmetrical. Now checking friends in common is just a matter of set intersection - expressed in Redis as the SINTER command.

fn friends_in_common(conn: &Connection, my_id: UserId, their_id: UserId) -> RedisResult<HashSet<UserId>> {
    let my_key = format!("friends:{}", my_id);
    let their_key = format!("friends:{}", their_id);
    Ok(try!(conn.sinter((my_key, their_key))))
}

We can now simulate adding a few friends:

for i in range(1, 10u64) {
    add_friend(&conn, i, i + 2).ok().expect("Friendship failed :(");
}
println!("You have {} friend(s) in common.",
         friends_in_common(&conn, 2, 3).map(|s| s.len()).unwrap_or(0));

Here's the output:

$ cargo run
You have 1 friend(s) in common.

Leaderboards

Sorted sets are possibly my favorite Redis data structure. They're a perfect fit to create leaderboards for example in online games. Add scores with ZADD, fetch the leaderboard with ZREVRANGE - that's the gist of it.

fn add_score(conn: &Connection, username: &str, score: uint) -> RedisResult<()> {
    conn.zadd("leaderboard", username, score)
}

The add_score function is just a wrapper to provide a more high-level API. It will be called every time player's score changes.

type Leaderboard = Vec<(String, uint)>;

fn show_leaderboard(conn: &Connection, n: int) {
    let result: RedisResult<Leaderboard> = conn.zrevrange_withscores("leaderboard", 0, n - 1);
    match result {
        Ok(board) => {
            println!("----==== Top {} players ====----", n);
            for (i, (username, score)) in board.into_iter().enumerate() {
                println!("{:<5} {:^20} {:>4}", i + 1, username, score);
            }
        },
        Err(_) => println!("Failed to fetch leaderboard."),
    }
}

The Leaderboard alias is there just to simplify the result type. We use zrevrange_withscores to get the leaderboard data (sorted by score descending) and display it using Rust's string formatting syntax.

Putting all this together:

let players = vec!["raynor", "kerrigan", "mengsk", "zasz", "tassadar"];
for player in players.iter() {
    let score = rand::random::<uint>() % 1000;
    add_score(&conn, *player, score).ok().expect("Nuclear launch detected");
}
show_leaderboard(&conn, 3);

And if we run this, we'll get something similar to the output below:

$ cargo run
----==== Top 3 players ====----
1            mengsk         986
2           tassadar        879
3           kerrigan        489

See also


Code examples in this article were built with rustc 0.13.0-nightly.

Photo by j_arlecchino and shared under the Creative Commons Attribution-NonCommercial 2.0 Generic License. See https://www.flickr.com/photos/116797173@N07/15121903130