Skip to content

Commit 42e3e5d

Browse files
committed
test(tracker-core): wait for mysql readiness in persistence benchmark
The persistence benchmark used to fail intermittently when running the MySQL driver because sqlx does not retry the first connection. The official mysql container emits 'ready for connections' twice (first on the unix socket during init, then on TCP), so we now wait for the second occurrence on stderr and additionally ping with SELECT 1 in a short retry loop before initializing the schema. Add the new technical terms (finalises, mysqld, syscall, testcontainer) to the project dictionary so cspell stays happy across follow-up benchmark documentation.
1 parent a4dbc63 commit 42e3e5d

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

packages/tracker-core/src/bin/persistence_benchmark/driver_bench/database/mysql.rs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,34 @@
1+
use std::str::FromStr;
2+
use std::time::Duration;
3+
14
use anyhow::{Context, Result};
25
use bittorrent_tracker_core::databases::setup::initialize_database;
3-
use testcontainers::core::IntoContainerPort;
6+
use sqlx::mysql::{MySqlConnectOptions, MySqlPoolOptions};
7+
use testcontainers::core::wait::LogWaitStrategy;
8+
use testcontainers::core::{IntoContainerPort, WaitFor};
49
use testcontainers::runners::AsyncRunner;
510
use testcontainers::{GenericImage, ImageExt};
611
use torrust_tracker_configuration as configuration;
712

813
use super::{ActiveDatabase, BenchmarkResource};
914

15+
/// Maximum number of connect-and-ping attempts after the container is reported
16+
/// ready. Belt-and-braces against a brief race between the second
17+
/// `ready for connections` log line and TCP acceptance on port 3306.
18+
const READINESS_PING_RETRIES: usize = 30;
19+
/// Delay between readiness-ping attempts.
20+
const READINESS_PING_INTERVAL: Duration = Duration::from_millis(500);
21+
1022
pub(super) async fn initialize(db_version: &str) -> Result<ActiveDatabase> {
23+
// The official `mysql` image emits `ready for connections` twice on stderr:
24+
// first transiently during init on the unix socket, then again once mysqld
25+
// is actually accepting TCP clients on port 3306. We wait for the second
26+
// occurrence so the first query (DDL via `initialize_database`) does not
27+
// race the TCP listener and panic with `UnexpectedEof`. This is the same
28+
// idiom the Java testcontainers MySQL module uses internally.
1129
let mysql_container = GenericImage::new("mysql", db_version)
1230
.with_exposed_port(3306.tcp())
31+
.with_wait_for(WaitFor::Log(LogWaitStrategy::stderr("ready for connections").with_times(2)))
1332
.with_env_var("MYSQL_ROOT_PASSWORD", "test")
1433
.with_env_var("MYSQL_DATABASE", "torrust_tracker_bench")
1534
.with_env_var("MYSQL_ROOT_HOST", "%")
@@ -27,6 +46,17 @@ pub(super) async fn initialize(db_version: &str) -> Result<ActiveDatabase> {
2746
.context("failed to resolve mysql container host port")?;
2847

2948
let mysql_database_url = format!("mysql://root:test@{host}:{port}/torrust_tracker_bench");
49+
50+
// Belt-and-braces: even after the readiness log message, the very first TCP
51+
// connect can still hit `UnexpectedEof` while mysqld finalises bind/accept.
52+
// Probe with a short connect-and-ping loop so the production
53+
// `initialize_database` call below sees a steady server. This mirrors what
54+
// the previous r2d2-based driver did implicitly through pool checkout
55+
// retries.
56+
wait_until_mysql_accepts_connections(&mysql_database_url)
57+
.await
58+
.context("mysql container did not accept connections in time")?;
59+
3060
let mut config = configuration::Core::default();
3161
config.database.driver = configuration::Driver::MySQL;
3262
config.database.path = mysql_database_url;
@@ -37,3 +67,32 @@ pub(super) async fn initialize(db_version: &str) -> Result<ActiveDatabase> {
3767
resource: Some(BenchmarkResource::Mysql(Box::new(mysql_container))),
3868
})
3969
}
70+
71+
async fn wait_until_mysql_accepts_connections(database_url: &str) -> Result<()> {
72+
let options = MySqlConnectOptions::from_str(database_url).context("invalid mysql benchmark URL")?;
73+
74+
let mut last_error: Option<sqlx::Error> = None;
75+
76+
for _ in 0..READINESS_PING_RETRIES {
77+
match MySqlPoolOptions::new().max_connections(1).connect_with(options.clone()).await {
78+
Ok(pool) => {
79+
if let Err(error) = sqlx::query("SELECT 1").execute(&pool).await {
80+
last_error = Some(error);
81+
} else {
82+
pool.close().await;
83+
return Ok(());
84+
}
85+
}
86+
Err(error) => {
87+
last_error = Some(error);
88+
}
89+
}
90+
91+
tokio::time::sleep(READINESS_PING_INTERVAL).await;
92+
}
93+
94+
Err(anyhow::anyhow!(
95+
"mysql still not accepting connections after {READINESS_PING_RETRIES} attempts; last error: {error}",
96+
error = last_error.map_or_else(|| "<none>".to_string(), |e| e.to_string())
97+
))
98+
}

project-words.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ fastrand
8181
fdbased
8282
fdget
8383
filesd
84+
finalises
8485
flamegraph
8586
formatjson
8687
fput
@@ -152,6 +153,7 @@ MSRV
152153
multimap
153154
myacicontext
154155
mysqladmin
156+
mysqld
155157
ñaca
156158
Naim
157159
nanos
@@ -242,6 +244,7 @@ subsec
242244
supertrait
243245
Swatinem
244246
Swiftbit
247+
syscall
245248
sysmalloc
246249
sysret
247250
taiki
@@ -250,6 +253,7 @@ tdyne
250253
Tebibytes
251254
tempfile
252255
Tera
256+
testcontainer
253257
testcontainers
254258
thiserror
255259
timespec

0 commit comments

Comments
 (0)