rustlmdbactix-web

Can I borrow values into a closure instead of moving them?


I'm writing a GET method for a server application written in actix-web. LMDB is the database I use, its transactions need to be aborted or committed before the end of their lifetime.

In order to avoid a bunch of nested matching, I tried using map_err on all of the functions that return a result. There I try to abort the transaction, but the transaction gets moved into the closure instead of being borrowed.

Is there any way to instead borrow the transaction into the closure, or do I have to bite the bullet and write a bunch of nested matches? Essentially, what's the most ergonomic way to write this function?

Example code (see comments next to txn.abort()):

pub async fn get_user(db: Data<Database>, id: Identity) -> Result<Json<User>, Error> {
    let username = id.identity().ok_or_else(|| error::ErrorUnauthorized(""))?;

    let txn = db
        .env
        .begin_ro_txn()
        .map_err(|_| error::ErrorInternalServerError(""))?;

    let user_bytes = txn.get(db.handle_users, &username).map_err(|e| {
        txn.abort(); // txn gets moved here

        match e {
            lmdb::Error::NotFound => {
                id.forget();
                error::ErrorUnauthorized("")
            }
            _ => error::ErrorInternalServerError(""),
        }
    })?;

    let user: User = serde_cbor::from_slice(user_bytes).map_err(|_| {
        txn.abort(); // cannot use txn here as is was moved
        error::ErrorInternalServerError("")
    })?;

    txn.abort(); // cannot use txn here as is was moved
    Ok(Json(user))
}

Solution

  • Sadly, in my case it is impossible to borrow the value into the closure, because abort consumes the transaction. (Thanks to @vkurchatkin for explaining that)

    In case anyone is interested, I've worked out a solution that satisfies me regardless of the issue. It was possible for me to avoid nesting a bunch of matches.

    I moved all of the logic that works on the transaction into a separate function and then delayed the evaluation of the function Result until after running txn.abort() (see comments):

    pub async fn get_user(db: Data<Database>, id: Identity) -> Result<Json<User>, Error> {
        let username = id.identity().ok_or_else(|| error::ErrorUnauthorized(""))?;
    
        let txn = db
            .env
            .begin_ro_txn()
            .map_err(|_| error::ErrorInternalServerError(""))?;
    
        let user = db_get_user(&db, &txn, &id, &username); // Execute separate function but do not evaluate the function Result yet, notice missing question mark operator!
        txn.abort(); // Abort the transaction after running the code. (Doesn't matter if it was successful or not. This consumes the transaction and it cannot be used anymore.)
        Ok(Json(user?)) // Now evaluate the Result using the question mark operator.
    }
    
    // New separate function that uses the transaction.
    fn db_get_user(
        db: &Database,
        txn: &RoTransaction,
        id: &Identity,
        username: &str,
    ) -> Result<User, Error> {
        let user_bytes = txn.get(db.handle_users, &username).map_err(|e| match e {
            lmdb::Error::NotFound => {
                id.forget();
                error::ErrorUnauthorized("")
            }
            _ => error::ErrorInternalServerError(""),
        })?;
    
        serde_cbor::from_slice(user_bytes).map_err(|_| error::ErrorInternalServerError(""))
    }