Hello everybody – i whish you all a nice Sunday
Last week i mostly solved my problem with your help with how to read compiler error messages the right way when dealing with Futures
. The thing i want to do – having an actix web server and inserting data with diesel while having a transaction with isolation level serializable and retry it if it fails with 40001
(SerializationFailure
) – is mostly working.
Now i want the code to be nice and learn something on the way. My main Problem – at least its how it appears to me – is that i now have a lot of closures paired with futures and i need my input data flow "down the stream" of that closures. I am stuck at a place that feels a little bit ugly and i want to get rid of it because of two reasons, first i think it is somehow superfluous and secondly it prevents me from making a nice abstraction. I have distilled the problem down to this playground example. The main attraction here is the register
function
fn register(new_user: NewUser) -> impl Future<Item = usize, Error = usize> {
result(Ok::<usize, usize>(1)) //just simulating input validation
//.from_err()
.and_then(|_| {
retry(
move || {
//how to avoid having this clone, or "parking" new_user
//here altogether? This is also the place where i have to
//"park" my diesel connection pool in the "real" code
let new_user = new_user.clone();
web_block(move || diesel_insert(&new_user))
},
handle_serialize_error,
)
})
}
retry
here needs a FnMut
because we want to call the closure multiple times eventually. web_block
wants to have a FnOnce
, similar to and_then
. retry
and web_block
are crafted after the methods in Futures-retry
and Actix-web
respectively. I put the "real implementation" at the end of this post, but this playground example should behave just the same.
So because the Futures can run long after the register
function has returned, the new_user
variable does not live long enough inside the register
function for the diesel_insert
function to have a valid/living new_user
borrow. So i need to move
it first inside the retry
closure and "park" it here because at this stage i want to use it multiple times and then move
it into the web_block
closure. As i said i find this somehow superfluous and it prevents me from having an abstraction like this
//I want a nice abstraction that somewhat looks like this
fn register_nice(new_user: NewUser) -> impl Future<Item = usize, Error = usize> {
result(Ok::<usize, usize>(1)) //just simulating input validation
//.from_err()
.and_then(|_| {
retry_db(|| diesel_insert(&new_user))
})
}
fn retry_db<FN>(fun: FN) -> impl Future<Item = usize, Error = usize>
where
FN: FnOnce() -> Result<usize, usize>,
{
retry(|| web_block(|| fun()), handle_serialize_error)
}
Because with this i don't see a way to "park" those variables. Mind you, i could craft a specific retry_db
function for every pair of parameters such "register_nice" function could have but ultimately i want to have a variable amount to be "parked" in a generic way so i only have to code this function once, otherwise this exercise would be pretty useless. In "real life" there is also the database connection to be parked there. i haven't really found a nice way around this, i think i understand why (the variables needs to live at the point where we want to use the variables multiple times, because the FnOnce
- web_block
consumes it every time)
As promised here is the "original Code". Just for reference but i think all reasoning can be done with the code above and the playground example
pub fn register(
register_user: Json<WsRegisterUser>,
db_pool: Data<DbPool>,
) -> impl Future<Item = HttpResponse, Error = ApiError> {
result(register_user.validate())
//.map_err(|_| { ApiError::BadRequest(WsResponseCode::BadRequest, "email or password invalid".into()) } )
.from_err()
.and_then(move |_| {
FutureRetry::new(
move || {
//we need to hold onto these variables here in this closure, because we want
//to use these values multiple times inside the FnOnce web::block(..)
let new_user: DbNewUser = register_user.clone().into(); //impl from with hash
let db_pool = db_pool.clone();
actix_web::web::block(move || {
let conn: DbConn = db_pool.get().unwrap(); //TODO(dustin) remove unwrap
conn.build_transaction()
.serializable()
.run(|| diesel::insert_into(users).values(&new_user).execute(&conn))
})
},
handle_serialize_error,
)
.from_err()
//.map_err(|_| { ApiError::BadRequest(WsResponseCode::BadRequest, "serialize error".into()) } )
.and_then(|x: usize| {
Ok(WsResponse::<()>::wrap_ok_none()) //TODO(dustin): find a way to remove type annotation `::<()>`
})
})
}