Storing self-referential type in container

I'm trying to implement a web app where a user can open a database transaction as part of an HTTP request handle, keep it open beyond the lifetime of that first request, then later commit the transaction. However, I'm running into a lifetime checking issue and, as a newbie, am not sure how to resolve it (Pin? GAT?). The core of the issue is demonstrated in the example below:

use core::marker::PhantomData;
struct DB {
}
impl DB {
     fn transaction(&self) -> Transaction<'_, Self> {
         Transaction {
            _marker: PhantomData::default(),
        }
     }
}

struct Transaction<'db, TheDB> {
    _marker: PhantomData<&'db TheDB>,
}
impl<'db, TheDB> Transaction<'db, TheDB> {
    fn commit(self) -> () {
        
    }
}

fn main() {
    let mut state = State { tx: None, db: DB{} };
    
    part1(&mut state);
    part2(&mut state);
    
    println!("ok");
}

struct State<'a> {
    db: DB,
    tx: Option<Transaction<'a, DB>>,
}

fn part1(state: &mut State) {
    let tx = state.db.transaction();
    state.tx = Some(tx);
}

fn part2(state: &mut State) {
    match state.tx.take() {
        Some(tx) => {
            tx.commit();
        },
        None => {}
    }
}

(Playground)

This fails to compile with:

error: lifetime may not live long enough
  --> src/main.rs:37:5
   |
35 | fn part1(state: &mut State) {
   |          -----  - let's call the lifetime of this reference `'1`
   |          |
   |          has type `&mut State<'2>`
36 |     let tx = state.db.transaction();
37 |     state.tx = Some(tx);
   |     ^^^^^^^^^^^^^^^^^^^ assignment requires that `'1` must outlive `'2`

In reality....

  • state is stored as a web::Data object in actix-web
  • DB is a rocksdb::TransactionDB.

This would be trivial to accomplish if the tx didn't have to be stored within State and instead begins and ends within a single borrow of State (during a single "part" of the above example).
What's the general approach to implementing something like this?

Can you do something like store the DB in a static OnceCell instead of the data object?

Rust does not support self referential datatypes, and GATs and Pin won't help. The only solutions are to avoid them, or to rely on unsafe code. In certain cases where avoiding allocations is important, Pin might become part of the safely encapsulating API that such an unsafe solution offers, but all in all, involving Pin will only make things harder.

The cool and convenient solution however is to use existing crates that offer macros that (safely) generate an (internally unsafe) implementation of a self referential data structure for you. One option for such a crate that's good IMO, and that I'm mentioning regularly for such questions, is called ouroboros.

Of course even with such crates, while they solve the problem that otherwise unsafety would be introduced, it's still a bit tedious to work with self referential data types, and depending on the use case, your code could become cleaner if you kept the owned and borrowing parts of your data separate. On the other hand, there certainly exist valid use cases where avoiding is hard or impossible, so then it's great to have them!

5 Likes

You can't, and in this case IMHO you shouldn't be doing this anyway.

A database handle is usually a shared object (multiple HTTP requests can access it), but adding a transaction makes it stateful. Having multiple requests access the same transaction would break isolation. You'd have multiple requests write as part of the same transaction, one request commit or roll back another request's changes. Chaos! If you tried to prevent such interference by locking, then it would completely serialize requests accessing the database.

Keep transaction as a completely separate object. Instead of separate part1, part2 functions that create shared state, create a helper function like:

fn with_transaction(&self, closure: impl FnOnce(&mut Transaction)) {
   let mut tx = self.db.transaction(); 
   closure(&mut tx);
   tx.commit();
}
3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.