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.