I am trying to build a webservice with actix-web + diesel. Everything works out so far but i have a problem. To avoid the XY-Problem here is my use case i am trying to solve. I have a database operation i want to do that needs a transaction isolation level serializable. There could be a transaction Y running that would fail the current transaction X. All i want to do is to wait a little bit and retry X and hope Y is finished when X is applied again. I tried to boil it down as much as i can and preserve the original compiler error. I think one can even boil it down much further but i just don't know exactly what the problem is, so here we are. I use the futures_retry crate to accomplish the retry functionality. here is the example
cargo.toml:
[package]
name = "fn_future_test"
version = "0.1.0"
edition = "2018"
[dependencies]
actix-web = "1.0.0"
futures = "0.1.27"
futures-retry = "0.3.3"
diesel = { version = "1.0.0", features = ["postgres", "r2d2"] }
main.rs:
use actix_web::error::ErrorBadRequest;
use actix_web::{web, App, Error, HttpResponse, HttpServer};
use futures::future::result;
use futures::Future;
use futures_retry::{FutureRetry, RetryPolicy, RetryPolicy::WaitRetry};
use std::time::Duration;
fn handle_serialize_error(e: diesel::result::Error) -> RetryPolicy<diesel::result::Error> {
//retry only if we have a SerializationError, everything else should fail immediately
match e {
diesel::result::Error::DatabaseError(
diesel::result::DatabaseErrorKind::SerializationFailure,
msg,
) => WaitRetry(Duration::from_millis(500)),
_ => RetryPolicy::ForwardError(e),
}
}
//would have input parameters like a Json<NewUser> that gets validated and a database connection
fn handler() -> impl Future<Item = HttpResponse, Error = Error> {
result(Ok(1)) //just simulating input validation
//.map_err(|err| ErrorBadRequest(err)) //in case the validation went wrong
.and_then(|val| {
let db_retry = FutureRetry::new(
|| {
actix_web::web::block(|| {
//make an insert with diesel that could yield a SerializationFailure inside a
//transaction something like:
//
//let conn = &db_pool.get().unwrap();
//conn.build_transaction()
// .serializable()
// .run(|| {
// diesel::insert_into(users)
// .values(&new_user)
// .execute(conn)
// })
//but just simulate an error here
Err(diesel::result::Error::DatabaseError(
diesel::result::DatabaseErrorKind::SerializationFailure,
Box::new("test error".to_string()),
))
})
},
handle_serialize_error,
);
db_retry
.map_err(|_err| ErrorBadRequest("error".into()))
.and_then(|x: usize| Ok(HttpResponse::Ok().body("every thing is fine")))
})
}
fn main() {
println!("starting server");
HttpServer::new(|| App::new().route("/", web::get().to_async(handler)))
.bind("127.0.0.1:8000")
.expect("Can not bind to port 8000")
.run()
.unwrap();
}
The output from the compiler i am getting is:
Checking fn_future_test v0.1.0 (/home/naikon/Documents/dev/test/rust/fn_future_test)
error[E0599]: no method named `map_err` found for type `futures_retry::future::FutureRetry<[closure@src/main.rs:25:17: 43:18], fn(diesel::result::Error) -> futures_retry::RetryPolicy<diesel::result::Error> {handle_serialize_error}>` in the current scope
--> src/main.rs:48:18
|
48 | .map_err(|_err| ErrorBadRequest("error".into()))
| ^^^^^^^
|
= note: the method `map_err` exists but the following trait bounds were not satisfied:
`&mut futures_retry::future::FutureRetry<[closure@src/main.rs:25:17: 43:18], fn(diesel::result::Error) -> futures_retry::RetryPolicy<diesel::result::Error> {handle_serialize_error}> : futures::future::Future`
`futures_retry::future::FutureRetry<[closure@src/main.rs:25:17: 43:18], fn(diesel::result::Error) -> futures_retry::RetryPolicy<diesel::result::Error> {handle_serialize_error}> : futures::future::Future`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0599`.
error: Could not compile `fn_future_test`.
To learn more, run the command again with --verbose.
My guess is, that is has to do with the Fn traits of the closures given by FutureRetry::new and the corresponding FutureFactory that takes an FnMut
and actix_web::web::block that takes an FnOnce
. I was thinking that the factory would just create a new FnOnce
every time it's invoked again, but it "leaks" somehow? I just don't know. I can't really figure out where this &mut futures_retry::future::FutureRetry<...
is coming from the compiler is complaining about
I hope someone can give me a hand. I think i could try another retry crate that is not based on Futures, because the actual database operation is sync anyway β as diesel is currently structured, therefore the actix_web::web::block
β but regardless of any solution i just want to know what the root cause is in this specific example, to learn something. And maybe someone could further boil this down to a very simple example, because this is confusing on its own because of the many things involved like diesel, actix-web, futures that have all very verbose types that make it hard to reason about and follow the types in the docs. Thanks to everybody that has come this far down of that wall of text