Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use arangors with a web server #10

Closed
arn-the-long-beard opened this issue Apr 17, 2020 · 14 comments
Closed

How to use arangors with a web server #10

arn-the-long-beard opened this issue Apr 17, 2020 · 14 comments

Comments

@arn-the-long-beard
Copy link
Contributor

Hello !
First, I am huge fan of ArangoDB, and I am starting rust serisouly only now. So big thank you for having made this package ! I am very fresh on rust, like a newbie, nice contrast from my js/ts world lol

Your package is the most advanced regarding Arango driver for Rust. But it seems you might need extra help maybe.

But first, I need some help. I am trying to use Arangodb on a actix-web server that I am making. But I am getting this error message

Here is the message I am having

error[E0277]: the trait bound arangors::database::Database<'_>: std::clone::Clone is not satisfied in [closure@src/main.rs:130:38: 161:6 db:arangors::database::Database<'_>]

And here is the code that make it happen

 let conn = Connection::establish_jwt("http://localhost:8529", "USER", "BEST PASSWORDEVER").unwrap();
    let db=  conn.db("test_db").unwrap() ;
    let mut server = HttpServer::new(|| {
        App::new()
            .data(db)
            .wrap(Logger::default())
            .service(
                web::scope("/api")
                    .service(
                        web::resource("/auth")
                            .route(web::post().to(login))
                    ),
            )
    });

I tried to implement it with :

impl Clone for Database { fn clone(&self) -> Self { unimplemented!() } }

But then I got

|
104 | impl Clone for Database {
| ^^^^^^^^^^^^^^^--------
| | |
| | arangors::database::Database is not defined in the current crate
| impl doesn't use only types from inside the current crate
|
= note: define and implement a trait or new type instead

Do you know how could I handle arangors inside my web server ? I do not want to write backend in Foxx Microservice, I really need to write rust code for my stuff. Also, if you need contributors, maybe I could be one if you want. But never done it before. I just have much time available right now.

Best regards,
a man with a very long beard because the barbershop is closed due to you know lol

@fMeow
Copy link
Owner

fMeow commented Apr 20, 2020

Thank you for sharing your experience with us.

The Clone trait is not implemented for neither Connection nor Database with historical reason. If you do not care about the reason, you can move on to the next paragraph. At that time, Connection holds a cached list of Database. If Clone are allowed, it can cause synchronization problems. Concretely, say we have two Connection objects A and B, and we drop a database with A for some reason. And then, we can still access the dropped database in B, because it is cached, and not knowing the database has been dropped. A manual sync from server might help, but I think it better to just create a new object instead of cloning the object.

Due to the orphan rule of rust, we cannot implement any trait to a struct if both are from other files or mod instead of the one we are writing on. That is the reason you got a compiler error when you tried implementing Clone to Database in your own code.

The current solution can be:

  1. edit the source code of arangors that add Clone trait to Connection, DataBase and Collection.
  2. forget cloning, and simply create a arangors Connection in each thread.
    This solution is reasonable that the cost of connection is a one time HTTP request to arangoDB server, in purpose of retrieval of access token, in addition to a ignorable memory cost of holding several instances of Connection or Database.
  3. simply wrap Database object in Arc.
    Then the wrapped object is now clonable and we only create ONE Connection or Database if the resource cost, when connecting to arangoDB server and holding multiple instances of Connection or DataBase, is really untolerable.
    let db=  Arc::new(conn.db("test_db").unwrap());

I personally recommend the third solution as it's simple and the usage of Arc is quite common in actix, which means the performance cost of inducing an Arc is ignorable in almost all cases.

But well, I should consider just adding a Clone to struct in arangors, as there is no reason now to avoid Clone.

Have a nice day.

@inzanez
Copy link
Contributor

inzanez commented Apr 20, 2020

Another solution could be using r2d2-arangors. It's an r2d2 implementation for the arangors driver. That way you could wrap the r2d2 pool in an Arc and just spawn new connections from that one.

May your beard grow ever longer.

@arn-the-long-beard
Copy link
Contributor Author

Hey guys !

Wow for your answer @fMeow , Thank you so much to take the time to explain. I am learning a lot right now !

I'll give a try to Arc as you suggest. I will also try Rd2d @inzanez !

Actually my code is directly inspired by the example for authentication service in the Actix-web repo example. I had some R2d2 and I was looking for its implementation for Arangors but I didn't find it.

Thank you so much guys !
Have a nice day !

@inzanez
Copy link
Contributor

inzanez commented Apr 20, 2020

@arn-the-long-beard you should find it using cargo search r2d2-arangors

@arn-the-long-beard
Copy link
Contributor Author

Thank you ! I'll come back once I have implemented everything :)

@arn-the-long-beard
Copy link
Contributor Author

arn-the-long-beard commented Apr 20, 2020

Okay, I got some issues still, héhé :)

