From 436bf0ef46cafbf6f64968c10c3e4787bee0cae0 Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Sat, 31 Jan 2026 21:55:13 -0600 Subject: [PATCH 1/2] Build: Add retry logic for database connection in install script. The `wp config create` command can fail with "Connection refused" when the database container reports healthy but isn't yet accepting connections. This race condition causes intermittent CI failures. Add `wp_cli_with_retry()` function with exponential backoff to handle database startup timing issues gracefully. --- tools/local-env/scripts/install.js | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/tools/local-env/scripts/install.js b/tools/local-env/scripts/install.js index 80b5c7eb19836..8aae308e14150 100644 --- a/tools/local-env/scripts/install.js +++ b/tools/local-env/scripts/install.js @@ -9,8 +9,8 @@ const local_env_utils = require( './utils' ); dotenvExpand.expand( dotenv.config() ); -// Create wp-config.php. -wp_cli( `config create --dbname=wordpress_develop --dbuser=root --dbpass=password --dbhost=mysql --force --config-file="wp-config.php"` ); +// Create wp-config.php. Use retry logic to handle database startup race conditions. +wp_cli_with_retry( `config create --dbname=wordpress_develop --dbuser=root --dbpass=password --dbhost=mysql --force --config-file="wp-config.php"` ); // Add the debug settings to wp-config.php. // Windows requires this to be done as an additional step, rather than using the --extra-php option in the previous step. @@ -60,3 +60,26 @@ wait_on( { function wp_cli( cmd ) { execSync( `npm --silent run env:cli -- ${cmd} --path=/var/www/${process.env.LOCAL_DIR}`, { stdio: 'inherit' } ); } + +/** + * Runs WP-CLI commands with retry logic for database connection issues. + * + * @param {string} cmd The WP-CLI command to run. + * @param {number} maxRetries Maximum number of retry attempts. Default 5. + * @param {number} delay Initial delay in milliseconds between retries. Default 1000. + */ +function wp_cli_with_retry( cmd, maxRetries = 5, delay = 1000 ) { + for ( let i = 0; i < maxRetries; i++ ) { + try { + wp_cli( cmd ); + return; + } catch ( err ) { + if ( i === maxRetries - 1 ) { + throw err; + } + const waitTime = delay * Math.pow( 2, i ); + console.log( `Database connection failed. Retrying in ${ waitTime }ms... (attempt ${ i + 1 }/${ maxRetries })` ); + execSync( `sleep ${ waitTime / 1000 }` ); + } + } +} From 2c8baf5489da414a22b43a1e51d48c8db3f982b7 Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Fri, 22 May 2026 16:35:49 -0500 Subject: [PATCH 2/2] Build: Address review feedback on env:install retry logic. - Replace `execSync('sleep ...')` with an `Atomics.wait`-based sync sleep so the retry works on Windows (where `sleep` is not a `cmd.exe` builtin). - Bump `maxRetries` from 5 to 6 so the backoff envelope is the 31s promised in the PR description (1+2+4+8+16s before the final throw). - Soften the retry log to "Command failed (likely transient database startup race)" since the catch handles every wp-cli failure (stderr is not captured via `stdio: 'inherit'`, so the retry cannot classify). - Emit a summary line on retry exhaustion so persistent failures are distinguishable from transient ones in CI logs. --- tools/local-env/scripts/install.js | 40 +++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/tools/local-env/scripts/install.js b/tools/local-env/scripts/install.js index 56948ec7bf703..898002e435732 100644 --- a/tools/local-env/scripts/install.js +++ b/tools/local-env/scripts/install.js @@ -63,24 +63,52 @@ function wp_cli( cmd ) { } /** - * Runs WP-CLI commands with retry logic for database connection issues. + * Synchronously pauses execution for the given number of milliseconds. + * + * Uses `Atomics.wait` so the pause works on every platform. Shelling out to a + * `sleep` command would fail on Windows, where `sleep` is not a `cmd.exe` builtin. + * + * @param {number} ms Milliseconds to wait. + */ +function sleep_sync( ms ) { + Atomics.wait( new Int32Array( new SharedArrayBuffer( 4 ) ), 0, 0, ms ); +} + +/** + * Runs WP-CLI commands with retry-on-failure to ride out database startup races. + * + * The Docker `mysqladmin ping` healthcheck can report MariaDB ready before the + * server accepts authenticated connections, causing `wp config create` to fail + * intermittently with "Database connection error (2002) Connection refused". + * + * Because `wp_cli` invokes `execSync` with `stdio: 'inherit'`, the caught error + * does not carry the underlying stderr that would let us classify the failure. + * Every failure is therefore treated as potentially transient and retried; the + * underlying wp-cli error is still streamed live to the user on each attempt, + * and a persistent failure surfaces the same error after the retry envelope + * is exhausted. + * + * Backoff schedule with the defaults: 1s, 2s, 4s, 8s, 16s (~31s total) before + * the final attempt. * * @param {string} cmd The WP-CLI command to run. - * @param {number} maxRetries Maximum number of retry attempts. Default 5. - * @param {number} delay Initial delay in milliseconds between retries. Default 1000. + * @param {number} maxRetries Maximum number of attempts. Default 6. + * @param {number} delay Initial backoff delay in milliseconds. Default 1000. */ -function wp_cli_with_retry( cmd, maxRetries = 5, delay = 1000 ) { +function wp_cli_with_retry( cmd, maxRetries = 6, delay = 1000 ) { for ( let i = 0; i < maxRetries; i++ ) { try { wp_cli( cmd ); return; } catch ( err ) { if ( i === maxRetries - 1 ) { + const total_wait_seconds = ( delay * ( Math.pow( 2, maxRetries - 1 ) - 1 ) ) / 1000; + console.error( `Command failed after ${ maxRetries } attempts (~${ total_wait_seconds }s of retries). Likely a persistent database or configuration issue.` ); throw err; } const waitTime = delay * Math.pow( 2, i ); - console.log( `Database connection failed. Retrying in ${ waitTime }ms... (attempt ${ i + 1 }/${ maxRetries })` ); - execSync( `sleep ${ waitTime / 1000 }` ); + console.log( `Command failed (likely transient database startup race). Retrying in ${ waitTime }ms... (attempt ${ i + 1 }/${ maxRetries })` ); + sleep_sync( waitTime ); } } }