Rocket Tutorial 04 ALTERNATIVE VERSION: Rocket and Redis

davidedelpapa

Davide Del Papa

Posted on December 8, 2020

Rocket Tutorial 04 ALTERNATIVE VERSION: Rocket and Redis

[Photo by Possessed Photography on Unsplash, modified(cropped)]
Last time we have shown the importance of data persistency with the help of a database.

This time we will explore an alternative "DB" that I think is very fitting for a micro-service architecture.

The code for this tutorial can be found in this repository: github.com/davidedelpapa/rocket-tut, and has been tagged for your convenience:

git clone https://github.com/davidedelpapa/rocket-tut.git
cd rocket-tut
git checkout tags/tut4alt
Enter fullscreen mode Exit fullscreen mode

Redis

We will use Redis as database for this tutorial. Redis is not a database in the strict sense, yet it is not something different from a database.

What?

Properly speaking Redis is a key-value store, that is it stores data, just as a DB, but this data is stored as a (key = value) pair. That is, the data is retrieved mostly by key.

The Redis server is meant to be run in-memory, BUT it syncs (almost instantaneously) with a persistent storage on the hard drive. Since it is in-memory Redis can be run on the same machine (or virtual machine, or container) as the micro-service that uses it, and be blazing fast with a small footprint; yet it will act as a persistent store, so the data is saved.

Redis is a good choice for fast response times, and it can be used as a (persistent) cache or a message broker. It is quite flexible too, but it cannot compare to a proper database in fact of user management and scalability (yes, it can be scaled, but it is nothing compared to proper DBMS).

It can be a good solution if the API server does not need to store much data, or if it just needs a simple cache to exchange data with other services. We will explore it as an alternative solution to MongoDB in order to have more choices to architecture the solution that best fit our problem.

A new branch

Notice that in order to be a replacement for MongoDB, we need to branch off from the old commit, before the changes made to adapt to MongoDB. If you have been following on your own repo, here are the instructions to revert from the older commit.

First let's check the ID of the commit with

git log --pretty=oneline
Enter fullscreen mode Exit fullscreen mode

When you have found it, you have to branch off from the older commit (fill in your own commit ID)

git checkout -b tut04alt <Commit-ID>
Enter fullscreen mode Exit fullscreen mode

With checkout -b we create the branch and switch to it at the same time, otherwise branch will just create the new branch so that we can switch to it later on.

Now we are at the same point we were after the tutorial 3 (or 3 part II).

Prerequisites

We need Redis, in case it's not already installed on our system.

sudo apt install redis-server
Enter fullscreen mode Exit fullscreen mode

Now we need to let Redis interface with systemctl. We need to modify /etc/redis/redis.conf, for example using nano

sudo nano /etc/redis/redis.conf
Enter fullscreen mode Exit fullscreen mode

If you have never used nano before (shame on you; ahahah, just kidding) to search the text we just need ctrl+w (on the bottom the instructions say ^W Where Is). We search for "supervised" which is the configuration we need to change. Once found we need to pass from

supervised no
Enter fullscreen mode Exit fullscreen mode

to

supervised systemd
Enter fullscreen mode Exit fullscreen mode

We save with ctrl+o and leave the same file name (just press enter). Now we can quit nano with ctrl+x.

Let's restart the Redis server:

sudo systemctl restart redis
Enter fullscreen mode Exit fullscreen mode

Now when we need it we can start it with sudo systemctl start redis and stop it with sudo systemctl stop redis

We can check that all went well with the cli (installed by default with the server):

redis-cli

127.0.0.1:6379> 
Enter fullscreen mode Exit fullscreen mode

If we get the prompt with local address and Redis port, everything went smooth. We can even check if the server is alive with the command ping:

127.0.0.1:6379> ping
PONG
Enter fullscreen mode Exit fullscreen mode

Perfect! (Command quit to exit...).

You can play with Redis and familiarize with its data structures if you want. Take a look at the interactive tutorial and the list of all commands to learn some more.