When trying Arc, the compiler gives me 2 errors.

Here is the code .

 let conn = Connection::establish_jwt("http://localhost:8529", "USER", "BEST PASSWORDEVER").unwrap();
    let db=  Arc::new(conn.db("test_db").unwrap()) ;
    let mut server = HttpServer::new(move|| {
        App::new()
            .data(db)
            .wrap(Logger::default())
            .service(
                web::scope("/api")
                    .service(
                        web::resource("/auth")
                            .route(web::post().to(auth::login))
                    ),
            )
    });

Then I got 2 errors. I understand the sens of the second that explains that the value lives too short. How can I make it live longer ? For the first error, it seems because of missing traits.

I do not understand the Fn closure message so.

error[E0507]: cannot move out of db, a captured variable in an Fn closure
--> src/main.rs:59:19
|
56 | let db= Arc::new(conn.db("test_db").unwrap()) ;
| -- captured outer variable
...
59 | .data(db)
| ^^ move occurs because db has type std::sync::Arc<arangors::database::Database<'_>>, which does not implement the Copy trait

And

error[E0597]: conn does not live long enough
--> src/main.rs:56:23
|
56 | let db= Arc::new(conn.db("test_db").unwrap()) ;
| ^^^^--------------
| |
| borrowed value does not live long enough
| argument requires that conn is borrowed for 'static

Whats is the thing I am missing there ?
I ll use r2d2-arangors, once I solved this first :)

@fMeow
Copy link
Owner

fMeow commented Apr 21, 2020

Wow, it's my first time that I heard of r2d2-arangors. Thanks for the community efforts. You should certainly tried it since the r2d2 family handles connection pool nicely.

As for the compiler error on life-time, this is deliberately designed that a Database instance should live no longer than the Connection instance from which it derived. This is obvious that all access to arango server are dependent on Connection instance. If it is dropped, then all the derived instances like Database, Collection or something about AQL also stop functioning.

So the solution is simple, wrap conn into Arc instead of db. And create a db instance in need.

let conn = Connection::establish_jwt("http://localhost:8529", "USER", "BEST PASSWORDEVER").unwrap();
let conn =  Arc::new(conn);
    let mut server = HttpServer::new(move|| {
        App::new()
            .data(conn)
            .wrap(Logger::default())
            .service(
                web::scope("/api")
                    .service(
                        web::resource("/auth")
                            .route(web::post().to(auth::login))
                    ),
            )
    });

If you care about the performance, you can wrap a tuple into Arc and stoed it in App.

let conn = Connection::establish_jwt("http://localhost:8529", "USER", "BEST PASSWORDEVER").unwrap();
let db=  conn.db("test_db").unwrap() ;
let saved = Arc::new((conn, db));

Maybe the following way can bypass the compiler error about lifetime, but I am not sure. If compiles, it's certainly the most elegant way that meets your need.

let db = Connection::establish_jwt("http://localhost:8529", "USER", "BEST PASSWORDEVER").unwrap().db("test_db").unwrap();
let db = Arc::new(db);

@fMeow
Copy link
Owner

fMeow commented Apr 23, 2020

The lastest version (v0.3.0) add Clone to both Connection and Database. The actix App should be happy now. Also, you can use implement a custom client with awc, and thus get rid of the all dependencies that reqwest induces.

@arn-the-long-beard
Copy link
Contributor Author

arn-the-long-beard commented Apr 23, 2020

Hey ! I was just gonna post :)

I succeed to make it work with r2d2, but it is adding a lot of code that I do not understand.

  let manager = ArangoDBConnectionManager::new("http://localhost:8529", "user", "password",  true);

    let pool = r2d2::Pool::builder()
        .build(manager)
        .expect("Failed to create pool.");
    let mut server = HttpServer::new(move || {
        App::new()
            .data(pool.clone())
            .wrap(Logger::default())
            .service(
                web::scope("/api")
                    .service(
                        web::resource("/auth")
                            .route(web::post().to(auth::login))
                    ),
            )
    });

