1

A Rust newbie here, attempting to write a webservice by combining

https://github.com/seanmonstar/warp/blob/master/examples/todos.rs and https://github.com/launchbadge/sqlx/blob/master/examples/postgres/todos/src/main.rs

The following code is in running state. My question is, do I need to clone dbpool for every handler? What's the idiomatic way in Rust (I am coming from Java/Kotlin->Go background, FWIW)

#![deny(warnings)]

use sqlx::postgres::{PgPoolOptions};
use std::env;
use warp::Filter;

#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect("postgres://:@localhost/todo_db").await?;
    if env::var_os("RUST_LOG").is_none() {
        env::set_var("RUST_LOG", "todos=info");
    }
    pretty_env_logger::init();


    let api = filters::todos(pool);

    let routes = api.with(warp::log("todos"));
    // Start up the server...
    warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
    Ok(())
}

mod filters {
    use sqlx::{Pool, Postgres};
    use super::handlers;
    use super::models::{ListOptions, Todo};
    use warp::Filter;

    pub fn todos(
        db: Pool<Postgres>,
    ) -> impl Filter<Extract=impl warp::Reply, Error=warp::Rejection> + Clone {
        todos_list(db)
    }

    /// GET /todos?offset=3&limit=5
    pub fn todos_list(
        db: Pool<Postgres>,
    ) -> impl Filter<Extract=impl warp::Reply, Error=warp::Rejection> + Clone {
        warp::path!("todos")
            .and(warp::get())
            .and(warp::query::<ListOptions>())
            .and(with_db(db))
            .and_then(handlers::list_todos)
    }

    fn with_db(db: Pool<Postgres>) -> impl Filter<Extract=(Pool<Postgres>, ), Error=std::convert::Infallible> + Clone {
        warp::any().map(move || db.clone())
    }

    fn _json_body() -> impl Filter<Extract=(Todo, ), Error=warp::Rejection> + Clone {
        warp::body::content_length_limit(1024 * 16).and(warp::body::json())
    }
}

mod handlers {
    use super::models::{ListOptions};
    use std::convert::Infallible;
    use sqlx::{Pool, Postgres};
    use crate::models::Todo;

    pub async fn list_todos(_opts: ListOptions, db: Pool<Postgres>) -> Result<impl warp::Reply, Infallible> {
        let recs = sqlx::query!(
        r#"
SELECT id, description, done
FROM todos
ORDER BY id
        "#
    )
            .fetch_all(&db).await.expect("Some error message");

        let x: Vec<Todo> = recs.iter().map(|rec| {
            Todo { id: rec.id, text: rec.description.clone(), completed: rec.done }
        }).collect();


        Ok(warp::reply::json(&x))
    }
}

mod models {
    use serde_derive::{Deserialize, Serialize};


    #[derive(Debug, Deserialize, Serialize)]
    pub struct Todo {
        pub id: i64,
        pub text: String,
        pub completed: bool,
    }

    // The query parameters for list_todos.
    #[derive(Debug, Deserialize)]
    pub struct ListOptions {
        pub offset: Option<usize>,
        pub limit: Option<usize>,
    }
}
so-random-dude
  • 15,277
  • 10
  • 68
  • 113
  • 4
    If accepting the pool by value as you do here, then yes, since a value is owned. Note that under the hood, the pool wraps an `Arc` and so cloning the pool value doesn't actually copy the pool, it [copies a thread-safe handle to the same pool](https://docs.rs/sqlx/0.4.0-beta.1/sqlx/struct.Pool.html#impl-Clone). – cdhowie Feb 21 '22 at 08:42
  • When asking rust questions with complicated dependencies (i.e. creates that have to have some combination of features enabled to build), it's always nice to also provide your `Cargo.toml`. In case of the `query!` macro, you also need some additional setup, which would also be nice to include. – Caesar Feb 21 '22 at 13:41

1 Answers1

4

While copying a pool only increase the reference counter in an Arc and is relatively cheap, as @cdhowie points out, you can avoid it if you fancy doing so: .fetch_all(db) only needs an immutable reference. You could thus pass in a &'static Pool<…>. The one tricky thing is: You can't directly declare a

static POOL: Pool<Postgres> = …;

because there's nothing you could put for the . You can only use const fn when initializing statics, and you can't use .await.

Instead, you can use a OnceCell. Multiple variants exist, the one included in tokio is probably most convenient here:

static POOL: OnceCell<Pool<Postgres>> = OnceCell::const_new();
#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
    POOL.get_or_try_init(|| async {
        PgPoolOptions::new()
            .max_connections(5)
            .connect("postgres://:@localhost/todo_db")
            .await
    })
    .await?;
    // Later, just access your pool with POOL.get().unwrap()
    // You don't need the with_db filter anymore

Personally though, I prefer creating connections or pools that live as long as the application itself with Box::leak(Box::new(PgPoolOptions()….await?)). If you think this is bad because it (obviously…) leaks memory, consider this: the OnceCell will never be dropped or free'd either. This also means that neither OnceCell nor Box::leak will allow for a clean shutdown of the connection pool, which your code with the internal Arcs could in theory do.

Caesar
  • 6,733
  • 4
  • 38
  • 44
  • 3
    (The std [`OnceCell`/`Lazy`](https://doc.rust-lang.org/stable/std/lazy/index.html) aren't stable yet.) – Caesar Mar 12 '22 at 13:18