Let's get going with the Rust part now:

cargo add dotenv r2d2 r2d2_redis anyhow
Enter fullscreen mode Exit fullscreen mode

We'll see the use of anyhow later on.

Now we can use Redis on our Rust server.

Let's put Redis to use

As for the MongoDB, we need to change src/data/db.rs in order to let update_password() and update_user() return an instance of the User changed.

pub fn update_password(&mut self, password: &String) -> Self {
    self.hashed_password = hash_password(password, &self.salt);
    self.updated = Utc::now();
    self.to_owned()
}
pub fn update_user(&mut self, name: &String, email: &String) -> Self {
    self.name = name.to_string();
    self.email = email.to_string();
    self.updated = Utc::now();
    self.to_owned()
}
Enter fullscreen mode Exit fullscreen mode

In src/data/ we need a redis_connection.rs

use std::ops::{Deref, DerefMut};
use std::env;
use dotenv::dotenv;
use r2d2;
use r2d2::PooledConnection;
use r2d2_redis::RedisConnectionManager;
use rocket::http::Status;
use rocket::request::{self, FromRequest};
use rocket::{Outcome, Request, State};

type Pool = r2d2::Pool<RedisConnectionManager>;
type PooledConn = PooledConnection<RedisConnectionManager>;

pub struct Conn(pub PooledConn);

impl<'a, 'r> FromRequest<'a, 'r> for Conn {
    type Error = ();

    fn from_request(request: &'a Request<'r>) -> request::Outcome<Conn, ()> {
        let pool = request.guard::<State<Pool>>()?;
        match pool.get() {
            Ok(database) => Outcome::Success(Conn(database)),
            Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
        }
    }
}
impl Deref for Conn {
    type Target = PooledConn;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
impl DerefMut for Conn {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

pub fn init_pool() -> Pool {
    dotenv().ok();
    let redis_address = env::var("REDIS_ADDRESS").expect("REDIS_ADDRESS missing");
    let redis_port = env::var("REDIS_PORT").expect("REDIS_PORT missing");
    let redis_db = env::var("REDIS_DB").expect("REDIS_DB missing");
    //let redis_password = env::var("REDIS_PASSWORD").expect("REDIS_PASSWORD missing");
    let manager = RedisConnectionManager::new(format!("redis://{}:{}/{}", redis_address, redis_port, redis_db)).expect("connection manager");
    // Otherwise, with password:
    //let manager = RedisConnectionManager::new(format!("redis://user:{}@{}:{}/{}", redis_password redis_address, redis_port, redis_db)).expect("connection manager");
    match r2d2::Pool::builder().max_size(15).build(manager) {
        Ok(pool) => pool,
        Err(e) => panic!("Error: failed to create database pool {}", e),
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see it is mostly similar to mongo_connection.rs: we define a Pool, a PooledConn and a Conn; we impl a FromRequest for Conn, as well as a Deref. This time we need also a DerefMut because all the redis commands are traits implemented on the connection, and they all take a &mut self as first argument. If the connection cannot be dereferenced as mutable, it won't work.

NOTE: see the full list of Commands; most of Redis' are implemented.

In the init_pool() we are still taking info from our .env file, but this time if we want a secure login we only need the password... Redis is not a proper database management system, and up to recently it did not have a way to register users, but only authenticate passwords...

Here is an example .env file,
I did not commit it, as usual...

REDIS_ADDRESS=127.0.0.1
REDIS_PORT=6379
REDIS_DB=1
REDIS_PASSWORD=password
Enter fullscreen mode Exit fullscreen mode

Speaking about the DB... Redis uses integers for the DBs names, and the cli connects directly to DB 0. We are going to use a separate DB (1) in order to have a series of keys that is unique for the server we are using, and not have a polluted name-space.

Another alternative that gets used a lot: just use a prefix for each key, in the form "prefix:actual_key". I'm going for the less complicated code-wise, a different DB.

Time to move on to src/lib.rs:

#![feature(proc_macro_hygiene, decl_macro)]
#![allow(unused_attributes)]

#[macro_use] use rocket::*;
use rocket_contrib::serve::StaticFiles;
use rocket_contrib::helmet::SpaceHelmet;

pub mod routes;
pub mod data;

pub fn rocket_builder() -> rocket::Rocket {

    rocket::ignite().attach(SpaceHelmet::default())
    .mount("/", routes![routes::ping::ping_fn])
    .mount("/api", routes![
        routes::user::user_list_rt,
        routes::user::new_user_rt,
        routes::user::info_user_rt,
        routes::user::update_user_rt,
        routes::user::delete_user_rt,
        routes::user::patch_user_rt,
        routes::user::id_user_rt
    ])
    .mount("/files", StaticFiles::from("static/"))
    .manage(data::redis_connection::init_pool())
}
Enter fullscreen mode Exit fullscreen mode

This too is almost identical to the one for MongoDB. Using r2d2 clearly has its advantages, and the similarity between the various interfaces is striking.

What is absolutely different between redis and mongodb is the handling of the commands.

As we have seen, the mongodb crate relied heavily on the pattern of returning Result<Option<T>, E>, whereas redis relies on the sole Result<T, E>. THis means there's less to match for us.

The user routes

Some architectural considerations are due, before even taking a look at the code of src/routes/user.rs:

  1. Since the crate's interface requires less matching, and since the Redis commands are more simple and less nuanced than those of MongoDB (for one, we do not have to use BSON docs!), we will resort to a strategy that for MongoDB would have been much more complicated and requiring many more exceptions: we are implementing two methods over User, one to retrieve a user from Redis, and the other one to save the User to Redis. That is, we are serializing and deserializing our Rust structs using the data types available in Redis.

  2. We will use a Redis data type called hashes to serialize the User. In practice, for each hash key, the value is an object with many fields: it is like a nested key-value. The number of keys each hash stores can be variable, like MongoDB documents, but we will use it just to serialize a struct conveniently (that is, we will know for now the number of fields at compile time).

  3. We will use an index lookup, that is, we will make a sort of index where checking for the email will return the corresponding user. This was absolutely not needed in MongoDB. Notice however, that this is a technique heavily used in relational databases in order to speed-up the response times of intensive queries. Here we will use it to the same purpose, in order not to search inside every user to check if it corresponds to the email we need to find (that would be a dumb and slow process).

A note here: since we were using BSON to serialize the User to MongoDB, in reality we did use this strategy also with MongoDB... But due to the complexity of the communication, we could not take it to the lengths we will see in Redis.

Serializing and deserializing the User

Before everything the use section:

use std::collections::HashMap;
use chrono::{DateTime, Utc};
use rocket::*;
use rocket_contrib::json::{Json, JsonValue};
use rocket_contrib::json;
use rocket_contrib::uuid::Uuid;
use uuid::Uuid as Uuid2;
use rocket::response;
use rocket::http::{ContentType, Status};
use rocket::response::{Responder, Response};
use r2d2_redis::redis as redis;
use redis::Commands;
use anyhow::Result as AnyResult;
use anyhow::anyhow;
use crate::data::db::{User, InsertableUser, ResponseUser, UserPassword};
use crate::data::redis_connection::Conn;

const LOOKUP: &str = "email_lookup";
Enter fullscreen mode Exit fullscreen mode

We define also the lookup key.

Notice that we rename redis and uuid, the first for convenience, the other not to pollute the name-space; for this same reason we rename the Result of anyhow as well... (in reality there should be no need to rename it, but I prefer this way).

We use the standard collection HashMap to get a hash from Redis.

Next we implement a default error response for the "Internal server error" as we did for the MongoDB tutorial; nothing new there, so check it in the repo if you need a reminder on how to do it.

What we are interested mostly is the impl User{} part.

impl User {
    fn to_redis(self, connection: &mut Conn) -> AnyResult<()> {
        let id = self.id.to_string();
        let email = self.email.clone();
        let r_user = [
            ("name", self.name),
            ("email", self.email),
            ("hashed_password", self.hashed_password),
            ("salt", self.salt),
            ("created", self.created.to_string()),
            ("updated", self.updated.to_string())
        ];
        connection.hset_multiple(&id, &r_user)?;
        // Add email lookup index
        let _ = connection.zadd(LOOKUP, format!("{}:{}", email, id), 0)?;
        Ok(())
    }
}
Enter fullscreen mode Exit fullscreen mode

In order to serialize the User, we first convert the id from Uuid to String, and we clone the email in order to borrow it two times.

We construct an array out of the info, the array being a list of tuples in the form (key: String, value: String).

Then we pass the array to the hset_multiple() setting an hash with key corresponding to the User's id, and the value being the array we constructed.

After this we add the email and id also to the lookup index with zadd(). The lookup index is a Redis sorted set; you can read more on its use as lookup index in this article: Secondary indexing with Redis.

The index works as this: the key is the lookup index name, the value is the pair email:id, and we have to attach to it a score of 0; this is because Redis has a function to search the by value for all the values that have the same score, in this way, by searching the email (first part of the value), we get also the id (second part).

All along we use anyhow to manage the errors, so we can use the ? instead of matching or (never to be done in production) unwrap -pping.

Now we need also to deserialize:

impl User {
    fn from_redis(connection: &mut Conn, id: &String) -> AnyResult<Self> {
        let r_user: HashMap<String, String>  = connection.hgetall(id)?;

        let r_user_id = Uuid2::parse_str(&* id)?;

        let r_user_name: &String = r_user.get(&"name".to_string()).ok_or(anyhow!(""))?;
        let r_user_email: &String = r_user.get(&"email".to_string()).ok_or(anyhow!(""))?;
        let r_user_hashed_password: &String = r_user.get(&"hashed_password".to_string()).ok_or(anyhow!(""))?;
        let r_user_salt: &String = r_user.get(&"salt".to_string()).ok_or(anyhow!(""))?;
        let r_user_created: &String = r_user.get(&"created".to_string()).ok_or(anyhow!(""))?;
        let r_user_updated: &String = r_user.get(&"updated".to_string()).ok_or(anyhow!(""))?;

        let created: DateTime<Utc> = r_user_created.parse()?;
        let updated: DateTime<Utc> = r_user_updated.parse()?;

        Ok(User {
            id: r_user_id,
            name: r_user_name.to_owned(),
            email: r_user_email.to_owned(),
            hashed_password: r_user_hashed_password.to_owned(),
            salt: r_user_salt.to_owned(),
            created,
            updated,
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

hgetall() returns a HashMap<String, String>.

We get from it all the info we serialized; for error handling we use the macro anyhow!() that lets us create a String error as a one-off error message.

We parse the id string to a Uuid with the Uuid crate, since the one provided by rocket_contrib does not have the parse_str() function. It would be nice if rocket_contib would export the current version of uuid but for now, let's get the best out of it...

We have to parse also the two timestamps, because in the HashMap we have only String values.

Once we have everything in order we can return the User well constructed.

Finally, the routes

The GET /users will not use the serialization/deserialization mechanism. It will not even use the regular redis Commands trait, because the command we need is not implemented, We need to make a manual call.

#[get("/users")]
pub fn user_list_rt(mut connection: Conn) -> ApiResponse {
    let connection = &mut *connection;
    let connection_raw: &mut r2d2_redis::redis::Connection = &mut *connection;
    let users_keys: Result<i32, _> = redis::cmd("DBSIZE").query(connection_raw);
    match users_keys {
        Ok(mut user_size) => {
            if user_size >= 2 {user_size -=1 };
            ApiResponse::ok(json!([user_size]))
        },
        Err(_) => ApiResponse::internal_err(),
    }
}
Enter fullscreen mode Exit fullscreen mode

In order to perform a manual command, we need to get a Connection object from the Conn we crated, which involves a two orders of dereferencing. Once we have it, we construct the command with the cmd() that is a lower command, and it is used with a builder pattern. So we have to pass to it also the Connection we will use with the query() method. If the command had some arguments, those would be passed with arg().

Notice that we need to decrease the number we get by 1 if it is 2 or more. This is so, because if even 1 user is set, the lookup key gets set as well, so we have either 0 or 2 keys in the DB.
We have to think of all the edge cases.

To create a new user with POST /users we will get first a User from the InsertableUser in the body of the request, as usual, then we will just serialize it with our custom serialization method:

#[post("/users", format = "json", data = "<user>")]
pub fn new_user_rt(mut connection: Conn, user: Json<InsertableUser>) -> ApiResponse {
    let ins_user = User::from_insertable((*user).clone());
    match ins_user.clone().to_redis(&mut connection){
        Ok(_) => ApiResponse::ok(json!(ResponseUser::from_user(&ins_user))),
        Err(_) => ApiResponse::internal_err(),
    }
}
Enter fullscreen mode Exit fullscreen mode

That was easy.

Also the GET /users/<id> is easy:

#[get("/users/<id>")]
pub fn info_user_rt(mut connection: Conn, id: Uuid) -> ApiResponse {
    let id = id.to_string();
    match User::from_redis(&mut connection, &id){
        Ok(user) => ApiResponse::ok(json!(ResponseUser::from_user(&user))),
        Err(_) => ApiResponse::err(json!(format!("id {} not found", id))),
    }
}
Enter fullscreen mode Exit fullscreen mode

In fact, it is just a matter of deserialization.

Next, the PUT /users/<id>:

#[put("/users/<id>", format = "json", data = "<user>")]
pub fn update_user_rt(mut connection: Conn, user: Json<InsertableUser>, id: Uuid) -> ApiResponse {
    let id = id.to_string();
    match User::from_redis(&mut connection, &id){
        Ok(user_from_redis) =>{
            let mut user_to_redis = user_from_redis.clone();
            if user_to_redis.match_password(&user.password) {
                let _res_lookup: Result<i32, _> = connection.zrem(LOOKUP, format!("{}:{}", user_from_redis.email, id));
                let insert_user = user_to_redis.update_user(&user.name, &user.email);
                match insert_user.clone().to_redis(&mut connection) {
                    Ok(_) => ApiResponse::ok(json!(ResponseUser::from_user(&insert_user))),
                    Err(_) => ApiResponse::internal_err(),
                }
            }
            else { ApiResponse::err(json!("user not authenticated")) }
        },
        Err(_) => ApiResponse::err(json!(format!("id {} not found", id)))
    }
}
Enter fullscreen mode Exit fullscreen mode

This requires first to deserialize a user, then to match if the password we got in the request body is correct, then to update the user. Finally we will serialize the updated user. Notice that if the key already exists Redis will just overwrite it with the new information.

Notice though, that before each time we serialize again the user we need to remove its email from the lookup table, otherwise there will be more than one email registered. Worst of all if the user updates the email field: in that case we have the old and the new email both pointing to the same uuid.

The method we employ is prone to fail, and there is no check that it actually failed. However this is a problem that can happen more in bigger databases settings.
We could go about in a more secure way here, but let's face it: if you think that it will be a big problem, because it can actually happen many times, maybe then you have to consider that Redis is also not the correct solution. Time to switch to a full-fledged DBMS.

In any case, you could write a script that once in a while (a cron once every week, maybe?) goes over the whole DB and prunes it, signals incorrectness, inconsistency, missing or fragmented data, and so on... It's actually good practice.

As for the DELETE /users/<id> route, we extract and match the password as for the PUT:

#[delete("/users/<id>", format = "json", data = "<user>")]
pub fn delete_user_rt(mut connection: Conn, user: Json<UserPassword>, id: Uuid) -> ApiResponse {
    let id = id.to_string();
    match User::from_redis(&mut connection, &id){
        Ok(user_from_redis) =>{
            if user_from_redis.match_password(&user.password) {
                let res: Result<i32, _> = connection.del(&id);
                let _res_lookup: Result<i32, _> = connection.zrem(LOOKUP, format!("{}:{}", user_from_redis.email, id));
                match res {
                    Ok(_) => ApiResponse::ok(json!(ResponseUser::from_user(&user_from_redis))),
                    Err(_) => ApiResponse::internal_err(),
                }
            }
            else { ApiResponse::err(json!("user not authenticated")) }
        },
        Err(_) => ApiResponse::err(json!(format!("id {} not found", id)))
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, if the password matches, we send a del(&id) over to the connection to delete the key altogether (with all its value). The lookup key is taken care of as well.

The PATCH /users/<id> route is very similar to the PUT /users/<id> route:

#[patch("/users/<id>", format = "json", data = "<user>")]
pub fn patch_user_rt(mut connection: Conn, user: Json<UserPassword>, id: Uuid) -> ApiResponse {
    match &user.new_password {
        Some(passw) => {
            let id = id.to_string();
            match User::from_redis(&mut connection, &id){
                Ok(mut user_from_redis) =>{
                    if user_from_redis.clone().match_password(&user.password) {
                        let insert_user = user_from_redis.update_password(&passw);
                        let _res_lookup: Result<i32, _> = connection.zrem(LOOKUP, format!("{}:{}", user_from_redis.email, id));
                        match insert_user.clone().to_redis(&mut connection) {
                            Ok(_) => ApiResponse::ok(json!("Password updated")),
                            Err(_) => ApiResponse::internal_err(),
                        }
                    }
                    else { ApiResponse::err(json!("user not authenticated")) }
                },
                Err(_) => ApiResponse::err(json!(format!("id {} not found", id))),
            }
        },
        None => ApiResponse::err(json!("Password not provided"))
    }
}
Enter fullscreen mode Exit fullscreen mode

The only difference with the PUT route is that in this case we do not update() the user but update_password(). Here too, since we serialize again the User, we need to cleanup the lookup index. Of course, we start off by matching if the new_password is given at all, otherwise we abort the procedure with an error, without even making a call to the Redis service.

The last route, the second ranking GET /users/<email> route, which gets us a user based on the email, uses finally the lookup index to retrieve stuff:

#[get("/users/<email>", rank = 2)]
pub fn id_user_rt(mut connection: Conn, email: String) -> ApiResponse {
    let get_item: Result<Vec<String>, _> = connection.zrangebylex(LOOKUP, format!("[{}", &email), format!("({}\\xff", &email));
    match get_item {
        Ok(lookup_vector) => {
            if lookup_vector.is_empty(){
                return ApiResponse::err(json!(format!("user {} not found",  &email)));
            }
            let split = lookup_vector[0].split(":").collect::<Vec<&str>>();
            let id = split[1].to_string();
            match User::from_redis(&mut connection, &id){
                Ok(user) => ApiResponse::ok(json!(ResponseUser::from_user(&user))),
                Err(_) => ApiResponse::err(json!(format!("user {} not found",  &email))),
            }
        },
        Err(_) => ApiResponse::internal_err()
    }
}
Enter fullscreen mode Exit fullscreen mode

At the start we make a call to the lookup key. We use the zrangebylex() which gets a range of sub-string o search for. it is similar as a regex, but it as a min argument, that sets the beginning of the research, and a max that says how to stop the range.

We start with the email followed by the colon. This way it will search for all keys starting with the email and colon, for example [me@mail.com will get all keys tat start with those characters, so it can get, for example:

me@mail.com
me@mail.com:A
me@mail.com:AA
me@mail.com:B
me@mail.com:BA
(...)
Enter fullscreen mode Exit fullscreen mode

The second parameter will set the stop. If we would give it a (me@mail.com:B value it would get of the above the following subset:

me@mail.com
me@mail.com:A
me@mail.com:AA
Enter fullscreen mode Exit fullscreen mode

If we set it wit a \xff, as in our case (backslash escaped), it will get all the keys starting with [me@mail.com, whatever the characters afterwards. This means, it will search all the uuid that are set for that email (remember, each email is unique, so it will get us the pair email:uuid) we are searching. It will get it as a String, though, not yet split up; but we will take care of that.

The actual zrangebylex() method will get us a Result wrapping a Vec<String>, because there might be more than a result.

The rest is easy: if we do not have an error (connection) we either have an empty vector, which means no user was found corresponding to that email, or we have a user in the vector. We pick up the String and we split it to the colon; we will find the uuid in the second sub-string, and we use it to get user, as in the other GET /users/<email>. At the end of the whole process, we return that user.

And that closes our routes, and our tour of integration between Rocket and Redis.

What is Wrong with our server so far...

Now we should build, then run the tests and see that everything works correctly!

Well, actually not, the tests FAIL!!!

How come?

There are two reasons these tests fail:

First of all the tests do not delete the database they fill, so there's always need to do it by hands. We should really delete each User we insert, after testing it.

But more importantly, our system has a requirement that the email is unique, but it does not enforce it, rendering everything useless.

Apart from this, since we used in both basic_test.rs and failures_test,rs some repeated user (my bad copypasta!), we already have users with duplicated info. In MongoDB this is not a problem, since we pick up the first user with the correct info... but with Redis it is not the case, since we are enforcing an email-based index...

Therefore, if we simply run:

cargo test
Enter fullscreen mode Exit fullscreen mode

we see a series of failures. The point is that every time the to_redis() and the email is used again, it keeps adding a new record with the same email, and another email with different id in the index. This leads to the failure of get_rank_fail and id_user_rt_fail, since the system find a user, when it actually should have found none!

We can see it easily in the redis-cli:

127.0.0.1:6379[1]> ZRANGE email_lookup 0 -1
 1) "j.doe@m.com:9f3d9ab7-7d8f-4369-9861-90b2d14aaa66"
 2) "j85@m.com:fb886446-256d-4723-8745-ad6369c0d212"
 3) "jack.doe@m.com:76cc101c-74e3-439a-ae72-520faf31a391"
 4) "jane.doe@m.com:472921bb-4cf7-4cdc-b9dc-4ba3263b34fb"
 5) "jane.doe@m.com:63db7cce-1c0c-4111-96a9-38ce1b3f706a"
 6) "janet.doe@m.com:08fc9080-cf8c-4acd-baf4-6156b1d8b7dc"
 7) "janet.doe@m.com:c375b7dd-006b-44f9-ac93-8eb973643ba0"
 8) "jkd@m.com:b0dbca02-e6a4-4b45-87f8-1d7ac71e623f"
 9) "jondon@m.com:647f8aa3-4f57-4c29-8fb4-3d750b6c1726"
10) "jondon@m.com:683b7520-3fd1-42fe-8239-bf13b2ce4a22"
Enter fullscreen mode Exit fullscreen mode

A ZRANGE on the email lookup reveals that there are at least three different emails, each used twice!

To run the tests, and make them pass, we should run each suite separately, while flushing the db with redis-cli in betweens with:

127.0.0.1:6379[1]> flushdb
OK
Enter fullscreen mode Exit fullscreen mode

So we run separately and flush:

cargo test --test basic_test  # now FLUSHDB on redis-cli
cargo test --test failures_test # now FLUSHDB on redis-cli
cargo test --test persistency_test
Enter fullscreen mode Exit fullscreen mode

Every test passes. Yeah!

But it is not the desired result!

We should change the tests to remove each user inserted after testing it, but also, we should enforce in our code the rejection of already used emails, as it is usually done in these cases.

However, this will be a matter for the next tutorial, together with another useful feature we will implement... so stay tuned!!!

💖 💪 🙅 🚩
davidedelpapa
Davide Del Papa

Posted on December 8, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related