then in my handler

pub async fn login(auth_data: web::Json<AuthData>,
                   id: Identity, conn: web::Data<DbConnection>) -> Result<HttpResponse, ServiceError> {
    let res = web::block(move || query(auth_data.into_inner(), conn)).await;

    match res {
        Ok(user) => {
            let user_string = serde_json::to_string(&user).unwrap();
            id.remember(user_string);
            Ok(HttpResponse::Ok().finish())
        }
        Err(err) => match err {
            BlockingError::Error(service_error) => Err(service_error),
            BlockingError::Canceled => Err(ServiceError::InternalServerError),
        },
    }
}

and the query

fn query(auth_data: AuthData, conn: web::Data<DbConnection>) -> Result<User, ServiceError> {
    let mut vars = HashMap::new();
    vars.insert("email", serde_json::value::to_value(auth_data.email).unwrap());
    let conn = &conn.get().unwrap();
    let db = conn.db("test_db").unwrap();
    let res: Result<Vec<FullUser>, failure::Error> = db.aql_bind_vars(r#"FOR u in users FILTER  u.data.email == @email return u"#, vars);
    //     Ok(user) => Ok(user),
    //     Err(e) => Err(ServiceError::InternalServerError),
    // };

    if res.is_err() {
        eprintln!("DbError: {}", res.unwrap_err());
        return Err(ServiceError::InternalServerError);
    }
    if let Some(user) = res.unwrap().pop() {
        if let Ok(matching) = verify(&user.hash, &auth_data.password) {
            if matching {
                return Ok(user.data.into());
            }
        }
    }
    Err(ServiceError::Unauthorized)
}

Question :

I used &conn instead of conn like on the actix ecample. but it works fine with just conn.

What are the benefits of using R2d2 over Arangors alone ?

I had to use diesel and r2d2, and I would like to limit my dependencies to the strict minimum :)

I ll try again with 0.3.0 without r2d2 to see how it does looks like the code.

Thank you a lot for your update 👍

@arn-the-long-beard
Copy link
Contributor Author

Okay I think I got the anwser. I do not succeed to make the 0.3.0 works. I got this error every I am using the connection.

error[E0599]: no method named unwrap found for opaque type impl std::future::Future in the current scope
--> src/handlers/auth.rs:45:17
|
45 | let res= db.unwrap().aql_bind_vars(r#"FOR u in users FILTER u.data.email == @email return u"#, vars);
| ^^^^^^ method not found in impl std::future::Future

And for some reason, I got a mismatch of type between the compiler and my IDE info. I have some Future in places where I do not have them.

I ll 'stick with r2d2, it actually make stuff easier :) The Future stuff is still not known for me. I need to skip it for now.

Thank you a lot guys for you help, I want to help in both projects. I am not the smartest programmer in the world, but I am good at documentation and giving examples 👍

How can I help you ? :)

@fMeow
Copy link
Owner

fMeow commented Apr 23, 2020

Thank you so much. Reaching out is already a valuable effort to help arangors and r2d2-arangors to make things clear, something that might be obvious for ones familiar with source code, but confusing for new users.

The v0.3.0 use async version by default, so either add await to every async function. Notice the await keyword in the following example:

let conn = Connection::establish_jwt("http://localhost:8529", "USER", "BEST PASSWORDEVER").await.unwrap();
let db=  conn.db("test_db").await.unwrap();

Refer to documentation when you are not sure whether a function is async or not. The one at docs.rs might differs a lot from your local compiler version if you tweak arangors with feature gate, so it's recommended to refer to cargo doc instead of docs.rs.

Also you can stick with the sync one by specified feature gate in the following way:

[dependencies]
arangors = { version = "0.3", features = ["reqwest_blocking"], default-features = false }

I recommend migrating to async, as the actix is spawned upon tokio async runtime, which is really a performance gain for web server. Another advantage is that, you can painlessly migrate to awc backend, actix web client, to further slim dependencies.

I am planning to implement awc backend in crates. Before that, you can also implement a client by yourself. See readme.md and example/custom_client.rs

@arn-the-long-beard
Copy link
Contributor Author

Hello !
Okay, I ll give a try to the async feature Thank you for the tips and the precious explanations :)

I''ll come back with the example code :)

@arn-the-long-beard
Copy link
Contributor Author

arn-the-long-beard commented May 1, 2020

