Some weird DB connection lost

I encountered a strange issue when I was writing some DB tests with SeaOrm a few days ago. For some reason, I felt that the DB connection was being dropped. Eventually, I realized the issue was caused by a “hidden” (not exactly) problem related to Tokio.

Issue

I’ll post the demo code here. I initially thought it might be related to the #[cfg(test)] macro, so I attempted to reproduce the issue within the mod tests module:

#[cfg(test)]
mod tests {
    use std::sync::LazyLock;

    use sea_orm::{ConnectionTrait, Database, DatabaseBackend, DatabaseConnection, Statement};
    use tokio::runtime::Runtime;
    use tracing::{debug, error, info};

    static DB_CONNECTION: LazyLock<DatabaseConnection> = LazyLock::new(|| {
        dbg!("in db connecting"); // Reached here
        let re = Runtime::new().unwrap().block_on(async {
            Database::connect("postgres://test_user:test_password@localhost:5432/test_db")
                .await
                .expect("db connect error")
        });
        dbg!("connected"); // Reached here
        re
    });

    #[test]
    fn test() {
        tracing_subscriber::fmt()
            .with_max_level(tracing::Level::DEBUG)
            .with_test_writer()
            .init();

        LazyLock::force(&DB_CONNECTION);

        let rt = Runtime::new().unwrap();

        rt.block_on(async {
            dbg!("in here?"); // Reached here
            let re = DB_CONNECTION
                .execute(Statement::from_string(
                    DatabaseBackend::Postgres,
                    "DELETE FROM test_table;",
                ))
                .await
                .unwrap();
            dbg!(re); 
            dbg!("done");
        });
    }
}

When I ran the code with env RUST_LOG="debug" cargo test, the output showed that the program was stuck on .execute.

Out of habit, I started inspecting the source code. Tracing from SeaOrm to sqlx, I began to suspect a connection loss.

After running more tests:

    #[test]
    fn test() {
        tracing_subscriber::fmt()
            .with_max_level(tracing::Level::DEBUG)
            .with_test_writer()
            .init();

        // This also times out
        let DB_CONNECTION: LazyLock<DatabaseConnection> = LazyLock::new(|| {
            dbg!("in db connecting");
            let re = Runtime::new().unwrap().block_on(async {
                Database::connect("postgres://test_user:test_password@localhost:5432/test_db")
                    .await
                    .expect("db connect error")
            });
            dbg!("connected");
            re
        });
        LazyLock::force(&DB_CONNECTION); // Force initialization of the DB connection

        let rt = Runtime::new().unwrap();
        rt.block_on(async {
            dbg!("in here?");
            let re = DB_CONNECTION
                .execute(Statement::from_string(
                    DatabaseBackend::Postgres,
                    "DELETE FROM test_table;",
                ))
                .await
                .unwrap();
            dbg!(re);
            dbg!("done");
        });
    }

This still timed out. I then suspected it might be due to the LazyLock. Since it was only added to the standard library recently, I decided to try the older lazy_static approach:

#[cfg(test)]
mod tests {
    use lazy_static::lazy_static;
    use std::sync::LazyLock;

    use sea_orm::{ConnectionTrait, Database, DatabaseBackend, DatabaseConnection, Statement};
    use tokio::runtime::Runtime;
    use tracing::{debug, error, info};

    lazy_static! {
        static ref DB_CONNECTION: DatabaseConnection = {
            dbg!("in db connecting");
            let re = Runtime::new().unwrap().block_on(async {
                Database::connect("postgres://test_user:test_password@localhost:5432/test_db")
                    .await
                    .expect("db connect error")
            });
            dbg!("connected");
            re
        };
    }

    fn use_connection(db: &DatabaseConnection) {}

    #[test]
    fn test() {
        tracing_subscriber::fmt()
            .with_max_level(tracing::Level::DEBUG)
            .with_test_writer()
            .init();

        let rt = Runtime::new().unwrap();
        use_connection(&DB_CONNECTION);

        rt.block_on(async {
            dbg!("in here?");
            let re = DB_CONNECTION
                .execute(Statement::from_string(
                    DatabaseBackend::Postgres,
                    "DELETE FROM test_table;",
                ))
                .await
                .unwrap();
            dbg!(re);
            dbg!("done");
        });
    }
}

Unfortunately, this also timed out.

How to Make it Work

If I avoid using a LazyLock-generated static DB connection, the code works fine:

        let rt = Runtime::new().unwrap();

        // Code below works
        let DB_CONNECTION = rt.block_on(async {
            Database::connect("postgres://test_user:test_password@localhost:5432/test_db")
                .await
                .expect("db connect error")
        });

        rt.block_on(async {
            dbg!("in here?");
            let re = DB_CONNECTION
                .execute(Statement::from_string(
                    DatabaseBackend::Postgres,
                    "DELETE FROM test_table;",
                ))
                .await
                .unwrap();
            dbg!(re);
            dbg!("done");
        });

Ok, I’m Stuck. Let Me See What the Internet Says

I posted this question here.

Thanks to @ohdanek for answering my question:

Actually, I recreated the problem and figured out that blocking occurs because you created Runtime in such way Runtime::new().unwrap().block_on()

Instance of the Runtime immediatly deconstructs after the block_on invocation, it leads shutting down all Runtime resources.

Because the DB_CONNECTION was created in the deconstructed Runtime, the second Runtime “loses” control because the asynchronous communication resources are no longer maintained.

Written on January 17, 2025