Simple ownership problem

How to modify this code to make it compile?

I'm trying to write a certain grpc service, which in one method uses a stream to send a large amount of data. The data transmitted via the stream have several different types, and at the proto level, they are defined as oneof, while at the rust level, it is visible as a certain enum with additional values for its elements. With the appearance of a certain type of information, I want to initiate a transaction, and then, when the data to be written to the database comes through the stream, save it with a dedicated method (for the test duration it could be located in StreamingContext, because I would like to hide the entire database handling in a struct). When an end-of-transaction marker appears in the stream, then I would like to call Commit or Rollback depending on the situation.
Unfortunately, what seemed like a simple matter to define, such as saving a Client object for postgres and the transaction that this client began in a struct, has become complicated.
I assumed that the ownership model in this case was very simple, but I was mistaken. The aim of this code is to place the Client for postgres and the transaction in the fields of the StreamingContext struct.

How can this be achieved?

mod probl_desc {
    use anyhow::Result;
    use tokio_postgres_rustls::MakeRustlsConnect;
    use bb8::PooledConnection;
    use bb8_postgres::PostgresConnectionManager;

    pub type PooledPostgresConnection<'a> = PooledConnection<'a, PostgresConnectionManager<MakeRustlsConnect>>;

    pub struct StreamingContext<'a, 'b> {
        pub conn: PooledPostgresConnection<'a>,
        pub transaction: Option<tokio_postgres::Transaction<'b>>,
    }
    
    impl<'a, 'b> StreamingContext<'a, 'b> {
        pub fn new(conn: PooledPostgresConnection<'a>) -> StreamingContext<'a, 'b> {
            StreamingContext {conn, transaction: None}
        }
    
        pub async fn start_transaction(&'a mut self) -> Result<()> {
            let transaction = self.conn.transaction().await;
            match transaction {
                Ok(transaction_ok) => {
                    self.transaction = Some(transaction_ok);
                    Ok(())
                }
                Err(e) => Err(anyhow::anyhow!("Error starting transaction: {}", e))
            }
        }
    
        pub async fn commit(&'a mut self) -> Result<()> {
            match &mut self.transaction {
                Some(transaction) => {
                    transaction.commit().await?;
                    Ok(())
                }
                None => Err(anyhow::anyhow!("No transaction to commit"))
            }
        }
    }
}

In Rust it's not allowed for one field to have a reference to another field in the same struct. See "self-referential structs" discussed many times in this forum.

You will not be able to have Connection and Transaction<'connection> stored or returned together anywhere. You must redesign your interface to have Connection created in some outer scope, and Transaction returned and kept separately inside a smaller scope. You could have connection_pool.with_transaction(|tx| {}) for example.


Additionally, &'a mut self is a terrible footgun that makes entire struct useless after a single call. That is because <'a> on the type means lifetime longer than the entire lifetime of the type itself (a type can never ever have a reference that is shorter than its own lifetime, because that would be a dangling pointer). &mut means exclusive, so &'a mut means exclusive for the entire lifetime of the whole type, and completely prevents doing anything else with the type.

4 Likes

Thanks!
I suspected that there might be something with cyclic dependencies. At first glance, it seemed to me that both instances (both connection and transaction) should be implemented without cross-references... I will not fight with this topic any longer, but rather I will decompose certain elements into independent variables pushed as separate parameters down the call chain...

2 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.