Hey guys, I was busy with some IRL stuff for the last few days. I am back. I just tried to used r2d2-arangors with the last arangors 0.3.0. It seems we need an update on the r2d2-arangors part because of dependency to 0.2.0.

Here is the working code in pure 0.3.0 without r2d2.

main.rs

let conn = Connection::establish_jwt("http://localhost:8529", "USER", "BEST PASSWORDEVER").unwrap();
let conn =  Arc::new(conn);
let mut server = HttpServer::new(move || {
        App::new()
            .data(conn.clone())
            .wrap(Logger::default())
            .service(
                web::scope("/api")
                    .service(
                        web::resource("/auth")
                            .route(web::post().to(auth::login))
                    ),
            )
    });

login.rs

pub async fn login(auth_data: web::Json<AuthData>,
                   id: Identity, conn: web::Data<Arc<GenericConnection<ReqwestClient>>>) -> Result<HttpResponse, ServiceError> {
    let query = query(auth_data.into_inner(), conn).await;
    let res = web::block(move || query)
        .await;


    match res {
        Ok(user) => {
            let user_string = serde_json::to_string(&user).unwrap();
            id.remember(user_string);
            Ok(HttpResponse::Ok().json(user))
        }
        Err(err) => match err {
            BlockingError::Error(service_error) => Err(service_error),
            BlockingError::Canceled => Err(ServiceError::InternalServerError),
        },
    }
}

//
/// Query for login
async fn query(auth_data: AuthData, conn: web::Data<Arc<GenericConnection<ReqwestClient>>>) -> Result<User, ServiceError> {
    let mut vars = HashMap::new();
    vars.insert("email", serde_json::value::to_value(auth_data.email).unwrap());
    // let conn = &conn.get().unwrap();
    let db = conn.db("test_db").await.unwrap();
    let res: Result<Vec<FullUser>, ClientError> = db
        .aql_bind_vars(r#"FOR u in users FILTER  u.data.email == @email return u"#, vars)
        .await;
    //     Ok(user) => Ok(user),
    //     Err(e) => Err(ServiceError::InternalServerError),
    // };

    if res.is_err() {
        eprintln!("DbError: {}", res.unwrap_err());
        return Err(ServiceError::InternalServerError);
    }
    if let Some(user) = res.unwrap().pop() {
        if let Ok(matching) = verify(&user.hash, &auth_data.password) {
            if matching {
                return Ok(user.data.into());
            }
        }
    }
    Err(ServiceError::Unauthorized)
}

Observations :

  1. I had to put a little more code, and make a new statement for the query part. I have the .await keyword everywhere now. My rust plugin on Clion does not give me info about it.
  2. I try to have my code very clean when I write js and ts stuff, my rust seems a bit dirty and redundant for now.
  3. I tried to make my custom client with the actix web client , but I failed due to some typing things between the arangors client and headers used in awc.

New questions :)

1 ) When I try to clone "Connection" without Arc, I get this Error

error[E0599]: no method named `clone` found for struct `arangors::connection::GenericConnection<arangors::client::reqwest::ReqwestClient>` in the current scope
  --> src/main.rs:62:24
   |
62 |             .data(conn.clone())
   |                        ^^^^^ method not found in `arangors::connection::GenericConnection<arangors::client::reqwest::ReqwestClient>`
   |
   = note: the method `clone` exists but the following trait bounds were not satisfied:
           `arangors::connection::GenericConnection<arangors::client::reqwest::ReqwestClient> : std::clone::Clone`

error: aborting due to previous error

What does it mean not statisfied in this context ?

  1. When I use the Arc<GenericConnection>, it is read as GenericConnection in the handler. It is very awesome and handy. but how does the compiler translate from Arc to T ?

  2. Is the use of r2r2 purpose for r2d2 arangors to increase performance by playing with pool and multi threading ? ( I am not familiar with concurrent stuff, i am Node.js programmer from base and I never needed so much to play with thread stuff before )

  3. I wanna help arangors project, where should I start ?
    I never have contribute to a project on github before.
    I would like to make tiny example code that works in order to make the library more accessible and make it more sexy to use until I get enough skill to write good code.

Thank you a lot guys,
Best regards,
The man with a bear longer than one week ago.

@inzanez
Copy link
Contributor

inzanez commented Jun 4, 2020

I just added support for async connection pooling using mobc:
https://github.com/inzanez/mobc-arangors

Published crate is called 'mobc-arangors'.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants