API wars. Rust Actix-Web vs Tower-Web vs Rocket

API wars. Rust Actix-Web vs Tower-Web vs Rocket

This is another article from Rust series where I’m going to show you, how to run the Rust API framework in AWS Lambda. In the first article, we managed to run a simple Lambda handler. In the second article, we have introduced Actix-Web API framework with database connection using Diesel ORM. Since there are few API frameworks on the market, I have decided to compare them and pick one going forward. Our competitors are Actix-Web, Tower-Web, and Rocket. The first two are working on the stable Rust.

Actix-Web API

You can find source code for Actix-Web project on my Github here. This API we have created in second article. I have only added JSON string to be able to test speed without DB connection.

Tower-Web API

To use Tower-Web API there are few changes needed to the application. Database and Diesel configuration is the same as in Actix-Web. First, let’s add dependencies in Cargo.toml for Tower-Web.

[dependencies]
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"
chrono = {version = "0.4.11", features = ["serde"] }
dotenv = "0.15.0"
dotenv_codegen="0.15.0"
tower-web = "0.3.7"
tokio = "0.2.13"


[dependencies.diesel]
version = "1.4.3"
default-features = false
features = ["r2d2","mysql", "chrono"]

Now, let’s add a handler to get users from the database. In Tower-Web API we are using impl_web! macro to define actions. I have created two structs. One is UserApi to represent the User handler and to keep a database connection. (One method that did not work very well, was to implement Tower-Web Extract trait for parameters to get the connection, but it created a connection for each request.) Second is UsersAPIResponse.

use crate::db::MysqlPool;
use crate::model::user::User;
use serde_json::json;
use std::io;
use std::io::Error;
use tower_web::impl_web;

#[derive(Clone)]
pub struct UsersAPI {
    conn: MysqlPool,
}

#[derive(Response)]
struct UsersAPIResponse {
    users: Vec<User>,
}

impl_web! {
    impl UsersAPI {

        pub fn new(conn: MysqlPool ) -> Self {
            Self { conn }
        }

    #[get("/")]
    #[content_type("json")]
    fn user_list(&self) -> Result<UsersAPIResponse, io::Error> {
        use crate::schema::user::dsl::*;
        use diesel::RunQueryDsl;

            let results = user.load::<User>(
                &self
                    .conn
                    .get().
                    map_err(|e| io::Error::new(io::ErrorKind::Other, e))?,
        )
        .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;

        Ok( UsersAPIResponse {users: results} )
    }

In main.rs we just need to build a service and run it.

#[macro_use]
extern crate tower_web;

#[macro_use]
extern crate diesel;

#[macro_use]
extern crate dotenv_codegen;

extern crate dotenv;
extern crate serde;
extern crate serde_json;

use tower_web::middleware::log::LogMiddleware;
use tower_web::ServiceBuilder;

pub mod db;
pub mod handlers;
pub mod model;
pub mod schema;

pub fn main() {
    let pool = db::connect();
    let addr = "127.0.0.1:8088".parse().expect("Invalid address");

    println!("Listening on http://{}", addr);

    ServiceBuilder::new()
        .resource(handlers::users::UsersAPI::new(pool))
        //.middleware(LogMiddleware::new(module_path!()))
        .run(&addr)
        .unwrap();
}

Since we want to test performance I have disabled logging. Code for Tower-Web API is available here.

Rocket API

This one is a little bit different because we need to use the nightly Rust version. To do that you can create a new project with +nightly and change rust compiler.

 cargo +nightly new user-rocket
 rustup default nightly
 rustup update
 

Let’s add dependencies to Cargo.toml.

[dependencies]
rocket = "0.4.4"
rocket_codegen = "0.4.4"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
chrono = {version = "0.4.11", features = ["serde"] }
dotenv = "0.15.0"
dotenv_codegen="0.15.0"

[dependencies.diesel]
version = "1.4.3"
default-features = false
features = ["r2d2","mysql", "chrono"]

[dependencies.rocket_contrib]
version = "*"
default-features = false
features = ["json"]

And again most of the changes are in handers and main.

use diesel::result::Error;
use rocket::http::Status;
use rocket_contrib::json::Json;

use crate::{db::DbConn, model::user};
use user::User;

use std::io::Error as ioError;

fn error_status(error: Error) -> Status {
    match error {
        Error::NotFound => Status::NotFound,
        _ => Status::InternalServerError,
    }
}

#[get("/")]
pub fn all(connection: DbConn) -> Result<Json<Vec<User>>, Status> {
    user::all(&connection)
        .map(|user| Json(user))
        .map_err(|error| error_status(error))
}
#![feature(decl_macro, proc_macro_hygiene)]
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate diesel;
extern crate dotenv;
extern crate rocket_contrib;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate dotenv_codegen;

pub mod db;
pub mod handlers;
pub mod model;
pub mod schema;

use rocket::config::{Config, Environment, LoggingLevel};

fn main() {
    let config = Config::build(Environment::Production)
        .address("127.0.0.1")
        .port(8088)
        .log_level(LoggingLevel::Critical)
        .keep_alive(0)
        .workers(8)
        .unwrap();

    rocket::custom(config)
        .manage(db::connect())
        .mount("/", routes![handlers::users::all])
        .launch();
}

To configure Rocket API setting you can use toml file, environment variables or in code config as above. For testing purposes, I have disabled keep_alive. When enabled, WRK tester had a lot of read errors. After some reading, it seems that WRK is not closing connections with Rocket and this should help. Another worth noticing is Environment setting switched to Production. Full code is available on my Github here.

Need for speed

We have our three API ready to test. I’m going to use WRK for testing. There will be two tests. One without database connection and one with DB for each API. Each test includes 4 runs. 

My laptop:

  • MacBook Pro 2016
  • 3,3 GHz Dual-Core Intel Core i7
  • 16 GB 2133 MHz LPDDR3

I’m using following command to test.

wrk -t 12 -c 400 -d 1m  http://127.0.0.1:8088  

I’m using the following command to test.

Results

Test without database
Test without database.
Test with database
Test with database connection.

As you can see in both tests Actix-Web was the winner but Tower-Web was just behind. On the other hand, Rocket API did horrible in both tests. I’m not sure what happened there but those results do not seem right. I tried a few changes and also ap testing tool, but the results were identical.

In summary, for now, I will continue with Actix-Web or Tower-Web (this one has Lambda plugin that I’m curious to test.)

Github:

Articles from this series:

If you have an interesting project or need a highly qualified team, take a look at Sufrago.com to learn more about our company and get in touch.

Leave a Reply

Your email address will not be published. Required fields are marked *