1+ use std:: str:: FromStr ;
2+ use std:: time:: Duration ;
3+
14use anyhow:: { Context , Result } ;
25use 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 } ;
49use testcontainers:: runners:: AsyncRunner ;
510use testcontainers:: { GenericImage , ImageExt } ;
611use torrust_tracker_configuration as configuration;
712
813use 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+
1022pub ( 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+ }
0 commit comments