Redis has been a staple of the web ecosystem for years. It’s often used for caching, as a message broker, or simply as a database.

In this guide, we’ll demonstrate how to use Redis inside a Rust web application.

We’ll build the application using the fantastic warp web framework. The techniques in this tutorial will work very similarly with other Rust web stacks.

We’ll explore three approaches to using Redis:

  • Directly, using one asynchronous connection per request
  • Using a synchronous connection pool
  • Using an asynchronous connection pool

As a client library for Redis, redis-rs is the most stable and widely used crate, so all variants use it as their basis.

For the synchronous pool, we’ll use the r2d2-based r2d2-redis. We’ll use mobc for the asynchronous solution. There are plenty of other async connection pools, such as deadpool and bb8, that all work in a similar way.

The application itself doesn’t do much. There is a handler for each of the different Redis connection approaches, which puts a hardcoded string into Redis with an expiration time of 60 seconds and then reads it out again.

Without further ado, let’s get started!

Setup

First, let’s set up some shared types and define a module for each Redis connection approach:

mod direct;
mod mobc_pool;
mod r2d2_pool;

type WebResult<T> = std::result::Result<T, Rejection>;
type Result<T> = std::result::Result<T, Error>;

const REDIS_CON_STRING: &str = "redis://127.0.0.1/";

The two Result types are defined to save some typing and to represent internal Errors (Result) and external Errors (WebResult).

Next, define this internal Error-type and implement Reject for it so it can be used to return HTTP errors from handlers.

#[derive(Error, Debug)]
pub enum Error {
    #[error("mobc error: {0}")]
    MobcError(#[from] MobcError),
    #[error("direct redis error: {0}")]
    DirectError(#[from] DirectError),
    #[error("r2d2 error: {0}")]
    R2D2Error(#[from] R2D2Error),
}

#[derive(Error, Debug)]
pub enum MobcError {
    #[error("could not get redis connection from pool : {0}")]
    RedisPoolError(mobc::Error<mobc_redis::redis::RedisError>),
    #[error("error parsing string from redis result: {0}")]
    RedisTypeError(mobc_redis::redis::RedisError),
    #[error("error executing redis command: {0}")]
    RedisCMDError(mobc_redis::redis::RedisError),
    #[error("error creating Redis client: {0}")]
    RedisClientError(mobc_redis::redis::RedisError),
}

#[derive(Error, Debug)]
pub enum R2D2Error {
    #[error("could not get redis connection from pool : {0}")]
    RedisPoolError(r2d2_redis::r2d2::Error),
    #[error("error parsing string from redis result: {0}")]
    RedisTypeError(r2d2_redis::redis::RedisError),
    #[error("error executing redis command: {0}")]
    RedisCMDError(r2d2_redis::redis::RedisError),
    #[error("error creating Redis client: {0}")]
    RedisClientError(r2d2_redis::redis::RedisError),
}

#[derive(Error, Debug)]
pub enum DirectError {
    #[error("error parsing string from redis result: {0}")]
    RedisTypeError(redis::RedisError),
    #[error("error executing redis command: {0}")]
    RedisCMDError(redis::RedisError),
    #[error("error creating Redis client: {0}")]
    RedisClientError(redis::RedisError),
}

impl warp::reject::Reject for Error {}

This is a lot of boilerplate for a couple of error types, but since the goal is to implement three ways to do the same thing, this is hard to avoid if we want to have nice errors.

The above defines the general error type and Error types for each of the Redis approaches we will implement. The errors themselves simply deal with connection, pool creation, and command execution errors. You’ll see them in action later.

Using redis-rs directly (async)

Now it’s time to implement the first way to interact with Redis — using one connection per request. The idea is to create a new connection for every request that comes in. This approach is fine if there isn’t a lot of traffic, but won’t scale well with hundreds or thousands of concurrent requests.

redis-rs supports both a synchronous and an asynchronous API. The maintainers did a great job keeping the two APIs very similar. While we’ll focus on the asynchronous way to use the crate directly, the synchronous version would look almost exactly the same.

First, let’s establish a new connection.

use crate::{DirectError::*, Result};
use redis::{aio::Connection, AsyncCommands, FromRedisValue};

pub async fn get_con(client: redis::Client) -> Result<Connection> {
    client
        .get_async_connection()
        .await
        .map_err(|e| RedisClientError(e).into())
}

In the above snippet, a redis::Client is passed in and an async connection comes out, handling the error. We’ll look at the creation of the redis::Client later.

Now that it’s possible to open a connection, the next step is to create some helpers to enable setting values in Redis and get them out again. In this simple case, we’ll only focus on String values.

pub async fn set_str(
    con: &mut Connection,
    key: &str,
    value: &str,
    ttl_seconds: usize,
) -> Result<()> {
    con.set(key, value).await.map_err(RedisCMDError)?;
    if ttl_seconds > 0 {
        con.expire(key, ttl_seconds).await.map_err(RedisCMDError)?;
    }
    Ok(())
}

pub async fn get_str(con: &mut Connection, key: &str) -> Result<String> {
    let value = con.get(key).await.map_err(RedisCMDError)?;
    FromRedisValue::from_redis_value(&value).map_err(|e| RedisTypeError(e).into())
}

This part will be very similar between all three implementations since this is just the redis-rs API in action. The API closely mirrors Redis commands and the FromRedisValue trait is a convenient way to convert values into the expected data types.

So far, so good. Next up is a synchronous pool based on the widely used r2d2 crate.

#rust #redis #developer

How to use Redis Inside a Rust Web Application
6.75 GEEK