From e842453913e8f5ab07647fc07955d9faf0338e6e Mon Sep 17 00:00:00 2001 From: Wes Rasada Date: Thu, 4 Jun 2026 15:59:54 -0700 Subject: [PATCH 1/4] perf(dev-env): tune local database performance defaults Disable binary logging (MySQL), raise the buffer pool to 1G (matching the VIP production default), raise redo log capacity, and relax per-commit flushing. These are server-wide settings that benefit all local database use; the largest gains are on bulk SQL imports. Bump DEV_ENVIRONMENT_VERSION so existing environments pick up the new flags on next start. Co-Authored-By: Claude Opus 4.8 (1M context) --- assets/dev-env.lando.template.yml.ejs | 11 +++++++++-- src/lib/constants/dev-environment.ts | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/assets/dev-env.lando.template.yml.ejs b/assets/dev-env.lando.template.yml.ejs index 2b3824f1c..a3fde4ddd 100644 --- a/assets/dev-env.lando.template.yml.ejs +++ b/assets/dev-env.lando.template.yml.ejs @@ -103,12 +103,19 @@ services: database: type: compose services: + # Bulk-import friendly settings (local dev only, durability is not a concern): + # - skip-log-bin (MySQL): binary logging is on by default in MySQL 8 and doubles the + # write volume of large SQL imports for no benefit locally. MariaDB has it off by default. + # - innodb-buffer-pool-size: the 128M default forces constant flushing on multi-GB imports. + # - innodb-redo-log-capacity (MySQL) / innodb-log-file-size (MariaDB): the ~100M default + # causes "Redo log writer is waiting for a new redo log file" stalls during imports. + # - innodb-flush-log-at-trx-commit=2: no fsync per commit; at most ~1s of writes lost on crash. <% if ( mariadb ) { %> image: mariadb:<%= mariadb %> - command: docker-entrypoint.sh mysqld --sql-mode=ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION --max_allowed_packet=67M + command: docker-entrypoint.sh mysqld --sql-mode=ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION --max_allowed_packet=67M --innodb-buffer-pool-size=1G --innodb-log-file-size=1G --innodb-flush-log-at-trx-commit=2 <% } else { %> image: mysql:8.4 - command: docker-entrypoint.sh mysqld --sql-mode=ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION --max_allowed_packet=67M --mysql-native-password=ON + command: docker-entrypoint.sh mysqld --sql-mode=ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION --max_allowed_packet=67M --mysql-native-password=ON --skip-log-bin --innodb-buffer-pool-size=1G --innodb-redo-log-capacity=1G --innodb-flush-log-at-trx-commit=2 <% } %> ports: - ":3306" diff --git a/src/lib/constants/dev-environment.ts b/src/lib/constants/dev-environment.ts index f99c18b0c..e75dfa932 100644 --- a/src/lib/constants/dev-environment.ts +++ b/src/lib/constants/dev-environment.ts @@ -52,4 +52,4 @@ export const DEV_ENVIRONMENT_DEFAULTS = { phpVersion: Object.keys( DEV_ENVIRONMENT_PHP_VERSIONS )[ 0 ], } as const; -export const DEV_ENVIRONMENT_VERSION = '2.3.3'; +export const DEV_ENVIRONMENT_VERSION = '2.3.4'; From b6c771d93bf5559cd66909e7295156a118974e47 Mon Sep 17 00:00:00 2001 From: Wes Rasada Date: Fri, 5 Jun 2026 10:23:40 -0700 Subject: [PATCH 2/4] test(dev-env): cover database defaults and version-upgrade path Asserts the running database is not on stock defaults (floors, not pinned values, so future tuning does not break the test) and that an environment stamped with the previous version auto-updates on start, applies the new flags over its existing data directory, and keeps data. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../devenv-e2e/014-database-defaults.spec.js | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 __tests__/devenv-e2e/014-database-defaults.spec.js diff --git a/__tests__/devenv-e2e/014-database-defaults.spec.js b/__tests__/devenv-e2e/014-database-defaults.spec.js new file mode 100644 index 000000000..c09d31a3a --- /dev/null +++ b/__tests__/devenv-e2e/014-database-defaults.spec.js @@ -0,0 +1,195 @@ +import { describe, expect, it, jest } from '@jest/globals'; +import Docker from 'dockerode'; +import nock from 'nock'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { CliTest } from './helpers/cli-test'; +import { vipDevEnvExec, vipDevEnvStart } from './helpers/commands'; +import { killProjectContainers } from './helpers/docker-utils'; +import { + createAndStartEnvironment, + destroyEnvironment, + getProjectSlug, + prepareEnvironment, +} from './helpers/utils'; + +jest.setTimeout( 600 * 1000 ).retryTimes( 1, { logErrorsBeforeRetry: true } ); + +// Stock server defaults that the template deliberately overrides. Values below are +// asserted as "not stock" floors rather than exact matches, so future tuning of the +// template (e.g. shrinking the buffer pool) does not break this test; only *losing* +// an override (falling back to the stock default) fails it. +const STOCK_BUFFER_POOL_SIZE = 134217728; // 128M +const STOCK_REDO_LOG_CAPACITY = 104857600; // ~100M + +const OLD_VERSION = '2.3.3'; +const OLD_DB_COMMAND = + 'docker-entrypoint.sh mysqld --sql-mode=ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION --max_allowed_packet=67M --mysql-native-password=ON'; + +describe( 'dev-env database performance defaults', () => { + /** @type {CliTest} */ + let cliTest; + /** @type {NodeJS.ProcessEnv} */ + let env; + /** @type {string} */ + let tmpPath; + /** @type {Docker} */ + let docker; + /** @type {string} */ + let slug; + + const dbQuery = async query => { + const result = await cliTest.spawn( + [ + process.argv[ 0 ], + vipDevEnvExec, + '--slug', + slug, + '--quiet', + '--', + 'wp', + 'db', + 'query', + query, + '--skip-column-names', + ], + { env }, + true + ); + expect( result.rc ).toBe( 0 ); + return result.stdout.trim(); + }; + + const assertTunedDatabaseDefaults = async () => { + const row = await dbQuery( + 'SELECT @@log_bin, @@innodb_buffer_pool_size, @@innodb_redo_log_capacity, @@innodb_flush_log_at_trx_commit' + ); + const [ logBin, bufferPoolSize, redoLogCapacity, flushMode ] = row.split( /\s+/ ).map( Number ); + + // Binary logging must be off: nothing consumes binlogs locally and they + // double the write volume of large imports. + expect( logBin ).toBe( 0 ); + // Must be configured above the stock defaults; exact values are a tuning choice. + expect( bufferPoolSize ).toBeGreaterThan( STOCK_BUFFER_POOL_SIZE ); + expect( redoLogCapacity ).toBeGreaterThan( STOCK_REDO_LOG_CAPACITY ); + // Anything but fsync-per-commit (1) preserves the bulk-write intent. + expect( flushMode ).not.toBe( 1 ); + }; + + beforeAll( async () => { + nock.cleanAll(); + nock.enableNetConnect(); + + cliTest = new CliTest(); + + tmpPath = await mkdtemp( path.join( os.tmpdir(), 'vip-dev-env-' ) ); + process.env.XDG_DATA_HOME = tmpPath; + + env = prepareEnvironment( tmpPath ); + docker = new Docker(); + + slug = getProjectSlug(); + await createAndStartEnvironment( cliTest, slug, env ); + } ); + + afterAll( async () => { + try { + await destroyEnvironment( cliTest, slug, env ); + } finally { + await killProjectContainers( docker, slug ); + } + } ); + + afterAll( () => rm( tmpPath, { recursive: true, force: true } ) ); + afterAll( () => nock.restore() ); + + // eslint-disable-next-line jest/expect-expect -- assertions live in assertTunedDatabaseDefaults + it( 'should run the database with tuned (non-stock) defaults', async () => { + await assertTunedDatabaseDefaults(); + } ); + + it( 'should apply the tuned defaults to an environment created by an older CLI', async () => { + // Simulate an environment created before the defaults changed: stamp the + // pre-tuning version and restore the old database command line. + const instanceDataPath = path.join( + tmpPath, + 'vip', + 'dev-environment', + slug, + 'instance_data.json' + ); + const instanceData = JSON.parse( await readFile( instanceDataPath, 'utf8' ) ); + expect( instanceData.version ).not.toBe( OLD_VERSION ); + instanceData.version = OLD_VERSION; + await writeFile( instanceDataPath, JSON.stringify( instanceData, null, 2 ) ); + + const landoFilePath = path.join( tmpPath, 'vip', 'dev-environment', slug, '.lando.yml' ); + const landoFile = await readFile( landoFilePath, 'utf8' ); + const oldLandoFile = landoFile.replace( + /command: docker-entrypoint\.sh mysqld[^\n]*/, + `command: ${ OLD_DB_COMMAND }` + ); + expect( oldLandoFile ).not.toBe( landoFile ); + await writeFile( landoFilePath, oldLandoFile ); + + // Plant data that must survive the upgrade rebuild. + const marker = await cliTest.spawn( + [ + process.argv[ 0 ], + vipDevEnvExec, + '--slug', + slug, + '--quiet', + '--', + 'wp', + 'option', + 'add', + 'upgrade_e2e_marker', + 'survived', + ], + { env }, + true + ); + expect( marker.rc ).toBe( 0 ); + + // Starting with the current CLI must detect the old version, re-render the + // template, and rebuild the environment without prompting. + const result = await cliTest.spawn( + [ process.argv[ 0 ], vipDevEnvStart, '--slug', slug, '-w' ], + { env }, + true + ); + expect( result.rc ).toBe( 0 ); + expect( result.stdout ).toContain( `Current local environment version is: ${ OLD_VERSION }` ); + expect( result.stdout ).toContain( 'Local environment version updated to:' ); + expect( result.stdout ).toMatch( /STATUS\s+UP/u ); + + const updatedInstanceData = JSON.parse( await readFile( instanceDataPath, 'utf8' ) ); + expect( updatedInstanceData.version ).not.toBe( OLD_VERSION ); + + // The tuned defaults apply to the pre-existing data directory... + await assertTunedDatabaseDefaults(); + + // ...and the data survived the transition. + const markerValue = await cliTest.spawn( + [ + process.argv[ 0 ], + vipDevEnvExec, + '--slug', + slug, + '--quiet', + '--', + 'wp', + 'option', + 'get', + 'upgrade_e2e_marker', + ], + { env }, + true + ); + expect( markerValue.rc ).toBe( 0 ); + expect( markerValue.stdout.trim() ).toBe( 'survived' ); + } ); +} ); From ffdc9111b868e27a5f399a83dc7ba54eeb42fe29 Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Wed, 12 Aug 2026 15:37:13 +0300 Subject: [PATCH 3/4] fix: tests --- __tests__/devenv-e2e/014-database-defaults.spec.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/__tests__/devenv-e2e/014-database-defaults.spec.js b/__tests__/devenv-e2e/014-database-defaults.spec.js index c09d31a3a..b0843c09e 100644 --- a/__tests__/devenv-e2e/014-database-defaults.spec.js +++ b/__tests__/devenv-e2e/014-database-defaults.spec.js @@ -1,6 +1,5 @@ import { describe, expect, it, jest } from '@jest/globals'; import Docker from 'dockerode'; -import nock from 'nock'; import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; @@ -40,6 +39,9 @@ describe( 'dev-env database performance defaults', () => { /** @type {string} */ let slug; + /** + * @param {string} query + */ const dbQuery = async query => { const result = await cliTest.spawn( [ @@ -79,9 +81,6 @@ describe( 'dev-env database performance defaults', () => { }; beforeAll( async () => { - nock.cleanAll(); - nock.enableNetConnect(); - cliTest = new CliTest(); tmpPath = await mkdtemp( path.join( os.tmpdir(), 'vip-dev-env-' ) ); @@ -103,7 +102,6 @@ describe( 'dev-env database performance defaults', () => { } ); afterAll( () => rm( tmpPath, { recursive: true, force: true } ) ); - afterAll( () => nock.restore() ); // eslint-disable-next-line jest/expect-expect -- assertions live in assertTunedDatabaseDefaults it( 'should run the database with tuned (non-stock) defaults', async () => { From 674ac5e25b6803dfda5d423ebaa06982ceb3792e Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Wed, 12 Aug 2026 15:45:48 +0300 Subject: [PATCH 4/4] tests: add MariaDB 10.3 upgrade test path --- .../devenv-e2e/014-database-defaults.spec.js | 162 +++++++++++++++++- 1 file changed, 153 insertions(+), 9 deletions(-) diff --git a/__tests__/devenv-e2e/014-database-defaults.spec.js b/__tests__/devenv-e2e/014-database-defaults.spec.js index b0843c09e..8bc9aa4fa 100644 --- a/__tests__/devenv-e2e/014-database-defaults.spec.js +++ b/__tests__/devenv-e2e/014-database-defaults.spec.js @@ -5,14 +5,21 @@ import os from 'node:os'; import path from 'node:path'; import { CliTest } from './helpers/cli-test'; -import { vipDevEnvExec, vipDevEnvStart } from './helpers/commands'; +import { + vipDevEnvCreate, + vipDevEnvExec, + vipDevEnvStart, + vipDevEnvUpdate, +} from './helpers/commands'; import { killProjectContainers } from './helpers/docker-utils'; import { + checkEnvExists, createAndStartEnvironment, destroyEnvironment, getProjectSlug, prepareEnvironment, } from './helpers/utils'; +import { DEV_ENVIRONMENT_VERSION } from '../../src/lib/constants/dev-environment'; jest.setTimeout( 600 * 1000 ).retryTimes( 1, { logErrorsBeforeRetry: true } ); @@ -26,6 +33,8 @@ const STOCK_REDO_LOG_CAPACITY = 104857600; // ~100M const OLD_VERSION = '2.3.3'; const OLD_DB_COMMAND = 'docker-entrypoint.sh mysqld --sql-mode=ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION --max_allowed_packet=67M --mysql-native-password=ON'; +const OLD_MARIADB_COMMAND = + 'docker-entrypoint.sh mysqld --sql-mode=ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION --max_allowed_packet=67M'; describe( 'dev-env database performance defaults', () => { /** @type {CliTest} */ @@ -38,17 +47,20 @@ describe( 'dev-env database performance defaults', () => { let docker; /** @type {string} */ let slug; + /** @type {string|undefined} */ + let mariaDbSlug; /** - * @param {string} query + * @param {string} query SQL query + * @param {string} projectSlug Environment slug */ - const dbQuery = async query => { + const dbQuery = async ( query, projectSlug = slug ) => { const result = await cliTest.spawn( [ process.argv[ 0 ], vipDevEnvExec, '--slug', - slug, + projectSlug, '--quiet', '--', 'wp', @@ -93,11 +105,28 @@ describe( 'dev-env database performance defaults', () => { await createAndStartEnvironment( cliTest, slug, env ); } ); + /** + * @param {string|undefined} projectSlug Environment slug + */ + const destroyProject = async projectSlug => { + if ( ! projectSlug ) { + return; + } + + try { + if ( await checkEnvExists( projectSlug ) ) { + await destroyEnvironment( cliTest, projectSlug, env ); + } + } finally { + await killProjectContainers( docker, projectSlug ); + } + }; + afterAll( async () => { try { - await destroyEnvironment( cliTest, slug, env ); + await destroyProject( slug ); } finally { - await killProjectContainers( docker, slug ); + await destroyProject( mariaDbSlug ); } } ); @@ -155,17 +184,19 @@ describe( 'dev-env database performance defaults', () => { // Starting with the current CLI must detect the old version, re-render the // template, and rebuild the environment without prompting. const result = await cliTest.spawn( - [ process.argv[ 0 ], vipDevEnvStart, '--slug', slug, '-w' ], + [ process.argv[ 0 ], vipDevEnvStart, '--slug', slug, '--skip-rebuild', '-w' ], { env }, true ); expect( result.rc ).toBe( 0 ); expect( result.stdout ).toContain( `Current local environment version is: ${ OLD_VERSION }` ); - expect( result.stdout ).toContain( 'Local environment version updated to:' ); + expect( result.stdout ).toContain( + `Local environment version updated to: ${ DEV_ENVIRONMENT_VERSION }` + ); expect( result.stdout ).toMatch( /STATUS\s+UP/u ); const updatedInstanceData = JSON.parse( await readFile( instanceDataPath, 'utf8' ) ); - expect( updatedInstanceData.version ).not.toBe( OLD_VERSION ); + expect( updatedInstanceData.version ).toBe( DEV_ENVIRONMENT_VERSION ); // The tuned defaults apply to the pre-existing data directory... await assertTunedDatabaseDefaults(); @@ -190,4 +221,117 @@ describe( 'dev-env database performance defaults', () => { expect( markerValue.rc ).toBe( 0 ); expect( markerValue.stdout.trim() ).toBe( 'survived' ); } ); + + it( 'should upgrade an existing MariaDB 10.3 environment without losing data', async () => { + mariaDbSlug = getProjectSlug(); + const instancePath = path.join( tmpPath, 'vip', 'dev-environment', mariaDbSlug ); + const instanceDataPath = path.join( instancePath, 'instance_data.json' ); + const landoFilePath = path.join( instancePath, '.lando.yml' ); + + let result = await cliTest.spawn( + [ process.argv[ 0 ], vipDevEnvCreate, '--slug', mariaDbSlug ], + { env }, + true + ); + expect( result.rc ).toBe( 0 ); + + const instanceData = JSON.parse( await readFile( instanceDataPath, 'utf8' ) ); + instanceData.mariadb = '10.3'; + await writeFile( instanceDataPath, JSON.stringify( instanceData, null, 2 ) ); + + result = await cliTest.spawn( + [ process.argv[ 0 ], vipDevEnvUpdate, '--slug', mariaDbSlug ], + { env }, + true + ); + expect( result.rc ).toBe( 0 ); + + const landoFile = await readFile( landoFilePath, 'utf8' ); + const oldLandoFile = landoFile.replace( + /command: docker-entrypoint\.sh mysqld[^\n]*/, + `command: ${ OLD_MARIADB_COMMAND }` + ); + expect( oldLandoFile ).not.toBe( landoFile ); + await writeFile( landoFilePath, oldLandoFile ); + + // Initialize the data directory with the old MariaDB command before + // exercising the version-triggered rebuild. + result = await cliTest.spawn( + [ process.argv[ 0 ], vipDevEnvStart, '--slug', mariaDbSlug, '-w' ], + { env }, + true + ); + expect( result.rc ).toBe( 0 ); + expect( result.stdout ).toMatch( /STATUS\s+UP/u ); + + const marker = await cliTest.spawn( + [ + process.argv[ 0 ], + vipDevEnvExec, + '--slug', + mariaDbSlug, + '--quiet', + '--', + 'wp', + 'option', + 'add', + 'mariadb_upgrade_e2e_marker', + 'survived', + ], + { env }, + true + ); + expect( marker.rc ).toBe( 0 ); + + const oldInstanceData = JSON.parse( await readFile( instanceDataPath, 'utf8' ) ); + oldInstanceData.version = OLD_VERSION; + await writeFile( instanceDataPath, JSON.stringify( oldInstanceData, null, 2 ) ); + + result = await cliTest.spawn( + [ process.argv[ 0 ], vipDevEnvStart, '--slug', mariaDbSlug, '--skip-rebuild', '-w' ], + { env }, + true + ); + expect( result.rc ).toBe( 0 ); + expect( result.stdout ).toContain( `Current local environment version is: ${ OLD_VERSION }` ); + expect( result.stdout ).toContain( + `Local environment version updated to: ${ DEV_ENVIRONMENT_VERSION }` + ); + expect( result.stdout ).toMatch( /STATUS\s+UP/u ); + + const updatedInstanceData = JSON.parse( await readFile( instanceDataPath, 'utf8' ) ); + expect( updatedInstanceData.version ).toBe( DEV_ENVIRONMENT_VERSION ); + expect( updatedInstanceData.mariadb ).toBe( '10.3' ); + + const row = await dbQuery( + 'SELECT @@log_bin, @@innodb_buffer_pool_size, @@innodb_log_file_size, @@innodb_log_files_in_group, @@innodb_flush_log_at_trx_commit', + mariaDbSlug + ); + const [ logBin, bufferPoolSize, logFileSize, logFileCount, flushMode ] = row + .split( /\s+/ ) + .map( Number ); + expect( logBin ).toBe( 0 ); + expect( bufferPoolSize ).toBeGreaterThan( STOCK_BUFFER_POOL_SIZE ); + expect( logFileSize * logFileCount ).toBeGreaterThan( STOCK_REDO_LOG_CAPACITY ); + expect( flushMode ).not.toBe( 1 ); + + const markerValue = await cliTest.spawn( + [ + process.argv[ 0 ], + vipDevEnvExec, + '--slug', + mariaDbSlug, + '--quiet', + '--', + 'wp', + 'option', + 'get', + 'mariadb_upgrade_e2e_marker', + ], + { env }, + true + ); + expect( markerValue.rc ).toBe( 0 ); + expect( markerValue.stdout.trim() ).toBe( 'survived' ); + } ); } );