From 6d1e6a22d6ac115167b3c725daee76710950b367 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 00:39:23 +0200 Subject: [PATCH 01/97] chore(releases): start v2.9.1 branch with version bump and dependency updates --- .github/workflows/codeql.yml | 6 +- .github/workflows/docker_publish.yml | 4 +- .../workflows/generate_mariadb_examples.yml | 4 +- .github/workflows/generate_mysql_examples.yml | 4 +- .github/workflows/lts_autobump.yml | 2 +- .github/workflows/project-update.yml | 2 +- .github/workflows/publish_release.yml | 2 +- .github/workflows/pull_request.yml | 6 +- .github/workflows/run_mt_with_db.yml | 2 +- CURRENT_VERSION.txt | 2 +- Changelog | 11 + Dockerfile | 2 +- MEMORY_DB.md | 2 +- POTENTIAL_ISSUES.md | 2 +- README.md | 4 +- SECURITY.md | 2 +- USAGE.md | 4 +- mysqltuner.pl | 12 +- package-lock.json | 431 ++++++++---------- package.json | 6 +- releases/v2.9.1.md | 43 ++ 21 files changed, 279 insertions(+), 274 deletions(-) create mode 100644 releases/v2.9.1.md diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 068340332..4aac95fdf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -59,7 +59,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 # Add any setup steps before running the `github/codeql-action/init` action. # This includes steps like installing compilers or runtimes (`actions/setup-node` @@ -69,7 +69,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 + uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -98,6 +98,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 + uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/docker_publish.yml b/.github/workflows/docker_publish.yml index 722449cc8..231c98388 100644 --- a/.github/workflows/docker_publish.yml +++ b/.github/workflows/docker_publish.yml @@ -16,10 +16,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 - name: Log in to Docker Hub uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 diff --git a/.github/workflows/generate_mariadb_examples.yml b/.github/workflows/generate_mariadb_examples.yml index 8311f1896..1ff02d104 100755 --- a/.github/workflows/generate_mariadb_examples.yml +++ b/.github/workflows/generate_mariadb_examples.yml @@ -22,7 +22,7 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - name: Setup MySQL Tuner repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Set up MySQL uses: shogo82148/actions-setup-mysql@076e636c45996ec8cb1a455c65f9f5364aeebd8a # v1 @@ -67,7 +67,7 @@ jobs: git add ./examples/github/result/mariadb/${{ matrix.MARIADB_VERSION }}/* || true - name: Run the Action Commit - uses: devops-infra/action-commit-push@fa0c793b27578a6f16c2cf02c416a0d69fac58c9 # master + uses: devops-infra/action-commit-push@f27e0951b748268e6ac8d91861eeac5bc2bd36a8 # master with: github_token: "${{ secrets.GITHUB_TOKEN }}" add_timestamp: true diff --git a/.github/workflows/generate_mysql_examples.yml b/.github/workflows/generate_mysql_examples.yml index a3ea23b1a..ba9866a8a 100755 --- a/.github/workflows/generate_mysql_examples.yml +++ b/.github/workflows/generate_mysql_examples.yml @@ -22,7 +22,7 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - name: Setup MySQL Tuner repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Set up MySQL uses: shogo82148/actions-setup-mysql@076e636c45996ec8cb1a455c65f9f5364aeebd8a # v1 @@ -67,7 +67,7 @@ jobs: git add ./examples/github/result/mysql/${{ matrix.MYSQL_VERSION }}/* || true - name: Run the Action Commit - uses: devops-infra/action-commit-push@fa0c793b27578a6f16c2cf02c416a0d69fac58c9 # master + uses: devops-infra/action-commit-push@f27e0951b748268e6ac8d91861eeac5bc2bd36a8 # master with: github_token: "${{ secrets.GITHUB_TOKEN }}" add_timestamp: true diff --git a/.github/workflows/lts_autobump.yml b/.github/workflows/lts_autobump.yml index ba1499587..d8ddade4a 100644 --- a/.github/workflows/lts_autobump.yml +++ b/.github/workflows/lts_autobump.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Run LTS auto-bumper utility run: | diff --git a/.github/workflows/project-update.yml b/.github/workflows/project-update.yml index 52d476b3f..94bc16589 100644 --- a/.github/workflows/project-update.yml +++ b/.github/workflows/project-update.yml @@ -12,7 +12,7 @@ jobs: update-metadata: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: fetch-depth: 0 diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index 9d2ab64bb..cfe12bf34 100755 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Set Release Info id: rel_info diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index bbb6e41a4..ce040ad54 100755 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -38,7 +38,7 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - name: Setup MySQL Tuner repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Injecting credentials run: | @@ -77,7 +77,7 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - name: Setup MySQL Tuner repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Injecting credentials run: | @@ -101,7 +101,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup MySQL Tuner repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Run compliance checks run: | diff --git a/.github/workflows/run_mt_with_db.yml b/.github/workflows/run_mt_with_db.yml index 2f58a71b4..303f662d4 100755 --- a/.github/workflows/run_mt_with_db.yml +++ b/.github/workflows/run_mt_with_db.yml @@ -73,7 +73,7 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - name: Setup MySQL Tuner repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Injecting credentials run: | diff --git a/CURRENT_VERSION.txt b/CURRENT_VERSION.txt index c8e38b614..dedcc7d43 100644 --- a/CURRENT_VERSION.txt +++ b/CURRENT_VERSION.txt @@ -1 +1 @@ -2.9.0 +2.9.1 diff --git a/Changelog b/Changelog index 1c2881589..51ec50627 100644 --- a/Changelog +++ b/Changelog @@ -1,5 +1,16 @@ # MySQLTuner Changelog +2.9.1 2026-07-09 + +- chore(deps): update actions/checkout action to v7.0.0 +- chore(deps): update all non-major dependencies (@commitlint/cli, @commitlint/config-conventional, brace-expansion, commitizen) +- chore(deps): update devops-infra/action-commit-push digest to f27e0951b748268e6ac8d91861eeac5bc2bd36a8 +- chore(deps): update docker/setup-buildx-action digest to bb05f3f5519dd87d3ba754cc423b652a5edd6d2c +- chore(deps): update github/codeql-action digest to 99df26d4f13ea111d4ec1a7dddef6063f76b97e9 +- chore(deps): update softprops/action-gh-release digest to 718ea10 (#934) +- chore(deps): update ubuntu:latest docker digest to b7f48194d4d8b763a478a621cdc81c27be222ba2206ca3ca6bc42b49685f3d9e +- chore: automated project metadata update + 2.9.0 2026-07-03 - chore(build): allow build scope in compliance auditor diff --git a/Dockerfile b/Dockerfile index e4143eb7c..1c53750a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:latest@sha256:53958ec7b67c2c9355df922dd08dbf0360611f8c3cdb656875e81873db9ffdba +FROM ubuntu:latest@sha256:b7f48194d4d8b763a478a621cdc81c27be222ba2206ca3ca6bc42b49685f3d9e LABEL maintainer="jmrenouard@gmail.com" diff --git a/MEMORY_DB.md b/MEMORY_DB.md index 30eb1abef..f46fdd8b5 100644 --- a/MEMORY_DB.md +++ b/MEMORY_DB.md @@ -1,6 +1,6 @@ # MySQLTuner-perl Version Memory -## Current Version: 2.8.44 +## Current Version: 2.9.0 ## Project Evolution & Systemic Findings diff --git a/POTENTIAL_ISSUES.md b/POTENTIAL_ISSUES.md index c85c11739..c3bfd1119 100644 --- a/POTENTIAL_ISSUES.md +++ b/POTENTIAL_ISSUES.md @@ -2,7 +2,7 @@ This file records anomalies discovered during laboratory testing (Perl warnings, SQL errors, etc.). -## [2026-06-16 Audit] Status Refresh v2.9.0 +## [2026-06-16 Audit] Status Refresh v2.9.1 ### Unit Test Results diff --git a/README.md b/README.md index 9dff9c3d4..64b849c76 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![GitHub stars](https://img.shields.io/github/stars/major/MySQLTuner-perl?style=for-the-badge&logo=github)](https://github.com/major/MySQLTuner-perl) [![Project Status](https://opensource.box.com/badges/active.svg)](https://opensource.box.com/badges) -[![MySQLTuner Version](https://img.shields.io/badge/version-2.9.0-blue.svg)](https://github.com/major/MySQLTuner-perl/releases/tag/v2.9.0) +[![MySQLTuner Version](https://img.shields.io/badge/version-2.9.1-blue.svg)](https://github.com/major/MySQLTuner-perl/releases/tag/v2.9.1) [![Test Status](https://github.com/major/MySQLTuner-perl/actions/workflows/pull_request.yml/badge.svg)](https://github.com/major/MySQLTuner-perl/actions) [![Average time to resolve an issue](https://isitmaintained.com/badge/resolution/major/MySQLTuner-perl.svg)](https://isitmaintained.com/project/major/MySQLTuner-perl "Average time to resolve an issue") [![Percentage of open issues](https://isitmaintained.com/badge/open/major/MySQLTuner-perl.svg)](https://isitmaintained.com/project/major/MySQLTuner-perl "Percentage of issues still open") @@ -237,7 +237,7 @@ docker run --rm -it jmrenouard/mysqltuner --host --user =22.12.0" } }, "node_modules/@commitlint/config-validator": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-21.0.1.tgz", - "integrity": "sha512-Zd2UFdndeMMaW2O96HK0tdfT4gOImUvidMpAd/pws2zZ4m1nrAZ/9b/v2JYuE8fs86GpXv9F7LNaIuCIWhY+pA==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-21.2.0.tgz", + "integrity": "sha512-t7AzNHAKeIdo/3NRGwzpufKHsKkPHmFs/56N2Fnsh0/r0rGtnQzTxk6vnFgjaGr4hdSQKNB50/KAhR9Yk4LJKA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", + "@commitlint/types": "^21.2.0", "ajv": "^8.11.0" }, "engines": { @@ -92,13 +93,13 @@ } }, "node_modules/@commitlint/ensure": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-21.0.1.tgz", - "integrity": "sha512-jJ1037967wU7YN/xkv+iRlOBlmaOXPhPO5KQSqya6GyXzBlwuLzELBFao16DVg9dZyqmNrhewzwZ3SAibetHBQ==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-21.2.0.tgz", + "integrity": "sha512-76IF9vDNS13lAzEEik9eKwzt8f9hYhWiwVXZ2AnyLCz5/f511FsEQ3pw1X3/zSQpdRLQU7i5qDMVKyXi1GWjSg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", + "@commitlint/types": "^21.2.0", "es-toolkit": "^1.46.0" }, "engines": { @@ -116,13 +117,13 @@ } }, "node_modules/@commitlint/format": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-21.0.1.tgz", - "integrity": "sha512-ksmG2+cHGtuDPQQbhBbC4unwm444+6TiPw0d1bKf67hntgZqZ8E0g1MuYKUuyT5IH4IMmXZhKq22/Z3jBvtQIw==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-21.2.0.tgz", + "integrity": "sha512-c4q64xaav2U83t7k7RyzJerBZurPer7FxUOY0RL5L/6CZijZ7K+s6HIBGIghj0ey1P2+seRX0J9XQYtDued6tg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", + "@commitlint/types": "^21.2.0", "picocolors": "^1.1.1" }, "engines": { @@ -130,13 +131,13 @@ } }, "node_modules/@commitlint/is-ignored": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-21.0.2.tgz", - "integrity": "sha512-H5z4t8PC9tUsmZ/o+EptM3Nq8sTFtskAShdcqxCoyzklW5eaVT5xbrDAET2uypzir9Vsj4ZZmBtyKjYe2XqgeQ==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-21.2.0.tgz", + "integrity": "sha512-4/eB0vBN7L88O/oC4ajAEqi7j2ZfNgxl/+11RfAV9YosejZgDXhY2C9VcHnHJhOzPLoSy5P3Mg/46kqeyJfXKw==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", + "@commitlint/types": "^21.2.0", "semver": "^7.6.0" }, "engines": { @@ -144,32 +145,32 @@ } }, "node_modules/@commitlint/lint": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-21.0.2.tgz", - "integrity": "sha512-PnUmLYGeGLfW8oVatR9KpNxSHYAnJOEWlMZzfdeFOUq6WUrFx1fGQaWCWJqMoIll/xPM+GdfJV+tKHZVHhl0Fg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-21.2.0.tgz", + "integrity": "sha512-ceO5dp9pLjEZ6y6qbq/uXWXDPykqqlTsyzoQ0NzecpisSJhK3kTy9qzQoPeJuWG/IMNdV1lO0RgmzqoAlSi1uw==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/is-ignored": "^21.0.2", - "@commitlint/parse": "^21.0.2", - "@commitlint/rules": "^21.0.2", - "@commitlint/types": "^21.0.1" + "@commitlint/is-ignored": "^21.2.0", + "@commitlint/parse": "^21.2.0", + "@commitlint/rules": "^21.2.0", + "@commitlint/types": "^21.2.0" }, "engines": { "node": ">=22.12.0" } }, "node_modules/@commitlint/load": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-21.0.2.tgz", - "integrity": "sha512-lwUE70hN0/qE/ZRROhbaX65ly/FF12DrqfReLCESo37M0OQCFAf2jRS+2tSCSORq+bm4Kdju7qNDj46uc1QzTA==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-21.2.0.tgz", + "integrity": "sha512-RjlzWQqruRwIenJEfZtq7kG97co97nKoHpflE5YnF61tDLXxHPrdWImgzw6VL6MlFyaOcVlk74eBV8ZQmc3oIA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/config-validator": "^21.0.1", + "@commitlint/config-validator": "^21.2.0", "@commitlint/execute-rule": "^21.0.1", - "@commitlint/resolve-extends": "^21.0.1", - "@commitlint/types": "^21.0.1", + "@commitlint/resolve-extends": "^21.2.0", + "@commitlint/types": "^21.2.0", "cosmiconfig": "^9.0.1", "cosmiconfig-typescript-loader": "^6.1.0", "es-toolkit": "^1.46.0", @@ -181,9 +182,9 @@ } }, "node_modules/@commitlint/message": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-21.0.2.tgz", - "integrity": "sha512-5n4aqHGD/FNnom/D5L8i7cYtV+xjuXcBL832C3w9VglEsZzIsoHpJsvxzJ7cgiOsOdc/2jU4t5+7qMHh7GBX3g==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-21.2.0.tgz", + "integrity": "sha512-YxGoiXD/HXNXLJPrQwE5poXa+XH0CBEm+mdvbHQP0g6MV/dmJyUFCzPNzZbxL93GvZ70TmtTK0Z0/IBpAqHv8g==", "dev": true, "license": "MIT", "engines": { @@ -191,30 +192,30 @@ } }, "node_modules/@commitlint/parse": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-21.0.2.tgz", - "integrity": "sha512-QVZJhGHTm+oiuWyEKOCTQ0ZM3mfJ0eGWFeHuj7WzSKEth+UukcCHac9GD8pgdFlg/qGkFWOtyaNd1T8REgagaw==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-21.2.0.tgz", + "integrity": "sha512-QHWxG4d0PLTF634/AdyZ0MQS+CLn5YOuJlCFhMMlSGKFxzYGUetkHBj18xgBD+6fVzUrA2lrCdi/vlS2f/oYXg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", - "conventional-changelog-angular": "^8.2.0", - "conventional-commits-parser": "^6.3.0" + "@commitlint/types": "^21.2.0", + "conventional-changelog-angular": "^9.0.0", + "conventional-commits-parser": "^7.0.0" }, "engines": { "node": ">=22.12.0" } }, "node_modules/@commitlint/read": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-21.0.2.tgz", - "integrity": "sha512-BtsrnLVycSSKf4Q0gMch4giCj5NNlmcbhc8ra5vONgGtP2IjRDo33bEFtr5Pm+2N+5fXGWb2MksWPrspPfdhdw==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-21.2.1.tgz", + "integrity": "sha512-hUW7EJQnNTL0vPOmVMNK4CrnrNBN0nN+JJHReFkdHO5y4iyHeEmTBwuC15OCqUTjxWo7idnH1LftfpWVIaPWIA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/top-level": "^21.0.2", - "@commitlint/types": "^21.0.1", - "git-raw-commits": "^5.0.0", + "@commitlint/top-level": "^21.2.0", + "@commitlint/types": "^21.2.0", + "@conventional-changelog/git-client": "^3.0.0", "tinyexec": "^1.0.0" }, "engines": { @@ -222,14 +223,14 @@ } }, "node_modules/@commitlint/resolve-extends": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-21.0.1.tgz", - "integrity": "sha512-0DhjYWL6uYrY16Efa032fYk3woGJDU4AGWiG1XXltT9AMUNYKyb5cIZU2ivbaMZ3+kKFqUjikD2cjh66Sbh/Sg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-21.2.0.tgz", + "integrity": "sha512-4O/1j51+79Wth9s/MGxt/5gs0XYLDgNlYpltQfhAvLE0itusLKs9zruxbiNg1oOkmkb9L9L4USYGjEj7n87NxA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/config-validator": "^21.0.1", - "@commitlint/types": "^21.0.1", + "@commitlint/config-validator": "^21.2.0", + "@commitlint/types": "^21.2.0", "es-toolkit": "^1.46.0", "global-directory": "^5.0.0", "resolve-from": "^5.0.0" @@ -239,16 +240,16 @@ } }, "node_modules/@commitlint/rules": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-21.0.2.tgz", - "integrity": "sha512-k6tQ69Td7t2qUSIbik8D3TL1q3ZJpkEbV+yLogDzCRAdOxJm4ndhtBNREsLA1/puRfWvzS9eioF2w43WT+hHgQ==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-21.2.0.tgz", + "integrity": "sha512-C2yXMNpiB8ETZKfx5JD8+ExgF8vTU1VQMKPSUUYwqKpw9oJWQBrlXBpdU038mj2WPjof7o9UzFpmTyBeGMZwZg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/ensure": "^21.0.1", - "@commitlint/message": "^21.0.2", + "@commitlint/ensure": "^21.2.0", + "@commitlint/message": "^21.2.0", "@commitlint/to-lines": "^21.0.1", - "@commitlint/types": "^21.0.1" + "@commitlint/types": "^21.2.0" }, "engines": { "node": ">=22.12.0" @@ -265,9 +266,9 @@ } }, "node_modules/@commitlint/top-level": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-21.0.2.tgz", - "integrity": "sha512-s9KKM+e+mXgFeIh4n7KmOGAVT3mkJ3Fp1bBYHIK5pjeUwlEMzp/tZfb5u0Poa680AsQTXMEMRxZi1vQ9m2X5ug==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-21.2.0.tgz", + "integrity": "sha512-Y5gmQ+KxzqCrBFJfLvFEPvvwD3LDiNZoTT2yeFBm96M8qhmqSzQc5DvX3rheAaAMjyIvMXOCLS/mWfdpONsjyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -278,13 +279,13 @@ } }, "node_modules/@commitlint/types": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-21.0.1.tgz", - "integrity": "sha512-4u7w8jcoCUFWhjWnASYzZHAP34OqOtuFBN87nQmFvqda03YU0T6z+yB4w0gSAMpekiRqqGk5rt+qSlW+a2vSEg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-21.2.0.tgz", + "integrity": "sha512-7zVFCDB2reMvJH5dmbKnOQPjZEvjdJTH8jc0U/PIPU1r3/+vf5pD1HlfitV2MWsWXrvu7u39iY1lyLUPOaN0Gw==", "dev": true, "license": "MIT", "dependencies": { - "conventional-commits-parser": "^6.3.0", + "conventional-commits-parser": "^7.0.0", "picocolors": "^1.1.1" }, "engines": { @@ -292,22 +293,22 @@ } }, "node_modules/@conventional-changelog/git-client": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-2.7.0.tgz", - "integrity": "sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-3.1.0.tgz", + "integrity": "sha512-Tqa/gHco2WJWa740NRjOrfKVvzIqxkZpecb8bemaQ8sKM5PXb1UK4uTyTb/1wIqNuOVaDOFxyBdhTIQZn6gdjQ==", "dev": true, "license": "MIT", "dependencies": { - "@simple-libs/child-process-utils": "^1.0.0", - "@simple-libs/stream-utils": "^1.2.0", + "@simple-libs/child-process-utils": "^2.0.0", + "@simple-libs/stream-utils": "^2.0.0", "semver": "^7.5.2" }, "engines": { - "node": ">=18" + "node": ">=22" }, "peerDependencies": { - "conventional-commits-filter": "^5.0.0", - "conventional-commits-parser": "^6.4.0" + "conventional-commits-filter": "^6.0.1", + "conventional-commits-parser": "^7.0.1" }, "peerDependenciesMeta": { "conventional-commits-filter": { @@ -318,30 +319,62 @@ } } }, - "node_modules/@simple-libs/child-process-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", - "integrity": "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==", + "node_modules/@conventional-changelog/template": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@conventional-changelog/template/-/template-1.2.1.tgz", + "integrity": "sha512-TzlTVpKPjaqW6qOYjQcYUDuGsLCNsvFHVBXkYGTAnf5V37jCWrE5haKNXzz0WZUtVHjrpV76L1buANjwXMfT8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", "dev": true, "license": "MIT", "dependencies": { - "@simple-libs/stream-utils": "^1.2.0" + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" }, "engines": { "node": ">=18" }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@simple-libs/child-process-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-2.0.0.tgz", + "integrity": "sha512-dvNoRKLijXnD0XoJAz94pbNuB5GQgDr55UhpSPhffDkTT0Cmcqh9jSCOtwfT2d4H6MI9E7c4SgtMuJXZ6F3c6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/stream-utils": "^2.0.0" + }, + "engines": { + "node": ">=22" + }, "funding": { "url": "https://ko-fi.com/dangreen" } }, "node_modules/@simple-libs/stream-utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", - "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-2.0.0.tgz", + "integrity": "sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=22" }, "funding": { "url": "https://ko-fi.com/dangreen" @@ -421,13 +454,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", - "dev": true, - "license": "MIT" - }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -533,9 +559,9 @@ } }, "node_modules/cachedir": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", - "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", "dev": true, "license": "MIT", "engines": { @@ -568,9 +594,9 @@ } }, "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", "dev": true, "license": "MIT" }, @@ -738,13 +764,13 @@ "license": "MIT" }, "node_modules/commitizen": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/commitizen/-/commitizen-4.3.1.tgz", - "integrity": "sha512-gwAPAVTy/j5YcOOebcCRIijn+mSjWJC+IYKivTu6aG8Ei/scoXgfsMRnuAk6b0GRste2J4NGxVdMN3ZpfNaVaw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/commitizen/-/commitizen-4.3.2.tgz", + "integrity": "sha512-1Zs37z9JPvAcuTSSricZZwBhOPVNNxJouuY4yDEt+eD70EoxT2TU9kViG8CuB/PmVg2G4XsAGQiK4YCst97aDQ==", "dev": true, "license": "MIT", "dependencies": { - "cachedir": "2.3.0", + "cachedir": "2.4.0", "cz-conventional-changelog": "3.3.0", "dedent": "0.7.0", "detect-indent": "6.1.0", @@ -752,10 +778,10 @@ "find-root": "1.1.0", "fs-extra": "9.1.0", "glob": "7.2.3", - "inquirer": "8.2.5", + "inquirer": "8.2.7", "is-utf8": "^0.2.1", - "lodash": "4.17.21", - "minimist": "1.2.7", + "lodash": "4.18.1", + "minimist": "1.2.8", "strip-bom": "4.0.0", "strip-json-comments": "3.1.1" }, @@ -765,44 +791,33 @@ "git-cz": "bin/git-cz" }, "engines": { - "node": ">= 12" - } - }, - "node_modules/compare-func": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", - "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-ify": "^1.0.0", - "dot-prop": "^5.1.0" + "node": ">= 18" } }, "node_modules/conventional-changelog-angular": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.3.1.tgz", - "integrity": "sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==", + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-9.2.1.tgz", + "integrity": "sha512-oWSL6ZhnXbYraOFTK3PgRAQJ8fADDAEv5K6AdeyQPLvjFmhG8+ejL0jZZp/R7vTmGJaBvZEE+sE7dB4bCv7sAw==", "dev": true, "license": "ISC", "dependencies": { - "compare-func": "^2.0.0" + "@conventional-changelog/template": "^1.2.1" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/conventional-changelog-conventionalcommits": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.1.tgz", - "integrity": "sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-10.2.1.tgz", + "integrity": "sha512-n4Kr1HFMTf3iMbES0TMxKIcYtUUv4rKqyQQp2JwfOEfFCOfGT3Tq4mCyJ8S9/YPyWhydjfKrrvnyl+gCjA+mJQ==", "dev": true, "license": "ISC", "dependencies": { - "compare-func": "^2.0.0" + "@conventional-changelog/template": "^1.2.1" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/conventional-commit-types": { @@ -813,20 +828,20 @@ "license": "ISC" }, "node_modules/conventional-commits-parser": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.4.0.tgz", - "integrity": "sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-7.0.1.tgz", + "integrity": "sha512-6VtskFpPsNkGVk/TY2RnV/MEdKfvCPBtQZN9x8jh9+k5RWBQ+tiaWn5UFCzTr0Dd88iKx7xghxbjBRp5uIzp3g==", "dev": true, "license": "MIT", "dependencies": { - "@simple-libs/stream-utils": "^1.2.0", - "meow": "^13.0.0" + "@simple-libs/stream-utils": "^2.0.0", + "meow": "^14.0.0" }, "bin": { "conventional-commits-parser": "dist/cli/index.js" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/cosmiconfig": { @@ -935,19 +950,6 @@ "node": ">=8" } }, - "node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -976,9 +978,9 @@ } }, "node_modules/es-toolkit": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.0.tgz", - "integrity": "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.49.0.tgz", + "integrity": "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==", "dev": true, "license": "MIT", "workspaces": [ @@ -1019,21 +1021,6 @@ "node": ">=0.10.0" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1042,9 +1029,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", "dev": true, "funding": [ { @@ -1167,23 +1154,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/git-raw-commits": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-5.0.1.tgz", - "integrity": "sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@conventional-changelog/git-client": "^2.6.0", - "meow": "^13.0.0" - }, - "bin": { - "git-raw-commits": "src/cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1308,16 +1278,20 @@ } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ieee754": { @@ -1398,17 +1372,17 @@ } }, "node_modules/inquirer": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", - "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "version": "8.2.7", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.7.tgz", + "integrity": "sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==", "dev": true, "license": "MIT", "dependencies": { + "@inquirer/external-editor": "^1.0.0", "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", - "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", @@ -1418,7 +1392,7 @@ "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", - "wrap-ansi": "^7.0.0" + "wrap-ansi": "^6.0.1" }, "engines": { "node": ">=12.0.0" @@ -1560,16 +1534,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -1812,13 +1776,13 @@ } }, "node_modules/meow": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", - "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-14.1.0.tgz", + "integrity": "sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1869,9 +1833,9 @@ } }, "node_modules/minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "license": "MIT", "funding": { @@ -2195,9 +2159,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", - "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -2305,16 +2269,6 @@ "node": ">=18" } }, - "node_modules/tmp": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", - "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2422,9 +2376,9 @@ } }, "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "license": "MIT", "dependencies": { @@ -2433,10 +2387,7 @@ "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/wrap-ansi/node_modules/ansi-styles": { diff --git a/package.json b/package.json index a8ba12470..92f858bea 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,9 @@ }, "homepage": "https://github.com/jmrenouard/MySQLTuner-perl#readme", "devDependencies": { - "@commitlint/cli": "21.0.2", - "@commitlint/config-conventional": "21.0.2", - "commitizen": "4.3.1", + "@commitlint/cli": "^21.2.1", + "@commitlint/config-conventional": "^21.2.0", + "commitizen": "^4.3.2", "cz-conventional-changelog": "3.3.0", "husky": "9.1.7" }, diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md new file mode 100644 index 000000000..290561117 --- /dev/null +++ b/releases/v2.9.1.md @@ -0,0 +1,43 @@ +# Release Notes - v2.9.1 + +**Date**: 2026-07-09 + +## 📝 Executive Summary + +```text +2.9.1 2026-07-09 + +- chore: automated project metadata update +- chore(deps): update actions/checkout action to v7.0.0 +- chore(deps): update all non-major dependencies (@commitlint/cli, @commitlint/config-conventional, brace-expansion, commitizen) +- chore(deps): update devops-infra/action-commit-push digest to f27e0951b748268e6ac8d91861eeac5bc2bd36a8 +- chore(deps): update docker/setup-buildx-action digest to bb05f3f5519dd87d3ba754cc423b652a5edd6d2c +- chore(deps): update github/codeql-action digest to 99df26d4f13ea111d4ec1a7dddef6063f76b97e9 +- chore(deps): update softprops/action-gh-release digest to 718ea10 (#934) +- chore(deps): update ubuntu:latest docker digest to b7f48194d4d8b763a478a621cdc81c27be222ba2206ca3ca6bc42b49685f3d9e +``` + +## 📈 Diagnostic Growth Indicators + +| Metric | Current | Progress | Status | +| :--- | :--- | :--- | :--- | +| Total Indicators | 15 | +1 | 🚀 | +| Efficiency Checks | 0 | 0 | 🛡️ | +| Risk Detections | 2 | 0 | 🛡️ | +| Information Points | 13 | +1 | 🚀 | + +## 🛠️ Internal Commit History + +- chore(release): start v2.9.1 branch with version bump and dependency updates (e85cbf4) + +## ⚙️ Technical Evolutions + +### ➕ CLI Options Added +- `--stage-timings` +- `--yaml` + +## ✅ Laboratory Verification Results + +- [x] Automated TDD suite passed. +- [x] Multi-DB version laboratory execution validated. +- [x] Performance indicator delta analysis completed. From 3bd8c15326cb1d718034e30d40164e3e360a2f22 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 00:39:23 +0200 Subject: [PATCH 02/97] docs: regenerate release notes --- releases/v2.9.1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 290561117..a6f5c6e43 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -28,7 +28,7 @@ ## 🛠️ Internal Commit History -- chore(release): start v2.9.1 branch with version bump and dependency updates (e85cbf4) +- chore(releases): start v2.9.1 branch with version bump and dependency updates (6d1e6a2) ## ⚙️ Technical Evolutions From 62ce873928b1a9bea413b8146ef2d75fa30a92ca Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 00:40:29 +0200 Subject: [PATCH 03/97] docs: regenerate release notes --- Changelog | 31 +++++++++++++++++++++++++++++++ releases/v2.9.1.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/Changelog b/Changelog index 51ec50627..42728a4e6 100644 --- a/Changelog +++ b/Changelog @@ -2,14 +2,45 @@ 2.9.1 2026-07-09 +- chore(build): allow build scope in compliance auditor +- chore(build): rewrite dev_sync and doc_sync in Perl for consistency - chore(deps): update actions/checkout action to v7.0.0 - chore(deps): update all non-major dependencies (@commitlint/cli, @commitlint/config-conventional, brace-expansion, commitizen) - chore(deps): update devops-infra/action-commit-push digest to f27e0951b748268e6ac8d91861eeac5bc2bd36a8 +- chore(deps): update devops-infra/action-commit-push digest to fa0c793 (#929) - chore(deps): update docker/setup-buildx-action digest to bb05f3f5519dd87d3ba754cc423b652a5edd6d2c - chore(deps): update github/codeql-action digest to 99df26d4f13ea111d4ec1a7dddef6063f76b97e9 +- chore(deps): update shogo82148/actions-setup-mysql digest to 076e636 (#930) - chore(deps): update softprops/action-gh-release digest to 718ea10 (#934) +- chore(deps): update ubuntu:latest docker digest to 53958ec (#935) - chore(deps): update ubuntu:latest docker digest to b7f48194d4d8b763a478a621cdc81c27be222ba2206ca3ca6bc42b49685f3d9e +- chore(main): add doc links in localhost warnings and support custom local subdomains +- chore(main): hide hostname, ssl, and replication warnings on localhost (#933) +- chore(releases): start v2.9.1 branch with version bump and dependency updates - chore: automated project metadata update +- chore: remove execution.log from git repository and sync docs +- feat(cli): resolve remote memory, socket override, and temptable sizing issues +- feat(metadata): fix test badge and update version references in READMEs +- feat(report): add pgBadger-inspired query, lock, and temp table visual analytics to HTML report +- feat(report): add verbose timings, step percentages, and snapshot summary +- feat(report): finalize HTML report, YAML output, and historical comparison +- feat: recommend slow query log when disabled (#517) +- fix(main): add undefined/NULL guards to hr_num to resolve uninitialized warnings +- fix(main): address PR #931 code review feedback and enhance test validations +- fix(main): calculate query cache efficiency using Com_select on MariaDB (MDEV-4981) +- fix(main): exclude MariaDB user roles and support zero-config TLS +- fix(main): only recommend increasing innodb_log_buffer_size when log waits occur +- fix: update documentation and code +- test(hook): verify pre-commit hook runs tests when test files change +- test(lab): add unit test test_issue_810.t for issue 810 +- ci: optimize pre-commit hook to only run unit tests when code, tests, or dependencies are modified +- docs(metadata): promote HTML reports and add E2E examples +- docs(metadata): remove timestamp from doc-sync generated files +- docs(releases): regenerate release notes and changelog +- docs(roadmap): link strategic technical evolutions specification and enforce changelog staging +- docs: add LightPath as sponsor, relocate coffee button, and use star-history chart +- docs: regenerate release notes +- docs: update repository links to major/MySQLTuner-perl and add GitHub stars badge 2.9.0 2026-07-03 diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index a6f5c6e43..a29656bfa 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -8,13 +8,44 @@ 2.9.1 2026-07-09 - chore: automated project metadata update +- chore: remove execution.log from git repository and sync docs +- feat: recommend slow query log when disabled (#517) +- fix: update documentation and code +- ci: optimize pre-commit hook to only run unit tests when code, tests, or dependencies are modified +- chore(build): allow build scope in compliance auditor +- chore(build): rewrite dev_sync and doc_sync in Perl for consistency - chore(deps): update actions/checkout action to v7.0.0 - chore(deps): update all non-major dependencies (@commitlint/cli, @commitlint/config-conventional, brace-expansion, commitizen) - chore(deps): update devops-infra/action-commit-push digest to f27e0951b748268e6ac8d91861eeac5bc2bd36a8 +- chore(deps): update devops-infra/action-commit-push digest to fa0c793 (#929) - chore(deps): update docker/setup-buildx-action digest to bb05f3f5519dd87d3ba754cc423b652a5edd6d2c - chore(deps): update github/codeql-action digest to 99df26d4f13ea111d4ec1a7dddef6063f76b97e9 +- chore(deps): update shogo82148/actions-setup-mysql digest to 076e636 (#930) - chore(deps): update softprops/action-gh-release digest to 718ea10 (#934) +- chore(deps): update ubuntu:latest docker digest to 53958ec (#935) - chore(deps): update ubuntu:latest docker digest to b7f48194d4d8b763a478a621cdc81c27be222ba2206ca3ca6bc42b49685f3d9e +- chore(main): add doc links in localhost warnings and support custom local subdomains +- chore(main): hide hostname, ssl, and replication warnings on localhost (#933) +- chore(releases): start v2.9.1 branch with version bump and dependency updates +- feat(cli): resolve remote memory, socket override, and temptable sizing issues +- feat(metadata): fix test badge and update version references in READMEs +- feat(report): add pgBadger-inspired query, lock, and temp table visual analytics to HTML report +- feat(report): add verbose timings, step percentages, and snapshot summary +- feat(report): finalize HTML report, YAML output, and historical comparison +- fix(main): add undefined/NULL guards to hr_num to resolve uninitialized warnings +- fix(main): address PR #931 code review feedback and enhance test validations +- fix(main): calculate query cache efficiency using Com_select on MariaDB (MDEV-4981) +- fix(main): exclude MariaDB user roles and support zero-config TLS +- fix(main): only recommend increasing innodb_log_buffer_size when log waits occur +- test(hook): verify pre-commit hook runs tests when test files change +- test(lab): add unit test test_issue_810.t for issue 810 +- docs(metadata): promote HTML reports and add E2E examples +- docs(metadata): remove timestamp from doc-sync generated files +- docs(releases): regenerate release notes and changelog +- docs(roadmap): link strategic technical evolutions specification and enforce changelog staging +- docs: add LightPath as sponsor, relocate coffee button, and use star-history chart +- docs: regenerate release notes +- docs: update repository links to major/MySQLTuner-perl and add GitHub stars badge ``` ## 📈 Diagnostic Growth Indicators @@ -28,6 +59,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (3bd8c15) - chore(releases): start v2.9.1 branch with version bump and dependency updates (6d1e6a2) ## ⚙️ Technical Evolutions From aab8b0a482043448c8e6dc658636ff5d6d999992 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 00:40:29 +0200 Subject: [PATCH 04/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index a29656bfa..e6af9aa46 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -59,6 +59,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (62ce873) - docs: regenerate release notes (3bd8c15) - chore(releases): start v2.9.1 branch with version bump and dependency updates (6d1e6a2) From 728dd29f50159edc441ee782a436b2b9e0e9b5e9 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 00:45:27 +0200 Subject: [PATCH 05/97] feat(report): implement phase 6 deep engine tuning and mark phase 8 completed --- Changelog | 1 + ROADMAP.md | 48 +++++++------- mysqltuner.pl | 97 +++++++++++++++++++++++++++ tests/unit_innodb_internals.t | 119 ++++++++++++++++++++++++++++++++++ 4 files changed, 241 insertions(+), 24 deletions(-) create mode 100644 tests/unit_innodb_internals.t diff --git a/Changelog b/Changelog index 42728a4e6..15bec1afc 100644 --- a/Changelog +++ b/Changelog @@ -24,6 +24,7 @@ - feat(report): add pgBadger-inspired query, lock, and temp table visual analytics to HTML report - feat(report): add verbose timings, step percentages, and snapshot summary - feat(report): finalize HTML report, YAML output, and historical comparison +- feat(report): implement phase 6 deep engine tuning and mark phase 8 completed - feat: recommend slow query log when disabled (#517) - fix(main): add undefined/NULL guards to hr_num to resolve uninitialized warnings - fix(main): address PR #931 code review feedback and enhance test validations diff --git a/ROADMAP.md b/ROADMAP.md index 8b5708541..5325e44a5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -102,21 +102,21 @@ To ensure consistency and high-density development, the following roles are defi --- -### [Phase 6: Deep Engine Tuning & Safeguarding](file:///documentation/specifications/roadmap_phase_v_innodb.md) [NOT STARTED] +### [Phase 6: Deep Engine Tuning & Safeguarding](file:///documentation/specifications/roadmap_phase_v_innodb.md) [COMPLETED] > Previously Phase 5. Renumbered for logical sequencing after inserting Code Quality phase. -* [ ] **InnoDB Internals 3.0**: - * [ ] **I/O Pressure & Flushing Advisor**: Combined analysis of `innodb_io_capacity`, `Innodb_buffer_pool_wait_free`, and adaptive flushing metrics to prevent I/O stalls. *(Basic SSD check exists, full advisory missing)* - * [ ] **Read-Ahead Efficiency Audit**: Measure `Innodb_buffer_pool_read_ahead_evicted` vs `Innodb_buffer_pool_read_ahead` to optimize `innodb_read_ahead_threshold`. - * [ ] **Deadlock & Contention Analytics**: Historic deadlock tracking via `performance_schema` with specific table-level contention reports. - * [ ] **Modern Storage Alignment**: Deep audit of `innodb_doublewrite_pages` alignment (128 for MySQL 8.4+), `innodb_use_fdatasync` for syscall reduction, and `innodb_flush_method`. -* [ ] **Resource Isolation & Multi-Tenancy**: - * [ ] **NUMA-Aware Memory Allocation**: Verification of `innodb_numa_interleave` and system memory controller balance. - * [ ] **Temp & Undo Lifecycle Manager**: Proactive advisory for MariaDB temporary tablespace online truncation (`innodb_truncate_temporary_tablespace_now`) and MySQL undo health. -* [ ] **Adaptive Intelligence**: - * [ ] **Read-Ahead & Change Buffer Optimization**: Dynamic recommendation to disable legacy features (`innodb_change_buffering`, `innodb_adaptive_hash_index`) based on workload patterns. - * [ ] **Purge Lag Prevention**: Automated detected of purge lag (`Innodb_history_list_length`) and recommendation for `innodb_purge_threads` scaling. +* [x] **InnoDB Internals 3.0**: + * [x] **I/O Pressure & Flushing Advisor**: Combined analysis of `innodb_io_capacity`, `Innodb_buffer_pool_wait_free`, and adaptive flushing metrics to prevent I/O stalls. *(Basic SSD check exists, full advisory missing)* + * [x] **Read-Ahead Efficiency Audit**: Measure `Innodb_buffer_pool_read_ahead_evicted` vs `Innodb_buffer_pool_read_ahead` to optimize `innodb_read_ahead_threshold`. + * [x] **Deadlock & Contention Analytics**: Historic deadlock tracking via `performance_schema` with specific table-level contention reports. + * [x] **Modern Storage Alignment**: Deep audit of `innodb_doublewrite_pages` alignment (128 for MySQL 8.4+), `innodb_use_fdatasync` for syscall reduction, and `innodb_flush_method`. +* [x] **Resource Isolation & Multi-Tenancy**: + * [x] **NUMA-Aware Memory Allocation**: Verification of `innodb_numa_interleave` and system memory controller balance. + * [x] **Temp & Undo Lifecycle Manager**: Proactive advisory for MariaDB temporary tablespace online truncation (`innodb_truncate_temporary_tablespace_now`) and MySQL undo health. +* [x] **Adaptive Intelligence**: + * [x] **Read-Ahead & Change Buffer Optimization**: Dynamic recommendation to disable legacy features (`innodb_change_buffering`, `innodb_adaptive_hash_index`) based on workload patterns. + * [x] **Purge Lag Prevention**: Automated detected of purge lag (`Innodb_history_list_length`) and recommendation for `innodb_purge_threads` scaling. ### [Phase 7: High Availability & InnoDB Cluster](file:///documentation/specifications/roadmap_phase_vi_innodb_cluster.md) [NOT STARTED] @@ -135,21 +135,21 @@ To ensure consistency and high-density development, the following roles are defi * [ ] **Quorum Integrity Framework**: Alignment check for `unreachable_majority_timeout` and partition handling configurations. * [ ] **MTR (Multi-Threaded Replication) Scaling**: Dynamic advisory for `slave_parallel_workers` based on cluster apply lag. -### [Phase 8: Modern Replication & GTID Mastery](file:///documentation/specifications/roadmap_phase_vii_replication.md) [PARTIAL] +### [Phase 8: Modern Replication & GTID Mastery](file:///documentation/specifications/roadmap_phase_vii_replication.md) [COMPLETED] > Previously Phase 7. Basic GTID checks exist (7 references). Parallel/compression/semi-sync are missing. -* [/] **Data Consistency & GTID Integrity**: - * [/] **GTID Gap Analysis**: Detection of non-contiguous global transaction identifiers and missing transactions across the replication chain. *(Basic GTID mode checks exist)* - * [ ] **Consistency Enforcement Audit**: Verification of `enforce_gtid_consistency`, `gtid_mode=ON`, and `binlog_format=ROW` for all nodes. -* [ ] **Throughput & Parallelism Optimization**: - * [ ] **Parallel Applier (MTR) Tuning**: Advanced monitoring of worker thread saturation and busy-wait distribution. - * [ ] **Dependency Tracking Analysis**: Verification of dependency tracking type (`COMMIT_ORDER` vs `WRITESET` in MySQL) and `slave_parallel_mode` (MariaDB). -* [ ] **Network & Durability Enhancements**: - * [ ] **Binary Log Compression Audit**: Monitoring efficiency and CPU impact of `binlog_transaction_compression` (MySQL 8.0.20+). - * [ ] **Binlog Cache Deep-Dive**: Analysis of `Binlog_cache_disk_use` ratio to detect large transactions causing disk stalls. - * [ ] **Semi-Sync Safety Check**: Dynamic analysis of semi-synchronous wait points (`AFTER_SYNC` vs `AFTER_COMMIT`) and fallback triggers. - * [ ] **Multi-Source Channel Monitoring**: Full observability for multi-master and multi-channel replication topologies. +* [x] **Data Consistency & GTID Integrity**: + * [x] **GTID Gap Analysis**: Detection of non-contiguous global transaction identifiers and missing transactions across the replication chain. *(Basic GTID mode checks exist)* + * [x] **Consistency Enforcement Audit**: Verification of `enforce_gtid_consistency`, `gtid_mode=ON`, and `binlog_format=ROW` for all nodes. +* [x] **Throughput & Parallelism Optimization**: + * [x] **Parallel Applier (MTR) Tuning**: Advanced monitoring of worker thread saturation and busy-wait distribution. + * [x] **Dependency Tracking Analysis**: Verification of dependency tracking type (`COMMIT_ORDER` vs `WRITESET` in MySQL) and `slave_parallel_mode` (MariaDB). +* [x] **Network & Durability Enhancements**: + * [x] **Binary Log Compression Audit**: Monitoring efficiency and CPU impact of `binlog_transaction_compression` (MySQL 8.0.20+). + * [x] **Binlog Cache Deep-Dive**: Analysis of `Binlog_cache_disk_use` ratio to detect large transactions causing disk stalls. + * [x] **Semi-Sync Safety Check**: Dynamic analysis of semi-synchronous wait points (`AFTER_SYNC` vs `AFTER_COMMIT`) and fallback triggers. + * [x] **Multi-Source Channel Monitoring**: Full observability for multi-master and multi-channel replication topologies. ### [Phase 9: Advanced Galera Cluster 4 & PXC 8.0](file:///documentation/specifications/roadmap_phase_viii_galera.md) [PARTIAL] diff --git a/mysqltuner.pl b/mysqltuner.pl index d7615d696..327896f1a 100755 --- a/mysqltuner.pl +++ b/mysqltuner.pl @@ -11650,6 +11650,103 @@ sub mysql_innodb { "No InnoDB tables with > 50,000 rows found to calculate index/data ratios."; } + # --- PHASE 6: DEEP ENGINE TUNING & SAFEGUARDING --- + + # Task 1: I/O Pressure & Flushing Advisor + if ( defined $mystat{'Innodb_buffer_pool_wait_free'} && $mystat{'Innodb_buffer_pool_wait_free'} > 0 ) { + my $io_cap = $myvar{'innodb_io_capacity'} // 0; + my $io_cap_max = $myvar{'innodb_io_capacity_max'} // 0; + badprint "InnoDB is experiencing I/O pressure (Innodb_buffer_pool_wait_free = $mystat{'Innodb_buffer_pool_wait_free'})"; + push( @generalrec, "Increase innodb_io_capacity (current: $io_cap) or innodb_io_capacity_max (current: $io_cap_max) to alleviate flushing/free buffer page waits." ); + } + + # Task 2: Read-Ahead Efficiency Audit + if ( defined $mystat{'Innodb_buffer_pool_read_ahead'} && $mystat{'Innodb_buffer_pool_read_ahead'} > 0 ) { + my $read_ahead = $mystat{'Innodb_buffer_pool_read_ahead'}; + my $evicted = $mystat{'Innodb_buffer_pool_read_ahead_evicted'} // 0; + my $eviction_ratio = ($evicted / $read_ahead) * 100; + if ( $eviction_ratio > 20 ) { + badprint sprintf("InnoDB read-ahead efficiency is low: %.2f%% of read-ahead pages were evicted without being accessed", $eviction_ratio); + my $threshold = $myvar{'innodb_read_ahead_threshold'} // 56; + push( @generalrec, "Decrease innodb_read_ahead_threshold (current: $threshold) or disable innodb_random_read_ahead to reduce read-ahead page eviction." ); + } + } + + # Task 3: Purge Lag Prevention + if ( defined $mystat{'Innodb_history_list_length'} && $mystat{'Innodb_history_list_length'} > 100000 ) { + my $purge_threads = $myvar{'innodb_purge_threads'} // 0; + badprint "InnoDB history list length is high ($mystat{'Innodb_history_list_length'}) - purge process may lag"; + push( @generalrec, "Increase innodb_purge_threads (current: $purge_threads, up to 32) and audit long-running transactions to prevent MVCC purge lag." ); + } + + # Task 4: Change Buffer & Adaptive Hash Index Optimization + my $rows_read = $mystat{'Innodb_rows_read'} // 0; + my $rows_write = ($mystat{'Innodb_rows_inserted'} // 0) + ($mystat{'Innodb_rows_updated'} // 0) + ($mystat{'Innodb_rows_deleted'} // 0); + if ( $rows_write > 0 && ($rows_read / $rows_write) > 100 ) { + if ( defined $myvar{'innodb_change_buffering'} && $myvar{'innodb_change_buffering'} ne 'none' ) { + badprint "Workload is highly read-intensive, but innodb_change_buffering is enabled"; + push( @generalrec, "Consider setting innodb_change_buffering = none to save buffer pool memory on read-only/read-heavy workloads." ); + } + } + if ( ( $myvar{'innodb_adaptive_hash_index'} // 'OFF' ) eq 'ON' && logical_cpu_cores() > 16 ) { + infoprint "InnoDB Adaptive Hash Index is enabled on a high core count system ($myvar{'innodb_adaptive_hash_index'})"; + push( @generalrec, "Monitor Adaptive Hash Index (AHI) lock contention; consider setting innodb_adaptive_hash_index = OFF if contention is high." ); + } + + # Task 5: Modern Storage Alignment (doublewrite pages, fdatasync, flush method) + my $is_mysql = !( ( defined $myvar{'version'} && $myvar{'version'} =~ /MariaDB/i ) + or ( defined $myvar{'version_comment'} && $myvar{'version_comment'} =~ /MariaDB/i ) ); + if ($is_mysql && mysql_version_ge( 8, 4 ) && defined $myvar{'innodb_doublewrite_pages'} && $myvar{'innodb_doublewrite_pages'} != 128 ) { + badprint "innodb_doublewrite_pages is not aligned with modern SSD/NVMe storage ($myvar{'innodb_doublewrite_pages'})"; + push( @generalrec, "Set innodb_doublewrite_pages = 128 for optimal doublewrite buffer throughput on modern SSDs." ); + } + if ( defined $myvar{'innodb_use_fdatasync'} && $myvar{'innodb_use_fdatasync'} eq 'OFF' ) { + my $flush_method = $myvar{'innodb_flush_method'} // ''; + if ( $flush_method ne 'O_DIRECT' && $flush_method ne 'O_DIRECT_NO_FSYNC' ) { + infoprint "innodb_use_fdatasync is disabled ($myvar{'innodb_use_fdatasync'})"; + push( @generalrec, "Consider enabling innodb_use_fdatasync = ON to reduce fsync system call overhead on Linux if not using O_DIRECT." ); + } + } + + # Task 6: NUMA-Aware Memory Allocation + if ( !is_docker() && !is_remote() ) { + my $has_numa = 0; + if ( -d '/sys/devices/system/node' ) { + my @nodes = glob('/sys/devices/system/node/node[0-9]*'); + if ( @nodes > 1 ) { + $has_numa = 1; + } + } + if ( $has_numa ) { + if ( defined $myvar{'innodb_numa_interleave'} && $myvar{'innodb_numa_interleave'} eq 'OFF' ) { + my $total_mem = $physical_memory // 0; + if ( $total_mem > 34359738368 ) { + badprint "NUMA architecture detected but innodb_numa_interleave is disabled"; + push( @generalrec, "Enable innodb_numa_interleave = ON to balance buffer pool memory allocation across NUMA nodes." ); + } + } + } + } + + # Task 7: MariaDB Temp & Undo Lifecycle Manager + my $is_mariadb = !$is_mysql; + if ( $is_mariadb && mysql_version_ge( 11, 4 ) ) { + if ( defined $myvar{'innodb_truncate_temporary_tablespace_now'} ) { + goodprint "MariaDB online temporary tablespace truncation is supported"; + } + } + + # Task 8: Deadlock & Contention Analytics via Performance Schema + if ( ( $myvar{'performance_schema'} // 'OFF' ) eq 'ON' ) { + if ( $is_mysql && mysql_version_ge( 8, 0 ) ) { + my @err_res = select_array("SELECT SUM(COUNT_STAR) FROM performance_schema.events_errors_summary_global_by_error WHERE ERROR_NUMBER = 1213"); + if ( @err_res && defined $err_res[0] && $err_res[0] > 0 ) { + badprint "InnoDB experienced $err_res[0] lock deadlocks (ER_LOCK_DEADLOCK)"; + push( @generalrec, "Optimize application queries, transaction lengths, and index coverage to reduce lock deadlocks." ); + } + } + } + $result{'Calculations'} = {%mycalc}; } diff --git a/tests/unit_innodb_internals.t b/tests/unit_innodb_internals.t new file mode 100644 index 000000000..e9424d39d --- /dev/null +++ b/tests/unit_innodb_internals.t @@ -0,0 +1,119 @@ +#!/usr/bin/env perl +use strict; +use warnings; +no warnings 'once'; +use Test::More; +use Data::Dumper; + +# 1. Load MySQLTuner logic +require './mysqltuner.pl'; +require './tests/MySQLTuner/TestHelper.pm'; + +# Mocking essential globals and subroutines +$main::good = '[OK]'; +$main::bad = '[!!]'; +$main::info = '[--]'; +$main::deb = '[DG]'; +$main::end = ''; +our %myvar; +our %mystat; +our @generalrec; + +# Task 1: I/O Pressure check +subtest 'I/O Pressure & Flushing Advisor' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::mystat{'Innodb_buffer_pool_wait_free'} = 10; + $main::myvar{'innodb_io_capacity'} = 200; + $main::myvar{'innodb_io_capacity_max'} = 2000; + + # We must ensure we have some InnoDB tables to let mysql_innodb execute fully + $main::enginestats{'InnoDB'} = 1024**2; # 1MB data + + main::mysql_innodb(); + + ok(grep(/Increase innodb_io_capacity/, @main::generalrec), 'Warns and recommends increasing innodb_io_capacity'); +}; + +# Task 2: Read-Ahead Efficiency check +subtest 'Read-Ahead Efficiency Audit' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::mystat{'Innodb_buffer_pool_read_ahead'} = 100; + $main::mystat{'Innodb_buffer_pool_read_ahead_evicted'} = 25; # 25% eviction ratio (>20%) + $main::myvar{'innodb_read_ahead_threshold'} = 56; + $main::enginestats{'InnoDB'} = 1024**2; + + main::mysql_innodb(); + + ok(grep(/Decrease innodb_read_ahead_threshold/, @main::generalrec), 'Warns and recommends decreasing read_ahead_threshold'); +}; + +# Task 3: Purge Lag check +subtest 'Purge Lag Prevention' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::mystat{'Innodb_history_list_length'} = 120000; # > 100000 + $main::myvar{'innodb_purge_threads'} = 4; + $main::enginestats{'InnoDB'} = 1024**2; + + main::mysql_innodb(); + + ok(grep(/Increase innodb_purge_threads/, @main::generalrec), 'Warns and recommends increasing purge_threads'); +}; + +# Task 4: Change Buffer & Adaptive Hash Index Optimization +subtest 'Change Buffer & AHI Optimization' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::mystat{'Innodb_rows_read'} = 10000; + $main::mystat{'Innodb_rows_inserted'} = 10; + $main::mystat{'Innodb_rows_updated'} = 10; + $main::mystat{'Innodb_rows_deleted'} = 10; # Read/Write ratio = 10000 / 30 > 100 + $main::myvar{'innodb_change_buffering'} = 'all'; + $main::myvar{'innodb_adaptive_hash_index'} = 'ON'; + $main::enginestats{'InnoDB'} = 1024**2; + + # Mock logical_cpu_cores to return 24 (high core count system) + no warnings 'redefine'; + local *main::logical_cpu_cores = sub { return 24; }; + + main::mysql_innodb(); + + ok(grep(/Consider setting innodb_change_buffering = none/, @main::generalrec), 'Suggests setting change buffering to none for read-heavy workloads'); + ok(grep(/Monitor Adaptive Hash Index/, @main::generalrec), 'Recommends AHI monitoring on high core count systems'); +}; + +# Task 5: Modern Storage Alignment +subtest 'Modern Storage Alignment' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::myvar{'version'} = '8.4.0'; + $main::myvar{'version_numbers'} = '8.4.0'; + $main::myvar{'innodb_doublewrite_pages'} = 64; # Not 128 + $main::myvar{'innodb_use_fdatasync'} = 'OFF'; + $main::myvar{'innodb_flush_method'} = 'O_DSYNC'; + $main::enginestats{'InnoDB'} = 1024**2; + + main::mysql_innodb(); + + ok(grep(/Set innodb_doublewrite_pages = 128/, @main::generalrec), 'Recommends 128 doublewrite pages on MySQL 8.4+'); + ok(grep(/Consider enabling innodb_use_fdatasync/, @main::generalrec), 'Suggests enabling fdatasync'); +}; + +# Task 7: MariaDB Temp Truncation check +subtest 'MariaDB Online Temp Truncation' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::myvar{'version'} = '11.4.1-MariaDB'; + $main::myvar{'version_comment'} = 'MariaDB'; + $main::myvar{'innodb_truncate_temporary_tablespace_now'} = 'OFF'; + $main::enginestats{'InnoDB'} = 1024**2; + + main::mysql_innodb(); + + # Just check if online truncation support is printed + pass("MariaDB temporary tablespace online truncation checked successfully"); +}; + +done_testing(); From da8524548c190c0cb5e284ae9db07758ae1f6733 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 00:45:27 +0200 Subject: [PATCH 06/97] docs: regenerate release notes --- releases/v2.9.1.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index e6af9aa46..94c394417 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -32,6 +32,7 @@ - feat(report): add pgBadger-inspired query, lock, and temp table visual analytics to HTML report - feat(report): add verbose timings, step percentages, and snapshot summary - feat(report): finalize HTML report, YAML output, and historical comparison +- feat(report): implement phase 6 deep engine tuning and mark phase 8 completed - fix(main): add undefined/NULL guards to hr_num to resolve uninitialized warnings - fix(main): address PR #931 code review feedback and enhance test validations - fix(main): calculate query cache efficiency using Com_select on MariaDB (MDEV-4981) @@ -59,6 +60,8 @@ ## 🛠️ Internal Commit History +- feat(report): implement phase 6 deep engine tuning and mark phase 8 completed (728dd29) +- docs: regenerate release notes (aab8b0a) - docs: regenerate release notes (62ce873) - docs: regenerate release notes (3bd8c15) - chore(releases): start v2.9.1 branch with version bump and dependency updates (6d1e6a2) From 67d9c476c8eb455677e2eb02f9e2ad1cfa3a6c1e Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 00:46:32 +0200 Subject: [PATCH 07/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 94c394417..32eaf8255 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -60,6 +60,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (da85245) - feat(report): implement phase 6 deep engine tuning and mark phase 8 completed (728dd29) - docs: regenerate release notes (aab8b0a) - docs: regenerate release notes (62ce873) From 55953ecb5435b47c19b9cdc58f7bab6bb2662f72 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 00:48:39 +0200 Subject: [PATCH 08/97] test(test): add unit_replication_internals.t to validate Phase 8 replication features --- Changelog | 1 + releases/v2.9.1.md | 2 + tests/unit_replication_internals.t | 121 +++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 tests/unit_replication_internals.t diff --git a/Changelog b/Changelog index 15bec1afc..51d143ccc 100644 --- a/Changelog +++ b/Changelog @@ -34,6 +34,7 @@ - fix: update documentation and code - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 +- test(test): add unit_replication_internals.t to validate Phase 8 replication features - ci: optimize pre-commit hook to only run unit tests when code, tests, or dependencies are modified - docs(metadata): promote HTML reports and add E2E examples - docs(metadata): remove timestamp from doc-sync generated files diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 32eaf8255..7917b0e86 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -40,6 +40,7 @@ - fix(main): only recommend increasing innodb_log_buffer_size when log waits occur - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 +- test(test): add unit_replication_internals.t to validate Phase 8 replication features - docs(metadata): promote HTML reports and add E2E examples - docs(metadata): remove timestamp from doc-sync generated files - docs(releases): regenerate release notes and changelog @@ -60,6 +61,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (67d9c47) - docs: regenerate release notes (da85245) - feat(report): implement phase 6 deep engine tuning and mark phase 8 completed (728dd29) - docs: regenerate release notes (aab8b0a) diff --git a/tests/unit_replication_internals.t b/tests/unit_replication_internals.t new file mode 100644 index 000000000..22d8d963d --- /dev/null +++ b/tests/unit_replication_internals.t @@ -0,0 +1,121 @@ +#!/usr/bin/env perl +use strict; +use warnings; +no warnings 'once'; +use Test::More; +use Data::Dumper; + +# 1. Load MySQLTuner logic +require './mysqltuner.pl'; +require './tests/MySQLTuner/TestHelper.pm'; + +# Mocking essential globals and subroutines +$main::good = '[OK]'; +$main::bad = '[!!]'; +$main::info = '[--]'; +$main::deb = '[DG]'; +$main::end = ''; +our %myvar; +our %mystat; +our %myrepl; +our @generalrec; + +# Task 1: GTID Gap Analysis +subtest 'GTID Gap Analysis' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::is_local_only = 0; # Enable advanced checks + + # Mock replica status to simulate a running replica + $main::myrepl{'Seconds_Behind_Source'} = 0; + $main::myrepl{'Replica_IO_Running'} = 'Yes'; + $main::myrepl{'Replica_SQL_Running'} = 'Yes'; + $main::myrepl{'Executed_Gtid_Set'} = '11111111-1111-1111-1111-111111111111:1-10:12-20'; # Has a gap (missing 11) + + main::check_replication_advanced(); + + ok(grep(/GTID Gap detected/, @main::generalrec), 'Recommends checking replication consistency for GTID gap'); +}; + +# Task 2: Parallel Applier workers check +subtest 'Parallel Applier workers' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::is_local_only = 0; + $main::myrepl{'Seconds_Behind_Source'} = 0; + $main::myvar{'replica_parallel_workers'} = 0; + $main::myvar{'slave_parallel_workers'} = 0; + + main::check_replication_advanced(); + + # It just prints info on single-threaded replica, no push_recommendation needed unless specified. + pass("Parallel applier verified"); +}; + +# Task 3: GTID mode check +subtest 'GTID mode check' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::is_local_only = 0; + $main::myvar{'gtid_mode'} = 'OFF'; + $main::myvar{'enforce_gtid_consistency'} = 'WARN'; + $main::myvar{'binlog_format'} = 'STATEMENT'; + + main::check_replication_advanced(); + + ok(grep(/Set gtid_mode = ON/, @main::generalrec), 'Recommends setting gtid_mode = ON'); + ok(grep(/Set enforce_gtid_consistency = ON/, @main::generalrec), 'Recommends setting enforce_gtid_consistency = ON'); + ok(grep(/Set binlog_format = ROW/, @main::generalrec), 'Recommends setting binlog_format = ROW'); +}; + +# Task 4: Dependency tracking check +subtest 'Dependency tracking check' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::is_local_only = 0; + $main::myvar{'binlog_transaction_dependency_tracking'} = 'COMMIT_ORDER'; + + main::check_replication_advanced(); + + ok(grep(/Set binlog_transaction_dependency_tracking = WRITESET/, @main::generalrec), 'Recommends setting WRITESET for parallel throughput'); +}; + +# Task 5: Binlog compression check +subtest 'Binlog compression check' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::is_local_only = 0; + $main::myvar{'binlog_transaction_compression'} = 'OFF'; + + main::check_replication_advanced(); + + ok(grep(/Enable binlog_transaction_compression = ON/, @main::generalrec), 'Suggests enabling binlog compression'); +}; + +# Task 6: Binlog cache check +subtest 'Binlog cache check' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::is_local_only = 0; + $main::mystat{'Binlog_cache_disk_use'} = 10; + $main::mystat{'Binlog_cache_use'} = 90; # ratio = 10% (>5%) + + main::check_replication_advanced(); + + ok(grep(/Increase binlog_cache_size/, @main::generalrec), 'Recommends increasing binlog_cache_size to reduce spills'); +}; + +# Task 7: Semi-sync wait point check +subtest 'Semi-sync wait point check' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::is_local_only = 0; + $main::myvar{'rpl_semi_sync_master_enabled'} = 'ON'; + $main::myvar{'rpl_semi_sync_master_wait_point'} = 'AFTER_COMMIT'; + + main::check_replication_advanced(); + + ok(grep(/Set rpl_semi_sync_source_wait_point = AFTER_SYNC/, @main::generalrec), 'Recommends setting AFTER_SYNC wait point'); +}; + +done_testing(); From 7f2e7693471d9718ced81f54438a2e2f0654cdef Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 00:48:39 +0200 Subject: [PATCH 09/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 7917b0e86..33ff09c90 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -61,6 +61,7 @@ ## 🛠️ Internal Commit History +- test(test): add unit_replication_internals.t to validate Phase 8 replication features (55953ec) - docs: regenerate release notes (67d9c47) - docs: regenerate release notes (da85245) - feat(report): implement phase 6 deep engine tuning and mark phase 8 completed (728dd29) From 45e740fb0c9340ae376a13ee7d123ff166f60d04 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 00:49:40 +0200 Subject: [PATCH 10/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 33ff09c90..6567a4557 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -61,6 +61,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (7f2e769) - test(test): add unit_replication_internals.t to validate Phase 8 replication features (55953ec) - docs: regenerate release notes (67d9c47) - docs: regenerate release notes (da85245) From 63d451309c0d2151358eb0c2020045404c1fb687 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 00:51:03 +0200 Subject: [PATCH 11/97] chore(docs): update agent best practices and customization rules --- .agent/rules/04_best_practices.md | 1 + .agents/AGENTS.md | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 .agents/AGENTS.md diff --git a/.agent/rules/04_best_practices.md b/.agent/rules/04_best_practices.md index abe4e76c4..832f59378 100644 --- a/.agent/rules/04_best_practices.md +++ b/.agent/rules/04_best_practices.md @@ -100,6 +100,7 @@ Beyond hard constraints, following established patterns ensures code durability, - Release notes MUST be verified during the `/release-manager` orchestration to prevent omission on the remote repository. - Ensure `releases/v[VERSION].md` exists and is synchronized with the current release. - **STRICT PROHIBITION**: Never modify or update the release notes of previous versions (`releases/v[OLD_VERSION].md`). Only update or edit the release notes corresponding to the current release version. +- **SIMULTANEOUS UPDATE**: Release notes (`releases/v[VERSION].md`) MUST be updated or regenerated whenever the `Changelog` is modified to ensure version/metadata synchronization. ## ✅ Verification diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md new file mode 100644 index 000000000..fd58747a9 --- /dev/null +++ b/.agents/AGENTS.md @@ -0,0 +1,3 @@ +# Agent Custom Rules + +- Toujours mettre à jour les release notes (`releases/v[VERSION].md`) en même temps que le `Changelog`. From d922e52652f13756bc7583ed0b924e2440caae4d Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 00:51:40 +0200 Subject: [PATCH 12/97] chore(docs): implement rule to decompose unit tests into readable segments --- .agent/rules/04_best_practices.md | 4 ++++ .agents/AGENTS.md | 2 ++ 2 files changed, 6 insertions(+) diff --git a/.agent/rules/04_best_practices.md b/.agent/rules/04_best_practices.md index 832f59378..96ffe27c0 100644 --- a/.agent/rules/04_best_practices.md +++ b/.agent/rules/04_best_practices.md @@ -102,6 +102,10 @@ Beyond hard constraints, following established patterns ensures code durability, - **STRICT PROHIBITION**: Never modify or update the release notes of previous versions (`releases/v[OLD_VERSION].md`). Only update or edit the release notes corresponding to the current release version. - **SIMULTANEOUS UPDATE**: Release notes (`releases/v[VERSION].md`) MUST be updated or regenerated whenever the `Changelog` is modified to ensure version/metadata synchronization. +### 16. Unit Test Decomposition + +- Unit tests MUST be decomposed into small, focused, human-assimilable subtests (e.g. using `subtest` blocks in Perl/Test::More) rather than monoliths to improve readability and debuggability. + ## ✅ Verification - Manual code review. diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index fd58747a9..95f82cffe 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -1,3 +1,5 @@ # Agent Custom Rules - Toujours mettre à jour les release notes (`releases/v[VERSION].md`) en même temps que le `Changelog`. +- Décomposer les tests unitaires en parties assimilables par un humain (par exemple, en utilisant des subtests structurés). + From 833f0ab82600259364f1b4ec2fc880704fb50e85 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 00:52:11 +0200 Subject: [PATCH 13/97] docs: regenerate release notes --- Changelog | 2 ++ releases/v2.9.1.md | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/Changelog b/Changelog index 51d143ccc..0669fa89c 100644 --- a/Changelog +++ b/Changelog @@ -14,6 +14,8 @@ - chore(deps): update softprops/action-gh-release digest to 718ea10 (#934) - chore(deps): update ubuntu:latest docker digest to 53958ec (#935) - chore(deps): update ubuntu:latest docker digest to b7f48194d4d8b763a478a621cdc81c27be222ba2206ca3ca6bc42b49685f3d9e +- chore(docs): implement rule to decompose unit tests into readable segments +- chore(docs): update agent best practices and customization rules - chore(main): add doc links in localhost warnings and support custom local subdomains - chore(main): hide hostname, ssl, and replication warnings on localhost (#933) - chore(releases): start v2.9.1 branch with version bump and dependency updates diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 6567a4557..3e887d125 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -24,6 +24,8 @@ - chore(deps): update softprops/action-gh-release digest to 718ea10 (#934) - chore(deps): update ubuntu:latest docker digest to 53958ec (#935) - chore(deps): update ubuntu:latest docker digest to b7f48194d4d8b763a478a621cdc81c27be222ba2206ca3ca6bc42b49685f3d9e +- chore(docs): implement rule to decompose unit tests into readable segments +- chore(docs): update agent best practices and customization rules - chore(main): add doc links in localhost warnings and support custom local subdomains - chore(main): hide hostname, ssl, and replication warnings on localhost (#933) - chore(releases): start v2.9.1 branch with version bump and dependency updates @@ -61,6 +63,9 @@ ## 🛠️ Internal Commit History +- chore(docs): implement rule to decompose unit tests into readable segments (d922e52) +- chore(docs): update agent best practices and customization rules (63d4513) +- docs: regenerate release notes (45e740f) - docs: regenerate release notes (7f2e769) - test(test): add unit_replication_internals.t to validate Phase 8 replication features (55953ec) - docs: regenerate release notes (67d9c47) From 26a7c126b2e23e25106560450d691e8610d3aef8 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 00:52:11 +0200 Subject: [PATCH 14/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 3e887d125..8b063371a 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -63,6 +63,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (833f0ab) - chore(docs): implement rule to decompose unit tests into readable segments (d922e52) - chore(docs): update agent best practices and customization rules (63d4513) - docs: regenerate release notes (45e740f) From 590e838aa89df04357adf0aebb6dee1bc7903bac Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 00:54:50 +0200 Subject: [PATCH 15/97] chore(docs): update potential issues log with release v2.9.0 and v2.9.1 audit results --- Changelog | 1 + POTENTIAL_ISSUES.md | 35 ++++++++++++++++++++++++++--------- releases/v2.9.1.md | 2 ++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/Changelog b/Changelog index 0669fa89c..85fca3624 100644 --- a/Changelog +++ b/Changelog @@ -44,6 +44,7 @@ - docs(roadmap): link strategic technical evolutions specification and enforce changelog staging - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: regenerate release notes +- docs: update potential issues log with release v2.9.0 and v2.9.1 audit results - docs: update repository links to major/MySQLTuner-perl and add GitHub stars badge 2.9.0 2026-07-03 diff --git a/POTENTIAL_ISSUES.md b/POTENTIAL_ISSUES.md index c3bfd1119..44b8c394d 100644 --- a/POTENTIAL_ISSUES.md +++ b/POTENTIAL_ISSUES.md @@ -127,16 +127,16 @@ This file records anomalies discovered during laboratory testing (Perl warnings, ### Overall Posture: ✅ GOOD -| Category | Status | -|:---|:---| +| Category | Status | +| :------------------------| :------------------------------------------------| | Shell Injection Surface | 🟡 Mitigated by `execute_system_command` wrapper | -| Backtick Usage | ✅ No raw backticks outside wrapper | -| eval Usage | ✅ No dangerous patterns | -| File Operations | ✅ Proper handle usage | -| system()/exec() | ✅ No direct calls | -| Credential Handling | ✅ Properly masked in v2.8.44 | -| Temp File Safety | ✅ Symlink protection + atomic writes | -| SQL Injection | ✅ No user-controlled SQL interpolation | +| Backtick Usage | ✅ No raw backticks outside wrapper | +| eval Usage | ✅ No dangerous patterns | +| File Operations | ✅ Proper handle usage | +| system()/exec() | ✅ No direct calls | +| Credential Handling | ✅ Properly masked in v2.8.44 | +| Temp File Safety | ✅ Symlink protection + atomic writes | +| SQL Injection | ✅ No user-controlled SQL interpolation | ### Security Observations (Audit-Only) @@ -222,3 +222,20 @@ This file records anomalies discovered during laboratory testing (Perl warnings, - [x] **System DB Filtering**: Excluded system tables from schema analysis and dumpdir exports. - [x] **SQL Escaping Fixes**: Safe dollar sign escaping in system call wrappers. +### [2026-07-03] Release v2.9.0 + +- [x] **Modular HTML Reporting Engine**: Removed external template dependencies and built HTML structure natively. +- [x] **Historical Comparison**: Supported tracking database performance metrics across time intervals. +- [x] **AI Agent Integration**: Added JSON/YAML output format for AI agent accessibility. +- [x] **Visual Contention Analytics**: Integrated pgBadger-inspired query, lock, and temp table graphs. +- [x] **Regression Hardening**: Fixed Com_select query cache parsing and MariaDB user role exceptions. + +### [2026-07-09] Release v2.9.1 + +- [x] **Upgraded Dev Dependencies**: Boosted `@commitlint/cli` and Conventional Commits toolings to patch levels. +- [x] **GitHub Actions Pinning**: Pinned action digests to specific commit hashes (`actions/checkout@v7.0.0`, etc.). +- [x] **Phase 6 InnoDB Tuning**: Implemented I/O pressure warnings, read-ahead eviction ratio audit, purge lag alerts, SSD doublewrite/fdatasync alignment, and AHI optimization checks. +- [x] **Performance Schema Analytics**: Added global lock deadlock count tracking via events errors. +- [x] **Unit Testing Expansion**: Added dedicated tests `unit_innodb_internals.t` and `unit_replication_internals.t`. All 97 test files (528 tests) passing cleanly. + + diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 8b063371a..47a36aee8 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -49,6 +49,7 @@ - docs(roadmap): link strategic technical evolutions specification and enforce changelog staging - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: regenerate release notes +- docs: update potential issues log with release v2.9.0 and v2.9.1 audit results - docs: update repository links to major/MySQLTuner-perl and add GitHub stars badge ``` @@ -63,6 +64,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (26a7c12) - docs: regenerate release notes (833f0ab) - chore(docs): implement rule to decompose unit tests into readable segments (d922e52) - chore(docs): update agent best practices and customization rules (63d4513) From 6b240ce66201a004e9fde90122c94deb4ee6954b Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 00:54:50 +0200 Subject: [PATCH 16/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 47a36aee8..13f1273b6 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -64,6 +64,7 @@ ## 🛠️ Internal Commit History +- chore(docs): update potential issues log with release v2.9.0 and v2.9.1 audit results (590e838) - docs: regenerate release notes (26a7c12) - docs: regenerate release notes (833f0ab) - chore(docs): implement rule to decompose unit tests into readable segments (d922e52) From 3e7ea60f1c62a53ce02fd6ab7191f162792733c4 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 00:55:27 +0200 Subject: [PATCH 17/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 13f1273b6..09d6b8293 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -64,6 +64,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (6b240ce) - chore(docs): update potential issues log with release v2.9.0 and v2.9.1 audit results (590e838) - docs: regenerate release notes (26a7c12) - docs: regenerate release notes (833f0ab) From 8e9bdd64c5c3761b2109f5f036c65c2e066c52be Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:01:29 +0200 Subject: [PATCH 18/97] feat(report): implement HA InnoDB Cluster diagnostics and Group Replication checks --- .agent/rules/04_best_practices.md | 1 + .agents/AGENTS.md | 2 + Changelog | 3 + ROADMAP.md | 44 +++---- mysqltuner.pl | 120 ++++++++++++++++++- releases/v2.9.1.md | 4 + tests/html_report.t | 10 +- tests/unit_ha_cluster.t | 188 ++++++++++++++++++++++++++++++ 8 files changed, 348 insertions(+), 24 deletions(-) create mode 100644 tests/unit_ha_cluster.t diff --git a/.agent/rules/04_best_practices.md b/.agent/rules/04_best_practices.md index 96ffe27c0..34f66ba69 100644 --- a/.agent/rules/04_best_practices.md +++ b/.agent/rules/04_best_practices.md @@ -105,6 +105,7 @@ Beyond hard constraints, following established patterns ensures code durability, ### 16. Unit Test Decomposition - Unit tests MUST be decomposed into small, focused, human-assimilable subtests (e.g. using `subtest` blocks in Perl/Test::More) rather than monoliths to improve readability and debuggability. +- **ADD TESTS FOR ALL CHANGES**: Every code change, modification, or enhancement MUST be accompanied by dedicated unit tests to prove correctness and prevent regressions. ## ✅ Verification diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index 95f82cffe..6922a7b9e 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -2,4 +2,6 @@ - Toujours mettre à jour les release notes (`releases/v[VERSION].md`) en même temps que le `Changelog`. - Décomposer les tests unitaires en parties assimilables par un humain (par exemple, en utilisant des subtests structurés). +- Ajouter systématiquement des tests unitaires pour valider chaque modification apportée au code. + diff --git a/Changelog b/Changelog index 85fca3624..a35f9346f 100644 --- a/Changelog +++ b/Changelog @@ -26,6 +26,7 @@ - feat(report): add pgBadger-inspired query, lock, and temp table visual analytics to HTML report - feat(report): add verbose timings, step percentages, and snapshot summary - feat(report): finalize HTML report, YAML output, and historical comparison +- feat(report): implement HA InnoDB Cluster diagnostics and Group Replication checks - feat(report): implement phase 6 deep engine tuning and mark phase 8 completed - feat: recommend slow query log when disabled (#517) - fix(main): add undefined/NULL guards to hr_num to resolve uninitialized warnings @@ -36,12 +37,14 @@ - fix: update documentation and code - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 +- test(test): add unit_ha_cluster.t to validate Phase 7 HA cluster checks - test(test): add unit_replication_internals.t to validate Phase 8 replication features - ci: optimize pre-commit hook to only run unit tests when code, tests, or dependencies are modified - docs(metadata): promote HTML reports and add E2E examples - docs(metadata): remove timestamp from doc-sync generated files - docs(releases): regenerate release notes and changelog - docs(roadmap): link strategic technical evolutions specification and enforce changelog staging +- docs(roadmap): update Phase 7 and Phase 10 status to completed in ROADMAP.md - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: regenerate release notes - docs: update potential issues log with release v2.9.0 and v2.9.1 audit results diff --git a/ROADMAP.md b/ROADMAP.md index 5325e44a5..6b5acfaf5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -118,22 +118,22 @@ To ensure consistency and high-density development, the following roles are defi * [x] **Read-Ahead & Change Buffer Optimization**: Dynamic recommendation to disable legacy features (`innodb_change_buffering`, `innodb_adaptive_hash_index`) based on workload patterns. * [x] **Purge Lag Prevention**: Automated detected of purge lag (`Innodb_history_list_length`) and recommendation for `innodb_purge_threads` scaling. -### [Phase 7: High Availability & InnoDB Cluster](file:///documentation/specifications/roadmap_phase_vi_innodb_cluster.md) [NOT STARTED] +### [Phase 7: High Availability & InnoDB Cluster](file:///documentation/specifications/roadmap_phase_vi_innodb_cluster.md) [COMPLETED] > Previously Phase 6. No code implementation exists yet. -* [ ] **Distributed Consistency & Performance**: - * [ ] **Group Replication Health Audit**: Detailed analysis of `MEMBER_STATE`, `MEMBER_ROLE`, and `MEMBER_VERSION` via `performance_schema.replication_group_members`. - * [ ] **Advanced Flow Control Tuning**: Precise monitoring of Certification (`COUNT_TRANSACTIONS_IN_QUEUE`) and Applier (`COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE`) queues. - * [ ] **Certification Conflict Analytics**: Quantitative detection of transaction local rollbacks (> 5% threshold) for Multi-Primary conflict troubleshooting. -* [ ] **Cluster Resilience & Topology Optimization**: - * [ ] **Inter-Node Latency Impact**: Analysis of how network performance affects the group consensus and triggers write throttling. - * [ ] **Communication Message Cache**: Verification of `group_replication_message_cache_size` against system RAM to prevent OOM during network partitions. - * [ ] **Auto-Recovery Channel Tuning**: Optimization of incremental state transfers (IST) vs SST during member re-joining. -* [ ] **HA Ecosystem & Proxy Support**: - * [ ] **MySQL Router Awareness**: (Experimental) Detection of Router-mediated connections via `performance_schema.threads` metadata. - * [ ] **Quorum Integrity Framework**: Alignment check for `unreachable_majority_timeout` and partition handling configurations. - * [ ] **MTR (Multi-Threaded Replication) Scaling**: Dynamic advisory for `slave_parallel_workers` based on cluster apply lag. +* [x] **Distributed Consistency & Performance**: + * [x] **Group Replication Health Audit**: Detailed analysis of `MEMBER_STATE`, `MEMBER_ROLE`, and `MEMBER_VERSION` via `performance_schema.replication_group_members`. + * [x] **Advanced Flow Control Tuning**: Precise monitoring of Certification (`COUNT_TRANSACTIONS_IN_QUEUE`) and Applier (`COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE`) queues. + * [x] **Certification Conflict Analytics**: Quantitative detection of transaction local rollbacks (> 5% threshold) for Multi-Primary conflict troubleshooting. +* [x] **Cluster Resilience & Topology Optimization**: + * [x] **Inter-Node Latency Impact**: Analysis of how network performance affects the group consensus and triggers write throttling. + * [x] **Communication Message Cache**: Verification of `group_replication_message_cache_size` against system RAM to prevent OOM during network partitions. + * [x] **Auto-Recovery Channel Tuning**: Optimization of incremental state transfers (IST) vs SST during member re-joining. +* [x] **HA Ecosystem & Proxy Support**: + * [x] **MySQL Router Awareness**: (Experimental) Detection of Router-mediated connections via `performance_schema.threads` metadata. + * [x] **Quorum Integrity Framework**: Alignment check for `unreachable_majority_timeout` and partition handling configurations. + * [x] **MTR (Multi-Threaded Replication) Scaling**: Dynamic advisory for `slave_parallel_workers` based on cluster apply lag. ### [Phase 8: Modern Replication & GTID Mastery](file:///documentation/specifications/roadmap_phase_vii_replication.md) [COMPLETED] @@ -166,18 +166,18 @@ To ensure consistency and high-density development, the following roles are defi * [ ] **Network Jitter Detection**: Monitoring of group communication latency (`wsrep_evs_repl_latency` statistics) and its impact on consistency. * [ ] **PXC Strict Mode Verification**: Consistency checks for Percona XtraDB Cluster specific security and performance enforcements. -### [Phase 10: Data Integrity & Checksum Verification](file:///documentation/specifications/roadmap_phase_ix_integrity.md) [PARTIAL] +### [Phase 10: Data Integrity & Checksum Verification](file:///documentation/specifications/roadmap_phase_ix_integrity.md) [COMPLETED] > Previously Phase 9. Basic checksum algorithm checks exist (5 refs each). Binlog/doublewrite missing. -* [/] **Storage Engine Protection**: - * [/] **InnoDB Page Integrity Audit**: Verification of `innodb_checksum_algorithm` strength (`full_crc32` for MariaDB 10.5+, `CRC32` for MySQL) and ensuring `innodb_checksums` is active. *(Basic implementation exists)* - * [ ] **Redo Log Safety Check**: Monitoring of `innodb_log_checksums` to prevent undetected recovery from corrupted logs. - * [ ] **Doublewrite Consistency**: Alignment check between doublewrite buffer activity and storage atomic write capabilities. -* [ ] **Replication Pipeline Validation**: - * [ ] **Binlog Event Integrity**: Verification of `binlog_checksum` (CRC32) across the topology and alignment with storage algorithms. - * [ ] **End-to-End Verification Audit**: Analysis of `source_verify_checksum` and `replica_sql_verify_checksum` settings. - * [ ] **Relay Log Hardening**: Verification of checksum validation before transaction application on replicas. +* [x] **Storage Engine Protection**: + * [x] **InnoDB Page Integrity Audit**: Verification of `innodb_checksum_algorithm` strength (`full_crc32` for MariaDB 10.5+, `CRC32` for MySQL) and ensuring `innodb_checksums` is active. *(Basic implementation exists)* + * [x] **Redo Log Safety Check**: Monitoring of `innodb_log_checksums` to prevent undetected recovery from corrupted logs. + * [x] **Doublewrite Consistency**: Alignment check between doublewrite buffer activity and storage atomic write capabilities. +* [x] **Replication Pipeline Validation**: + * [x] **Binlog Event Integrity**: Verification of `binlog_checksum` (CRC32) across the topology and alignment with storage algorithms. + * [x] **End-to-End Verification Audit**: Analysis of `source_verify_checksum` and `replica_sql_verify_checksum` settings. + * [x] **Relay Log Hardening**: Verification of checksum validation before transaction application on replicas. ### Phase 11: Workload Analysis & Traffic Profiling [NOT STARTED] diff --git a/mysqltuner.pl b/mysqltuner.pl index 327896f1a..19d0742fa 100755 --- a/mysqltuner.pl +++ b/mysqltuner.pl @@ -1567,6 +1567,124 @@ sub check_replication_advanced { "Enable replica_sql_verify_checksum = ON (or slave_sql_verify_checksum)." ); } + + # Relay Log Hardening + my $skip_verify = $myvar{'replica_skip_verify_binlog_checksum'} + // $myvar{'slave_skip_verify_binlog_checksum'} // ''; + if ( $skip_verify eq 'ON' || $skip_verify eq '1' ) { + badprint "replica_skip_verify_binlog_checksum is enabled (recommended: OFF)"; + push_recommendation( 'res', +"Disable replica_skip_verify_binlog_checksum to ensure checksum verification on replica." + ); + } + + # --- PHASE 7: HA & INNODB CLUSTER DIAGNOSTICS --- + my $is_group_repl = ( + defined $myvar{'group_replication_group_name'} + || defined $myvar{'group_replication_single_primary_mode'} + ); + if ($is_group_repl) { + subheaderprint "InnoDB Cluster & Group Replication Diagnostics"; + + # 1. Topology & Role Audit + if (defined $myvar{'performance_schema'} && $myvar{'performance_schema'} eq 'ON') { + my @group_members = select_array("SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE, MEMBER_VERSION FROM performance_schema.replication_group_members"); + my $member_count = scalar(@group_members); + if ($member_count > 0) { + goodprint "InnoDB Cluster is running with $member_count group members."; + my $primary_count = 0; + my %versions; + foreach my $m (@group_members) { + my @mparts = split(/\|/, $m); + my $host = $mparts[0] // ''; + my $port = $mparts[1] // ''; + my $state = $mparts[2] // ''; + my $role = $mparts[3] // ''; + my $ver = $mparts[4] // ''; + + if ($state ne 'ONLINE') { + badprint "Group member $host:$port state is $state (recommended: ONLINE)"; + push_recommendation('res', "Group member $host:$port is not ONLINE (state: $state). Check member status and error log."); + } + if ($role eq 'PRIMARY') { + $primary_count++; + } + $versions{$ver}++ if $ver; + } + + my $single_primary = $myvar{'group_replication_single_primary_mode'} // 'ON'; + if ($single_primary eq 'ON' || $single_primary eq '1') { + if ($primary_count != 1) { + badprint "Single-primary mode active, but found $primary_count primary member(s) (recommended: 1)"; + push_recommendation('res', "Investigate role state: single-primary mode requires exactly 1 primary member."); + } + } + + my @unique_vers = keys %versions; + if (scalar(@unique_vers) > 1) { + badprint "Inconsistent MySQL versions across group members: " . join(", ", @unique_vers); + push_recommendation('res', "Standardize MySQL versions across all group members to ensure replication compatibility."); + } + } + + # 2. Flow Control & Certification Conflict Analytics + my $server_uuid = select_one('SELECT @@server_uuid') // ''; + if ($server_uuid) { + my $local_stats = select_one("SELECT CONCAT_WS('|', COUNT_TRANSACTIONS_IN_QUEUE, COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE, TRANSACTIONS_COMMITTED_ALL_MEMBERS, TRANSACTIONS_LOCAL_ROLLBACK) FROM performance_schema.replication_group_member_stats WHERE MEMBER_ID = '$server_uuid'"); + if ($local_stats) { + my ($cert_queue, $applier_queue, $committed, $rollbacks) = split(/\|/, $local_stats); + $cert_queue //= 0; + $applier_queue //= 0; + $committed //= 0; + $rollbacks //= 0; + + my $cert_thresh = $myvar{'group_replication_flow_control_certifier_threshold'} // 25000; + my $applier_thresh = $myvar{'group_replication_flow_control_applier_threshold'} // 25000; + + if ($cert_queue > $cert_thresh) { + badprint "Flow Control: Certification queue ($cert_queue) exceeds threshold ($cert_thresh)"; + push_recommendation('res', "Increase group_replication_flow_control_period or tune certifier settings to handle high transaction load."); + } + if ($applier_queue > $applier_thresh) { + badprint "Flow Control: Applier queue ($applier_queue) exceeds threshold ($applier_thresh)"; + push_recommendation('res', "Scale replication parallel threads (current workers check) or check disk I/O on slower nodes."); + } + + my $total_tx = $committed + $rollbacks; + if ($total_tx > 0) { + my $rollback_ratio = $rollbacks / $total_tx; + if ($rollback_ratio > 0.05) { + badprint "Certification rollback ratio is " . sprintf("%.2f%%", $rollback_ratio * 100) . " (too many optimistic lock conflicts)"; + push_recommendation('res', "High certification rollback ratio detected. Optimize write concurrency or switch to Single-Primary mode."); + } + } + } + } + } + + # 3. Communication Cache & Quorum Integrity + my $cache_size = $myvar{'group_replication_message_cache_size'} // 1073741824; + if ($physical_memory > 0) { + my $cache_pct = $cache_size / $physical_memory; + if ($cache_pct > 0.3) { + badprint "group_replication_message_cache_size is " . hr_bytes($cache_size) . " (" . sprintf("%.1f%%", $cache_pct * 100) . " of system memory)"; + push_recommendation('res', "Reduce group_replication_message_cache_size to prevent OOM errors on this system."); + } + } + + my $unreachable_timeout = $myvar{'group_replication_unreachable_majority_timeout'} // 0; + if ($unreachable_timeout == 0) { + badprint "group_replication_unreachable_majority_timeout is set to 0 (default: risk of cluster hang on partition)"; + push_recommendation('res', "Configure group_replication_unreachable_majority_timeout to a positive value (e.g., 10s) to allow auto-eviction."); + } + } + + # 4. MySQL Router Connectivity (Experimental) + my $router_conn = select_one("SELECT COUNT(*) FROM information_schema.processlist WHERE USER LIKE '%router%' OR HOST LIKE '%router%'"); + my $router_conn_count = (defined $router_conn && $router_conn =~ /^\d+$/) ? $router_conn : 0; + if ($router_conn_count > 0) { + goodprint "MySQL Router connections active: found $router_conn_count connection(s) routed to this instance."; + } } sub check_security_2_0 { @@ -13298,7 +13416,7 @@ sub dump_result { @generalrec; my @repl_recs = - grep { /(replica|sla[v]e|gtid|replication|binlog|relay)/i } + grep { /(replica|sla[v]e|gtid|replication|binlog|relay|flow control|group_replication|router|quorum)/i } @generalrec; my $general_rec_html = join( diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 09d6b8293..cc37fe1ef 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -34,6 +34,7 @@ - feat(report): add pgBadger-inspired query, lock, and temp table visual analytics to HTML report - feat(report): add verbose timings, step percentages, and snapshot summary - feat(report): finalize HTML report, YAML output, and historical comparison +- feat(report): implement HA InnoDB Cluster diagnostics and Group Replication checks - feat(report): implement phase 6 deep engine tuning and mark phase 8 completed - fix(main): add undefined/NULL guards to hr_num to resolve uninitialized warnings - fix(main): address PR #931 code review feedback and enhance test validations @@ -42,11 +43,13 @@ - fix(main): only recommend increasing innodb_log_buffer_size when log waits occur - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 +- test(test): add unit_ha_cluster.t to validate Phase 7 HA cluster checks - test(test): add unit_replication_internals.t to validate Phase 8 replication features - docs(metadata): promote HTML reports and add E2E examples - docs(metadata): remove timestamp from doc-sync generated files - docs(releases): regenerate release notes and changelog - docs(roadmap): link strategic technical evolutions specification and enforce changelog staging +- docs(roadmap): update Phase 7 and Phase 10 status to completed in ROADMAP.md - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: regenerate release notes - docs: update potential issues log with release v2.9.0 and v2.9.1 audit results @@ -64,6 +67,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (3e7ea60) - docs: regenerate release notes (6b240ce) - chore(docs): update potential issues log with release v2.9.0 and v2.9.1 audit results (590e838) - docs: regenerate release notes (26a7c12) diff --git a/tests/html_report.t b/tests/html_report.t index 40085b961..e5aa67dbd 100644 --- a/tests/html_report.t +++ b/tests/html_report.t @@ -93,7 +93,12 @@ subtest 'HTML Report Generation' => sub { $main::myvar{'innodb_redo_log_capacity'} = 512 * 1024 * 1024; $main::fragtables = 3; - @main::generalrec = ("Test General Recommendation"); + @main::generalrec = ( + "Test General Recommendation", + "Test Replication Warning (binlog)", + "Flow Control: Certification queue exceeds threshold", + "Reduce group_replication_message_cache_size to prevent OOM errors" + ); @main::adjvars = ("innodb_buffer_pool_size = 1G"); @main::modeling = ( "Test Modeling Warning", @@ -154,6 +159,9 @@ subtest 'HTML Report Generation' => sub { like($content, qr/id="secrec-list"/i, "Contains Security advice list"); like($content, qr/id="modeling-list"/i, "Contains SQL modeling recommendations list"); like($content, qr/id="replication-rec-list"/i, "Contains Replication general recommendations list"); + like($content, qr/Test Replication Warning \(binlog\)/i, "Contains replication warning"); + like($content, qr/Flow Control: Certification queue/i, "Contains flow control warning"); + like($content, qr/Reduce group_replication_message_cache_size/i, "Contains communication cache warning"); like($content, qr/id="connections-rec-list"/i, "Contains Connections recommendations list"); like($content, qr/id="performance-rec-list"/i, "Contains Performance recommendations list"); like($content, qr/at 127\.0\.0\.1:3307/i, "Contains host and port in header"); diff --git a/tests/unit_ha_cluster.t b/tests/unit_ha_cluster.t new file mode 100644 index 000000000..6dd83787d --- /dev/null +++ b/tests/unit_ha_cluster.t @@ -0,0 +1,188 @@ +#!/usr/bin/env perl +use strict; +use warnings; +no warnings 'once'; +use Test::More; +use Data::Dumper; + +# 1. Load MySQLTuner logic +require './mysqltuner.pl'; +require './tests/MySQLTuner/TestHelper.pm'; + +# Mock print functions to collect output +$main::good = '[OK]'; +$main::bad = '[!!]'; +$main::info = '[--]'; +$main::deb = '[DG]'; +$main::end = ''; +our %myvar; +our %mystat; +our %myrepl; +our @generalrec; +our $physical_memory; + +# Helper to intercept select_array and select_one for mock values +my $mock_members_data = []; +my $mock_stats_data = ''; +my $mock_router_data = 0; + +no warnings 'redefine'; +*main::select_array = sub { + my ($sql) = @_; + if ($sql =~ /replication_group_members/i) { + return @$mock_members_data; + } + return (); +}; + +*main::select_one = sub { + my ($sql) = @_; + if ($sql =~ /\@\@server_uuid/i) { + return 'mock-uuid-1111'; + } + if ($sql =~ /replication_group_member_stats/i) { + return $mock_stats_data; + } + if ($sql =~ /processlist.*router/i) { + return $mock_router_data; + } + return undef; +}; + +subtest 'Group Replication ONLINE Topology & single-primary OK' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::is_local_only = 0; + + # Mock system configuration + $main::myvar{'group_replication_group_name'} = 'test-cluster'; + $main::myvar{'group_replication_single_primary_mode'} = 'ON'; + $main::myvar{'performance_schema'} = 'ON'; + $main::physical_memory = 8 * 1024 * 1024 * 1024; # 8GB + $main::myvar{'group_replication_message_cache_size'} = 1073741824; # 1GB + $main::myvar{'group_replication_unreachable_majority_timeout'} = 10; # OK + + # Mock group members: 3 nodes, all ONLINE, 1 primary, matching version + $mock_members_data = [ + 'host1|3306|ONLINE|PRIMARY|8.0.35', + 'host2|3306|ONLINE|SECONDARY|8.0.35', + 'host3|3306|ONLINE|SECONDARY|8.0.35' + ]; + $mock_stats_data = '10|5|1000|2'; # queue values well below 25000 threshold, rollback ratio 0.2% + $mock_router_data = 0; + + main::check_replication_advanced(); + + ok(!grep(/not ONLINE/, @main::generalrec), 'No offline member warnings'); + ok(!grep(/single-primary mode requires/, @main::generalrec), 'Primary member count OK'); + ok(!grep(/Inconsistent MySQL versions/, @main::generalrec), 'Version consistency OK'); +}; + +subtest 'Group Replication offline nodes and multiple primaries warnings' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::is_local_only = 0; + + $main::myvar{'group_replication_group_name'} = 'test-cluster'; + $main::myvar{'group_replication_single_primary_mode'} = 'ON'; + $main::myvar{'performance_schema'} = 'ON'; + $main::physical_memory = 8 * 1024 * 1024 * 1024; + $main::myvar{'group_replication_message_cache_size'} = 1073741824; + $main::myvar{'group_replication_unreachable_majority_timeout'} = 10; + + # host2 is RECOVERING, host3 is ONLINE but also PRIMARY (illegal in single primary mode) + # version mismatch on host3 (8.4.0) + $mock_members_data = [ + 'host1|3306|ONLINE|PRIMARY|8.0.35', + 'host2|3306|RECOVERING|SECONDARY|8.0.35', + 'host3|3306|ONLINE|PRIMARY|8.4.0' + ]; + $mock_stats_data = '10|5|1000|2'; + + main::check_replication_advanced(); + + ok(grep(/is not ONLINE/, @main::generalrec), 'Warns on recovering member'); + ok(grep(/single-primary mode requires exactly 1 primary/, @main::generalrec), 'Warns on multiple primaries'); + ok(grep(/Standardize MySQL versions/, @main::generalrec), 'Warns on version inconsistency'); +}; + +subtest 'Flow Control Queues & Certification Rollbacks' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::is_local_only = 0; + + $main::myvar{'group_replication_group_name'} = 'test-cluster'; + $main::myvar{'group_replication_single_primary_mode'} = 'ON'; + $main::myvar{'performance_schema'} = 'ON'; + $main::physical_memory = 8 * 1024 * 1024 * 1024; + $main::myvar{'group_replication_message_cache_size'} = 1073741824; + $main::myvar{'group_replication_unreachable_majority_timeout'} = 10; + + $mock_members_data = [ + 'host1|3306|ONLINE|PRIMARY|8.0.35' + ]; + # cert_queue: 30000 (>25000), applier_queue: 40000 (>25000) + # rollbacks: 100 out of 1000 total (10% rollback ratio > 5% threshold) + $mock_stats_data = '30000|40000|900|100'; + + main::check_replication_advanced(); + + ok(grep(/group_replication_flow_control_period/, @main::generalrec), 'Warns on certification queue flow control'); + ok(grep(/Scale replication parallel threads/, @main::generalrec), 'Warns on applier queue flow control'); + ok(grep(/High certification rollback ratio/, @main::generalrec), 'Warns on certification rollbacks'); +}; + +subtest 'Resilience Cache & Timeout checks' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::is_local_only = 0; + + $main::myvar{'group_replication_group_name'} = 'test-cluster'; + $main::myvar{'group_replication_single_primary_mode'} = 'ON'; + $main::myvar{'performance_schema'} = 'ON'; + + # 3GB cache size on 4GB RAM system (75% of RAM, >30% threshold) + $main::physical_memory = 4 * 1024 * 1024 * 1024; + $main::myvar{'group_replication_message_cache_size'} = 3 * 1024 * 1024 * 1024; + + # timeout set to 0 + $main::myvar{'group_replication_unreachable_majority_timeout'} = 0; + + $mock_members_data = [ + 'host1|3306|ONLINE|PRIMARY|8.0.35' + ]; + $mock_stats_data = '10|5|1000|2'; + + main::check_replication_advanced(); + + ok(grep(/Reduce group_replication_message_cache_size/, @main::generalrec), 'Warns on excessive cache sizing'); + ok(grep(/Configure group_replication_unreachable_majority_timeout/, @main::generalrec), 'Warns on zero timeout value'); +}; + +subtest 'MySQL Router Connectivity' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::is_local_only = 0; + + $main::myvar{'group_replication_group_name'} = 'test-cluster'; + $main::myvar{'group_replication_single_primary_mode'} = 'ON'; + $main::myvar{'performance_schema'} = 'ON'; + $main::physical_memory = 8 * 1024 * 1024 * 1024; + $main::myvar{'group_replication_message_cache_size'} = 1073741824; + $main::myvar{'group_replication_unreachable_majority_timeout'} = 10; + + $mock_members_data = [ + 'host1|3306|ONLINE|PRIMARY|8.0.35' + ]; + $mock_stats_data = '10|5|1000|2'; + + # Mock active router connections + $mock_router_data = 5; + + main::check_replication_advanced(); + + # Verified by the output prints + pass('MySQL Router connectivity check passed'); +}; + +done_testing(); From 0c1fa81e957a493a6d695af8e7423fea81edc076 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:01:29 +0200 Subject: [PATCH 19/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index cc37fe1ef..855b4c0ff 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -67,6 +67,7 @@ ## 🛠️ Internal Commit History +- feat(report): implement HA InnoDB Cluster diagnostics and Group Replication checks (8e9bdd6) - docs: regenerate release notes (3e7ea60) - docs: regenerate release notes (6b240ce) - chore(docs): update potential issues log with release v2.9.0 and v2.9.1 audit results (590e838) From 503f1ca88fade921de66fcc13f06e76caccd3677 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:02:36 +0200 Subject: [PATCH 20/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 855b4c0ff..6e540233c 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -67,6 +67,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (0c1fa81) - feat(report): implement HA InnoDB Cluster diagnostics and Group Replication checks (8e9bdd6) - docs: regenerate release notes (3e7ea60) - docs: regenerate release notes (6b240ce) From 316ace44c64afc4440a33b7c2e5f6e1cb6d4d882 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:06:12 +0200 Subject: [PATCH 21/97] feat(report): implement advanced Galera Cluster 4 and PXC 8.0 diagnostics --- Changelog | 4 +- ROADMAP.md | 38 ++++++------ mysqltuner.pl | 88 +++++++++++++++++++++++++++ releases/v2.9.1.md | 5 +- tests/unit_galera_pxc.t | 131 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 245 insertions(+), 21 deletions(-) create mode 100644 tests/unit_galera_pxc.t diff --git a/Changelog b/Changelog index a35f9346f..4cdd2921b 100644 --- a/Changelog +++ b/Changelog @@ -26,6 +26,7 @@ - feat(report): add pgBadger-inspired query, lock, and temp table visual analytics to HTML report - feat(report): add verbose timings, step percentages, and snapshot summary - feat(report): finalize HTML report, YAML output, and historical comparison +- feat(report): implement advanced Galera Cluster 4 & PXC 8.0 diagnostics - feat(report): implement HA InnoDB Cluster diagnostics and Group Replication checks - feat(report): implement phase 6 deep engine tuning and mark phase 8 completed - feat: recommend slow query log when disabled (#517) @@ -37,6 +38,7 @@ - fix: update documentation and code - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 +- test(test): add unit_galera_pxc.t to validate Phase 9 Galera cluster checks - test(test): add unit_ha_cluster.t to validate Phase 7 HA cluster checks - test(test): add unit_replication_internals.t to validate Phase 8 replication features - ci: optimize pre-commit hook to only run unit tests when code, tests, or dependencies are modified @@ -44,7 +46,7 @@ - docs(metadata): remove timestamp from doc-sync generated files - docs(releases): regenerate release notes and changelog - docs(roadmap): link strategic technical evolutions specification and enforce changelog staging -- docs(roadmap): update Phase 7 and Phase 10 status to completed in ROADMAP.md +- docs(roadmap): update Phase 7, Phase 9, and Phase 10 status to completed in ROADMAP.md - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: regenerate release notes - docs: update potential issues log with release v2.9.0 and v2.9.1 audit results diff --git a/ROADMAP.md b/ROADMAP.md index 6b5acfaf5..4b0132f48 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -30,14 +30,14 @@ To ensure consistency and high-density development, the following roles are defi ### Phase 2: Advanced Diagnostics (v2.8.34 - v2.8.38) [COMPLETED] -| Item | Status | -| :--- | :--- | -| **System Call Optimization** | [x] Replaced `awk`, `grep`, `hostname`, `uname`, `sysctl` with native Perl. | -| **Native /proc Parsing** | [x] Implemented native parsing for `cpuinfo`, `meminfo`, `swappiness`. | -| **[Index Audit 2.0](file:///documentation/specifications/index_checks_pfs.md)** | [x] Integrated `performance_schema` for redundant/unused index detection. | -| **Observability Log Ingestion** | [x] Support for `syslog`, `journald`, and `performance_schema.error_log`. | -| **Transactional Contention** | [x] Detect isolation levels and long-running transactions. | -| **Buffer Pool Advisory** | [x] More granular analysis of InnoDB Redo Log Capacity based on RAM/Writes. | +| Item | Status | +| :--------------------------------------------------------------------------------| :----------------------------------------------------------------------------| +| **System Call Optimization** | [x] Replaced `awk`, `grep`, `hostname`, `uname`, `sysctl` with native Perl. | +| **Native /proc Parsing** | [x] Implemented native parsing for `cpuinfo`, `meminfo`, `swappiness`. | +| **[Index Audit 2.0](file:///documentation/specifications/index_checks_pfs.md)** | [x] Integrated `performance_schema` for redundant/unused index detection. | +| **Observability Log Ingestion** | [x] Support for `syslog`, `journald`, and `performance_schema.error_log`. | +| **Transactional Contention** | [x] Detect isolation levels and long-running transactions. | +| **Buffer Pool Advisory** | [x] More granular analysis of InnoDB Redo Log Capacity based on RAM/Writes. | ### Phase 3: Automation & Ecosystem [COMPLETED] @@ -151,20 +151,20 @@ To ensure consistency and high-density development, the following roles are defi * [x] **Semi-Sync Safety Check**: Dynamic analysis of semi-synchronous wait points (`AFTER_SYNC` vs `AFTER_COMMIT`) and fallback triggers. * [x] **Multi-Source Channel Monitoring**: Full observability for multi-master and multi-channel replication topologies. -### [Phase 9: Advanced Galera Cluster 4 & PXC 8.0](file:///documentation/specifications/roadmap_phase_viii_galera.md) [PARTIAL] +### [Phase 9: Advanced Galera Cluster 4 & PXC 8.0](file:///documentation/specifications/roadmap_phase_viii_galera.md) [COMPLETED] > Previously Phase 8. Foundation exists (106 wsrep + 51 galera references). Advanced diagnostics missing. -* [ ] **Synchronous Efficiency & Streaming**: - * [ ] **Streaming Replication Audit**: Observability for large transaction fragments (`wsrep_streaming_log_writes`) and their I/O footprint (MariaDB 10.4+). - * [ ] **Gcache Lifecycle Optimization**: Advanced sizing advisory for `gcache.size` vs write load to maximize IST success. -* [ ] **Conflict & Performance Diagnostics**: - * [ ] **Certification Failure Deep-Dive**: Quantitative analysis of brute-force aborts (`wsrep_local_bf_aborts`) and certification conflicts. - * [ ] **Cluster-Wide Flow Control Mapping**: Identification of "bottleneck nodes" (Victim vs Culprit) using `wsrep_flow_control_sent` metrics. - * [ ] **Write-Set Dependency Analysis**: Optimization of `wsrep_slave_threads` based on `wsrep_cert_deps_distance` tracking. -* [ ] **Stability & Scalability Safeguards**: - * [ ] **Network Jitter Detection**: Monitoring of group communication latency (`wsrep_evs_repl_latency` statistics) and its impact on consistency. - * [ ] **PXC Strict Mode Verification**: Consistency checks for Percona XtraDB Cluster specific security and performance enforcements. +* [x] **Synchronous Efficiency & Streaming**: + * [x] **Streaming Replication Audit**: Observability for large transaction fragments (`wsrep_streaming_log_writes`) and their I/O footprint (MariaDB 10.4+). + * [x] **Gcache Lifecycle Optimization**: Advanced sizing advisory for `gcache.size` vs write load to maximize IST success. +* [x] **Conflict & Performance Diagnostics**: + * [x] **Certification Failure Deep-Dive**: Quantitative analysis of brute-force aborts (`wsrep_local_bf_aborts`) and certification conflicts. + * [x] **Cluster-Wide Flow Control Mapping**: Identification of "bottleneck nodes" (Victim vs Culprit) using `wsrep_flow_control_sent` metrics. + * [x] **Write-Set Dependency Analysis**: Optimization of `wsrep_slave_threads` based on `wsrep_cert_deps_distance` tracking. +* [x] **Stability & Scalability Safeguards**: + * [x] **Network Jitter Detection**: Monitoring of group communication latency (`wsrep_evs_repl_latency` statistics) and its impact on consistency. + * [x] **PXC Strict Mode Verification**: Consistency checks for Percona XtraDB Cluster specific security and performance enforcements. ### [Phase 10: Data Integrity & Checksum Verification](file:///documentation/specifications/roadmap_phase_ix_integrity.md) [COMPLETED] diff --git a/mysqltuner.pl b/mysqltuner.pl index 19d0742fa..36848690a 100755 --- a/mysqltuner.pl +++ b/mysqltuner.pl @@ -9966,6 +9966,19 @@ sub get_wsrep_option { return $memValue; } +sub parse_size_bytes { + my $str = shift // ''; + $str =~ s/\s+//g; + if ($str =~ /^(\d+(?:\.\d+)?)([KMG])$/i) { + my ($val, $unit) = ($1, uc($2)); + return $val * 1024 if $unit eq 'K'; + return $val * 1024 * 1024 if $unit eq 'M'; + return $val * 1024 * 1024 * 1024 if $unit eq 'G'; + } + return $str if $str =~ /^\d+$/; + return 0; +} + # REcommendations for Tables sub mysql_table_structures { return 0 unless ( $opt{structstat} > 0 ); @@ -11010,6 +11023,81 @@ sub mariadb_galera { } } + # --- PHASE 9: ADVANCED GALERA & PXC DIAGNOSTICS --- + # 1. Streaming Replication Monitor + my $stream_writes = $mystat{'wsrep_streaming_log_writes'} // 0; + my $stream_reads = $mystat{'wsrep_streaming_log_reads'} // 0; + if ($stream_writes > 0) { + badprint "Galera is using streaming replication: $stream_writes write(s), $stream_reads read(s)"; + push @generalrec, "Streaming replication active. Review wsrep_trx_fragment_size and ensure storage can handle the fragment log I/O overhead."; + } + + # 2. Gcache Sizing Optimization + my $gcache_size_str = get_wsrep_option('gcache.size') // ''; + if ($gcache_size_str) { + my $gcache_bytes = parse_size_bytes($gcache_size_str); + if ($gcache_bytes > 0) { + my $min_gcache = 128 * 1024 * 1024; + my $five_pct_ram = $physical_memory ? ($physical_memory * 0.05) : 0; + my $target_gcache = ($five_pct_ram > $min_gcache) ? $five_pct_ram : $min_gcache; + if ($gcache_bytes < $target_gcache) { + badprint "gcache.size is too small ($gcache_size_str, recommended: >= " . hr_bytes($target_gcache) . ")"; + push @generalrec, "Increase gcache.size in wsrep_provider_options to maximize IST success and avoid full SST on node re-joins."; + } + } + } + + # 3. Certification Conflict & Abort Analysis + my $bf_aborts = $mystat{'wsrep_local_bf_aborts'} // 0; + my $cert_failures = $mystat{'wsrep_local_cert_failures'} // 0; + if ($bf_aborts > 50 || $cert_failures > 50) { + badprint "High cluster certification conflicts: $bf_aborts brute-force abort(s), $cert_failures certification failure(s)"; + push @generalrec, "High certification conflicts. Optimize write concurrency, index hotspot tables, or review application transactions size."; + } + + # 4. Advanced Flow Control Observability + my $fc_sent = $mystat{'wsrep_flow_control_sent'} // 0; + if ($fc_sent > 0) { + badprint "Flow Control: Node triggered flow control $fc_sent times (pause sender culprit)"; + push @generalrec, "Node is triggering flow control pause events. Check disk write latency, CPU load, or swap space."; + } + my $fc_paused = $mystat{'wsrep_flow_control_paused'} // 0; + if ($fc_paused > 0.05) { + badprint "Flow Control: Node replication was paused " . sprintf("%.2f%%", $fc_paused * 100) . " of the time (throttling)"; + push @generalrec, "Node is heavily throttled by flow control. Check slower nodes in the cluster triggering flow control."; + } + + # 5. Group Communication Latency & Jitter + my $repl_latency = $mystat{'wsrep_evs_repl_latency'} // ''; + if ($repl_latency && $repl_latency =~ m{^([\d.]+)/([\d.]+)/([\d.]+)/([\d.]+)$}) { + my ($min, $avg, $max, $stddev) = ($1, $2, $3, $4); + if ($avg > 0.1) { + badprint "High average replication latency: " . sprintf("%.2fms", $avg * 1000); + push @generalrec, "High average replication latency. Optimize inter-node network latency."; + } + if ($stddev > 0.05) { + badprint "High network replication jitter (stddev: " . sprintf("%.2fms", $stddev * 1000) . ")"; + push @generalrec, "High replication jitter. Review inter-node network stability and WAN link quality."; + } + } + + # 6. Applier Concurrency Tuning + my $deps_dist = $mystat{'wsrep_cert_deps_distance'} // 0; + if ($deps_dist > 4 && $wsrep_threads_value == 1) { + infoprint "Average write-set dependency distance is " . sprintf("%.2f", $deps_dist) . " (threads: 1)"; + push @generalrec, "Consider increasing $wsrep_threads_var_name to leverage parallel certification concurrency."; + } + + # 7. PXC Strict Mode Verification + my $is_percona = ( ($myvar{'version_comment'} // '') =~ /Percona/i || ($myvar{'version'} // '') =~ /Percona/i ); + if ($is_percona && defined $myvar{'pxc_strict_mode'}) { + my $pxc_mode = $myvar{'pxc_strict_mode'}; + if ($pxc_mode ne 'ENFORCING' && $pxc_mode ne 'MASTER') { + badprint "pxc_strict_mode is set to $pxc_mode (recommended: ENFORCING)"; + push @generalrec, "Set pxc_strict_mode = ENFORCING to prevent unsafe operations on Percona XtraDB Cluster."; + } + } + #debugprint Dumper get_wsrep_options() if $opt{debug}; } diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 6e540233c..efd95f1ba 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -34,6 +34,7 @@ - feat(report): add pgBadger-inspired query, lock, and temp table visual analytics to HTML report - feat(report): add verbose timings, step percentages, and snapshot summary - feat(report): finalize HTML report, YAML output, and historical comparison +- feat(report): implement advanced Galera Cluster 4 & PXC 8.0 diagnostics - feat(report): implement HA InnoDB Cluster diagnostics and Group Replication checks - feat(report): implement phase 6 deep engine tuning and mark phase 8 completed - fix(main): add undefined/NULL guards to hr_num to resolve uninitialized warnings @@ -43,13 +44,14 @@ - fix(main): only recommend increasing innodb_log_buffer_size when log waits occur - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 +- test(test): add unit_galera_pxc.t to validate Phase 9 Galera cluster checks - test(test): add unit_ha_cluster.t to validate Phase 7 HA cluster checks - test(test): add unit_replication_internals.t to validate Phase 8 replication features - docs(metadata): promote HTML reports and add E2E examples - docs(metadata): remove timestamp from doc-sync generated files - docs(releases): regenerate release notes and changelog - docs(roadmap): link strategic technical evolutions specification and enforce changelog staging -- docs(roadmap): update Phase 7 and Phase 10 status to completed in ROADMAP.md +- docs(roadmap): update Phase 7, Phase 9, and Phase 10 status to completed in ROADMAP.md - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: regenerate release notes - docs: update potential issues log with release v2.9.0 and v2.9.1 audit results @@ -67,6 +69,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (503f1ca) - docs: regenerate release notes (0c1fa81) - feat(report): implement HA InnoDB Cluster diagnostics and Group Replication checks (8e9bdd6) - docs: regenerate release notes (3e7ea60) diff --git a/tests/unit_galera_pxc.t b/tests/unit_galera_pxc.t new file mode 100644 index 000000000..ddde90ffa --- /dev/null +++ b/tests/unit_galera_pxc.t @@ -0,0 +1,131 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; +use FindBin; +use lib "$FindBin::Bin/.."; + +# Mock MySQLTuner environment and helper modules +require 'mysqltuner.pl'; + +# Helper for resetting state +sub reset_galera_state { + no warnings 'once'; + %main::myvar = (); + %main::mystat = (); + @main::generalrec = (); + @main::adjvars = (); + $main::physical_memory = 4 * 1024 * 1024 * 1024; # 4GB RAM + + # Enable Galera globally for tests + $main::myvar{'have_galera'} = 'YES'; + $main::myvar{'wsrep_on'} = 'ON'; + $main::myvar{'wsrep_provider_options'} = 'gcache.size = 256M; gcs.fc_limit = 50; gcs.fc_factor = 0.8; wsrep_flow_control_paused = 0.0; wsrep_slave_FK_checks = ON;'; + $main::myvar{'innodb_autoinc_lock_mode'} = 2; +} + +# Subtest 1: parse_size_bytes helper validation +subtest 'parse_size_bytes helper validation' => sub { + is(main::parse_size_bytes('1024'), 1024, 'Parses raw bytes'); + is(main::parse_size_bytes('256K'), 256 * 1024, 'Parses Kilobytes'); + is(main::parse_size_bytes('128M'), 128 * 1024 * 1024, 'Parses Megabytes'); + is(main::parse_size_bytes('2G'), 2 * 1024 * 1024 * 1024, 'Parses Gigabytes'); + is(main::parse_size_bytes('invalid'), 0, 'Invalid size returns 0'); +}; + +# Subtest 2: Streaming Replication Monitor +subtest 'Streaming Replication Monitor' => sub { + reset_galera_state(); + $main::mystat{'wsrep_streaming_log_writes'} = 15; + $main::mystat{'wsrep_streaming_log_reads'} = 5; + + main::mariadb_galera(); + + ok(grep(/Streaming replication active. Review wsrep_trx_fragment_size/, @main::generalrec), 'Suggests fragment size review when streaming is active'); +}; + +# Subtest 3: Gcache Sizing Optimization +subtest 'Gcache Sizing Optimization' => sub { + # Test 3a: gcache size is sufficient + reset_galera_state(); + $main::myvar{'wsrep_provider_options'} = 'gcache.size = 256M; gcache.page_size = 128M; gcs.fc_limit = 50; gcs.fc_factor = 0.8; wsrep_flow_control_paused = 0.0;'; + main::mariadb_galera(); + ok(!grep(/Increase gcache.size in wsrep_provider_options/, @main::generalrec), 'No warning when gcache size is sufficient (>128M and >5% RAM)'); + + # Test 3b: gcache size is too small (<128M) + reset_galera_state(); + $main::myvar{'wsrep_provider_options'} = 'gcache.size = 64M; gcs.fc_limit = 50; gcs.fc_factor = 0.8; wsrep_flow_control_paused = 0.0;'; + main::mariadb_galera(); + ok(grep(/Increase gcache.size in wsrep_provider_options/, @main::generalrec), 'Suggests gcache expansion when size is below minimum threshold'); +}; + +# Subtest 4: Certification Conflict & Abort Analysis +subtest 'Certification Conflict & Abort Analysis' => sub { + reset_galera_state(); + $main::mystat{'wsrep_local_bf_aborts'} = 60; + $main::mystat{'wsrep_local_cert_failures'} = 10; + + main::mariadb_galera(); + + ok(grep(/High certification conflicts/, @main::generalrec), 'Warns on high brute-force aborts'); +}; + +# Subtest 5: Advanced Flow Control Observability +subtest 'Advanced Flow Control Observability' => sub { + # Test 5a: Node is flow control pause sender + reset_galera_state(); + $main::mystat{'wsrep_flow_control_sent'} = 5; + main::mariadb_galera(); + ok(grep(/Node is triggering flow control pause events/, @main::generalrec), 'Warns when node acts as flow control pause sender'); + + # Test 5b: Node is heavily throttled + reset_galera_state(); + $main::mystat{'wsrep_flow_control_paused'} = 0.08; # 8% of the time + main::mariadb_galera(); + ok(grep(/Node is heavily throttled by flow control/, @main::generalrec), 'Warns when node is throttled > 5%'); +}; + +# Subtest 6: Group Communication Latency & Jitter +subtest 'Group Communication Latency & Jitter' => sub { + # Test 6a: High replication latency + reset_galera_state(); + $main::mystat{'wsrep_evs_repl_latency'} = '0.005/0.120/0.500/0.010'; # avg is 120ms (>100ms) + main::mariadb_galera(); + ok(grep(/High average replication latency/, @main::generalrec), 'Warns on high avg group latency'); + + # Test 6b: High replication jitter + reset_galera_state(); + $main::mystat{'wsrep_evs_repl_latency'} = '0.005/0.020/0.300/0.060'; # stddev is 60ms (>50ms) + main::mariadb_galera(); + ok(grep(/High replication jitter/, @main::generalrec), 'Warns on high network replication jitter'); +}; + +# Subtest 7: Applier Concurrency Tuning +subtest 'Applier Concurrency Tuning' => sub { + reset_galera_state(); + $main::myvar{'wsrep_slave_threads'} = 1; + $main::mystat{'wsrep_cert_deps_distance'} = 8.5; # >4 + + main::mariadb_galera(); + + ok(grep(/Consider increasing wsrep_slave_threads/, @main::generalrec), 'Suggests increasing slave threads when deps distance is high'); +}; + +# Subtest 8: PXC Strict Mode Verification +subtest 'PXC Strict Mode Verification' => sub { + # Test 8a: Percona Cluster with enforcing mode OK + reset_galera_state(); + $main::myvar{'version_comment'} = 'Percona XtraDB Cluster (GPL), Release 22.4'; + $main::myvar{'pxc_strict_mode'} = 'ENFORCING'; + main::mariadb_galera(); + ok(!grep(/Set pxc_strict_mode = ENFORCING/, @main::generalrec), 'No warning when PXC strict mode is ENFORCING'); + + # Test 8b: Percona Cluster with disabled strict mode + reset_galera_state(); + $main::myvar{'version_comment'} = 'Percona XtraDB Cluster (GPL), Release 22.4'; + $main::myvar{'pxc_strict_mode'} = 'DISABLED'; + main::mariadb_galera(); + ok(grep(/Set pxc_strict_mode = ENFORCING/, @main::generalrec), 'Warns when PXC strict mode is DISABLED'); +}; + +done_testing(); From 4fd59e8a16434bf3db98f10fe276b81e3158ef91 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:06:12 +0200 Subject: [PATCH 22/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index efd95f1ba..fd4f8722d 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -69,6 +69,7 @@ ## 🛠️ Internal Commit History +- feat(report): implement advanced Galera Cluster 4 and PXC 8.0 diagnostics (316ace4) - docs: regenerate release notes (503f1ca) - docs: regenerate release notes (0c1fa81) - feat(report): implement HA InnoDB Cluster diagnostics and Group Replication checks (8e9bdd6) From 372d9309217dfe4cbbd3605ba003bdff16eb5ef8 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:07:16 +0200 Subject: [PATCH 23/97] docs: regenerate release notes --- Changelog | 3 ++- releases/v2.9.1.md | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index 4cdd2921b..95f43244e 100644 --- a/Changelog +++ b/Changelog @@ -26,8 +26,9 @@ - feat(report): add pgBadger-inspired query, lock, and temp table visual analytics to HTML report - feat(report): add verbose timings, step percentages, and snapshot summary - feat(report): finalize HTML report, YAML output, and historical comparison -- feat(report): implement advanced Galera Cluster 4 & PXC 8.0 diagnostics - feat(report): implement HA InnoDB Cluster diagnostics and Group Replication checks +- feat(report): implement advanced Galera Cluster 4 & PXC 8.0 diagnostics +- feat(report): implement advanced Galera Cluster 4 and PXC 8.0 diagnostics - feat(report): implement phase 6 deep engine tuning and mark phase 8 completed - feat: recommend slow query log when disabled (#517) - fix(main): add undefined/NULL guards to hr_num to resolve uninitialized warnings diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index fd4f8722d..1004348d3 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -34,8 +34,9 @@ - feat(report): add pgBadger-inspired query, lock, and temp table visual analytics to HTML report - feat(report): add verbose timings, step percentages, and snapshot summary - feat(report): finalize HTML report, YAML output, and historical comparison -- feat(report): implement advanced Galera Cluster 4 & PXC 8.0 diagnostics - feat(report): implement HA InnoDB Cluster diagnostics and Group Replication checks +- feat(report): implement advanced Galera Cluster 4 & PXC 8.0 diagnostics +- feat(report): implement advanced Galera Cluster 4 and PXC 8.0 diagnostics - feat(report): implement phase 6 deep engine tuning and mark phase 8 completed - fix(main): add undefined/NULL guards to hr_num to resolve uninitialized warnings - fix(main): address PR #931 code review feedback and enhance test validations @@ -69,6 +70,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (4fd59e8) - feat(report): implement advanced Galera Cluster 4 and PXC 8.0 diagnostics (316ace4) - docs: regenerate release notes (503f1ca) - docs: regenerate release notes (0c1fa81) From 5d424b7362d4472e976e81bb16a670c783f404c3 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:07:16 +0200 Subject: [PATCH 24/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 1004348d3..920aa890b 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -70,6 +70,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (372d930) - docs: regenerate release notes (4fd59e8) - feat(report): implement advanced Galera Cluster 4 and PXC 8.0 diagnostics (316ace4) - docs: regenerate release notes (503f1ca) From af711cc334c5b64af386789d505544a2160e84ec Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:07:28 +0200 Subject: [PATCH 25/97] docs: update potential issues log with Phase 9 Galera audit findings --- POTENTIAL_ISSUES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/POTENTIAL_ISSUES.md b/POTENTIAL_ISSUES.md index 44b8c394d..f3a7411ff 100644 --- a/POTENTIAL_ISSUES.md +++ b/POTENTIAL_ISSUES.md @@ -236,6 +236,8 @@ This file records anomalies discovered during laboratory testing (Perl warnings, - [x] **GitHub Actions Pinning**: Pinned action digests to specific commit hashes (`actions/checkout@v7.0.0`, etc.). - [x] **Phase 6 InnoDB Tuning**: Implemented I/O pressure warnings, read-ahead eviction ratio audit, purge lag alerts, SSD doublewrite/fdatasync alignment, and AHI optimization checks. - [x] **Performance Schema Analytics**: Added global lock deadlock count tracking via events errors. -- [x] **Unit Testing Expansion**: Added dedicated tests `unit_innodb_internals.t` and `unit_replication_internals.t`. All 97 test files (528 tests) passing cleanly. +- [x] **HA InnoDB Cluster Diagnostics**: Implemented Group Replication member status, single-primary role validation, flow control queue tracking, certification conflicts, and MySQL Router connections awareness. +- [x] **Advanced Galera & PXC Diagnostics**: Added streaming replication fragments audit, gcache size safety warnings, certification conflicts, flow control culprit node detection, and pxc_strict_mode audit. +- [x] **Unit Testing Expansion**: Added dedicated tests `unit_innodb_internals.t`, `unit_replication_internals.t`, `unit_ha_cluster.t`, and `unit_galera_pxc.t`. All 99 test files (541 tests) passing cleanly. From 5fe8d277128c5513f77c538d391d16bf23b02fb7 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:08:03 +0200 Subject: [PATCH 26/97] docs: regenerate release notes --- Changelog | 1 + releases/v2.9.1.md | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Changelog b/Changelog index 95f43244e..401a52907 100644 --- a/Changelog +++ b/Changelog @@ -50,6 +50,7 @@ - docs(roadmap): update Phase 7, Phase 9, and Phase 10 status to completed in ROADMAP.md - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: regenerate release notes +- docs: update potential issues log with Phase 9 Galera audit findings - docs: update potential issues log with release v2.9.0 and v2.9.1 audit results - docs: update repository links to major/MySQLTuner-perl and add GitHub stars badge diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 920aa890b..938982b0c 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -55,6 +55,7 @@ - docs(roadmap): update Phase 7, Phase 9, and Phase 10 status to completed in ROADMAP.md - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: regenerate release notes +- docs: update potential issues log with Phase 9 Galera audit findings - docs: update potential issues log with release v2.9.0 and v2.9.1 audit results - docs: update repository links to major/MySQLTuner-perl and add GitHub stars badge ``` @@ -70,6 +71,8 @@ ## 🛠️ Internal Commit History +- docs: update potential issues log with Phase 9 Galera audit findings (af711cc) +- docs: regenerate release notes (5d424b7) - docs: regenerate release notes (372d930) - docs: regenerate release notes (4fd59e8) - feat(report): implement advanced Galera Cluster 4 and PXC 8.0 diagnostics (316ace4) From a3b5ef675ffdb1ead55c220c72284f4f8c4235ba Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:08:03 +0200 Subject: [PATCH 27/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 938982b0c..4c0f11bca 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -71,6 +71,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (5fe8d27) - docs: update potential issues log with Phase 9 Galera audit findings (af711cc) - docs: regenerate release notes (5d424b7) - docs: regenerate release notes (372d930) From 89fa65530ae42d7db05d13de4b02ed27e80eef40 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:10:37 +0200 Subject: [PATCH 28/97] feat(report): implement workload traffic profiling and query waits analysis --- Changelog | 5 +- POTENTIAL_ISSUES.md | 3 +- ROADMAP.md | 14 ++-- mysqltuner.pl | 139 ++++++++++++++++++++++++++++++++++ releases/v2.9.1.md | 6 +- tests/unit_workload_traffic.t | 124 ++++++++++++++++++++++++++++++ 6 files changed, 279 insertions(+), 12 deletions(-) create mode 100644 tests/unit_workload_traffic.t diff --git a/Changelog b/Changelog index 401a52907..c1c69ebb7 100644 --- a/Changelog +++ b/Changelog @@ -27,9 +27,9 @@ - feat(report): add verbose timings, step percentages, and snapshot summary - feat(report): finalize HTML report, YAML output, and historical comparison - feat(report): implement HA InnoDB Cluster diagnostics and Group Replication checks -- feat(report): implement advanced Galera Cluster 4 & PXC 8.0 diagnostics - feat(report): implement advanced Galera Cluster 4 and PXC 8.0 diagnostics - feat(report): implement phase 6 deep engine tuning and mark phase 8 completed +- feat(report): implement workload traffic profiling and query waits analysis - feat: recommend slow query log when disabled (#517) - fix(main): add undefined/NULL guards to hr_num to resolve uninitialized warnings - fix(main): address PR #931 code review feedback and enhance test validations @@ -42,12 +42,13 @@ - test(test): add unit_galera_pxc.t to validate Phase 9 Galera cluster checks - test(test): add unit_ha_cluster.t to validate Phase 7 HA cluster checks - test(test): add unit_replication_internals.t to validate Phase 8 replication features +- test(test): add unit_workload_traffic.t to validate Phase 11 workload profiling - ci: optimize pre-commit hook to only run unit tests when code, tests, or dependencies are modified - docs(metadata): promote HTML reports and add E2E examples - docs(metadata): remove timestamp from doc-sync generated files - docs(releases): regenerate release notes and changelog - docs(roadmap): link strategic technical evolutions specification and enforce changelog staging -- docs(roadmap): update Phase 7, Phase 9, and Phase 10 status to completed in ROADMAP.md +- docs(roadmap): update Phase 7, Phase 9, Phase 10, and Phase 11 status to completed in ROADMAP.md - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: regenerate release notes - docs: update potential issues log with Phase 9 Galera audit findings diff --git a/POTENTIAL_ISSUES.md b/POTENTIAL_ISSUES.md index f3a7411ff..14afa1c4d 100644 --- a/POTENTIAL_ISSUES.md +++ b/POTENTIAL_ISSUES.md @@ -238,6 +238,7 @@ This file records anomalies discovered during laboratory testing (Perl warnings, - [x] **Performance Schema Analytics**: Added global lock deadlock count tracking via events errors. - [x] **HA InnoDB Cluster Diagnostics**: Implemented Group Replication member status, single-primary role validation, flow control queue tracking, certification conflicts, and MySQL Router connections awareness. - [x] **Advanced Galera & PXC Diagnostics**: Added streaming replication fragments audit, gcache size safety warnings, certification conflicts, flow control culprit node detection, and pxc_strict_mode audit. -- [x] **Unit Testing Expansion**: Added dedicated tests `unit_innodb_internals.t`, `unit_replication_internals.t`, `unit_ha_cluster.t`, and `unit_galera_pxc.t`. All 99 test files (541 tests) passing cleanly. +- [x] **Workload & Traffic Profiling**: Implemented read/write ratio workload classification, wait event bottleneck fingerprinting, table churn defragmentation advisor, and auto-increment overflow warnings. +- [x] **Unit Testing Expansion**: Added dedicated tests `unit_innodb_internals.t`, `unit_replication_internals.t`, `unit_ha_cluster.t`, `unit_galera_pxc.t`, and `unit_workload_traffic.t`. All 100 test files (545 tests) passing cleanly. diff --git a/ROADMAP.md b/ROADMAP.md index 4b0132f48..9070937ec 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -179,16 +179,16 @@ To ensure consistency and high-density development, the following roles are defi * [x] **End-to-End Verification Audit**: Analysis of `source_verify_checksum` and `replica_sql_verify_checksum` settings. * [x] **Relay Log Hardening**: Verification of checksum validation before transaction application on replicas. -### Phase 11: Workload Analysis & Traffic Profiling [NOT STARTED] +### Phase 11: Workload Analysis & Traffic Profiling [COMPLETED] > Previously Phase 10. -* [ ] **Query Performance Profiling**: - * [ ] **Wait Event Fingerprinting**: Aggregation of `performance_schema` wait events to identify the primary database bottleneck (CPU, disk, lock, network). - * [ ] **Workload Characterization**: Automated classification of the database as Read-Heavy, Write-Heavy, or Mixed based on I/O ratios. -* [ ] **Metadata & Object Lifecycle**: - * [ ] **Table Churn & Fragmentation Advisor**: Identification of tables with frequent DML that require periodic `OPTIMIZE TABLE`. - * [ ] **Auto-Increment Exhaustion Audit**: Monitoring of large tables for potential auto-increment overflow (especially 32-bit integers). +* [x] **Query Performance Profiling**: + * [x] **Wait Event Fingerprinting**: Aggregation of `performance_schema` wait events to identify the primary database bottleneck (CPU, disk, lock, network). + * [x] **Workload Characterization**: Automated classification of the database as Read-Heavy, Write-Heavy, or Mixed based on I/O ratios. +* [x] **Metadata & Object Lifecycle**: + * [x] **Table Churn & Fragmentation Advisor**: Identification of tables with frequent DML that require periodic `OPTIMIZE TABLE`. + * [x] **Auto-Increment Exhaustion Audit**: Monitoring of large tables for potential auto-increment overflow (especially 32-bit integers). ### [Phase 12: Advanced Log Parser & Lock Monitoring](file:///documentation/specifications/roadmap_phase_xi_log_parser.md) [NOT STARTED] diff --git a/mysqltuner.pl b/mysqltuner.pl index 36848690a..aec483594 100755 --- a/mysqltuner.pl +++ b/mysqltuner.pl @@ -1711,6 +1711,144 @@ sub check_security_2_0 { } } +sub check_workload_traffic { + subheaderprint "Workload Analysis & Traffic Profiling"; + + # 1. Workload Characterization (Read-Heavy vs Write-Heavy vs Mixed) + my $com_select = $mystat{'Com_select'} // 0; + my $com_insert = $mystat{'Com_insert'} // 0; + my $com_update = $mystat{'Com_update'} // 0; + my $com_delete = $mystat{'Com_delete'} // 0; + my $com_replace = $mystat{'Com_replace'} // 0; + + my $total_reads = $com_select; + my $total_writes = $com_insert + $com_update + $com_delete + $com_replace; + my $total_ops = $total_reads + $total_writes; + + if ($total_ops > 0) { + my $read_ratio = $total_reads / $total_ops; + my $char_str = "Mixed"; + if ($read_ratio > 0.80) { + $char_str = "Read-Heavy (" . sprintf("%.1f%%", $read_ratio * 100) . " reads)"; + goodprint "Workload Characterization: $char_str"; + } elsif ($read_ratio < 0.20) { + $char_str = "Write-Heavy (" . sprintf("%.1f%%", (1 - $read_ratio) * 100) . " writes)"; + badprint "Workload Characterization: $char_str"; + push @generalrec, "Write-heavy workload detected. Optimize redo logs size, transaction log flushing, and doublewrite buffer."; + } else { + $char_str = "Mixed (" . sprintf("%.1f%%", $read_ratio * 100) . " reads, " . sprintf("%.1f%%", (1 - $read_ratio) * 100) . " writes)"; + goodprint "Workload Characterization: $char_str"; + } + } else { + infoprint "Workload Characterization: No query counters traffic recorded yet."; + } + + # 2. Wait Event Fingerprinting (Performance Schema Waits analysis) + if (defined $myvar{'performance_schema'} && $myvar{'performance_schema'} eq 'ON') { + my @wait_events = select_array( + "SELECT EVENT_NAME, SUM_TIMER_WAIT FROM performance_schema.events_waits_summary_global_by_event_name WHERE SUM_TIMER_WAIT > 0 AND EVENT_NAME NOT LIKE 'idle%' AND EVENT_NAME NOT LIKE 'wait/io/table/%' ORDER BY SUM_TIMER_WAIT DESC LIMIT 5" + ); + if (scalar(@wait_events) > 0) { + infoprint "Top Wait Events (excluding idle/table I/O):"; + my $disk_waits = 0; + my $lock_waits = 0; + my $net_waits = 0; + foreach my $we (@wait_events) { + my ($name, $wait) = split(/\|/, $we); + $name //= ''; + $wait //= 0; + infoprint " - $name: " . sprintf("%.2fs", $wait / 1000000000000); + + if ($name =~ m{^wait/io/file/}) { $disk_waits += $wait; } + elsif ($name =~ m{^wait/synch/}) { $lock_waits += $wait; } + elsif ($name =~ m{^wait/io/socket/}) { $net_waits += $wait; } + } + + my $total_wait = $disk_waits + $lock_waits + $net_waits; + if ($total_wait > 0) { + if ($disk_waits / $total_wait > 0.5) { + badprint "Primary DB Bottleneck detected: Disk/File I/O waits"; + push @generalrec, "Primary database bottleneck is Disk I/O. Check storage performance, swap usage, or increase buffer pool."; + } elsif ($lock_waits / $total_wait > 0.5) { + badprint "Primary DB Bottleneck detected: Synchronization Locks/Mutex waits"; + push @generalrec, "Primary database bottleneck is Lock contention. Optimize slow transactions and reduce lock wait timeout."; + } elsif ($net_waits / $total_wait > 0.5) { + badprint "Primary DB Bottleneck detected: Network Sockets waits"; + push @generalrec, "Primary database bottleneck is Network/Socket I/O. Check client connections latency and network bandwidth."; + } + } + } + } + + # 3. Table Churn & Fragmentation Advisor (Combining PFS write metrics and fragmentation stats) + if (defined $myvar{'performance_schema'} && $myvar{'performance_schema'} eq 'ON') { + my @churn_tables = select_array( + "SELECT OBJECT_SCHEMA, OBJECT_NAME, COUNT_WRITE FROM performance_schema.table_io_waits_summary_by_table WHERE OBJECT_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys') AND COUNT_WRITE > 1000 ORDER BY COUNT_WRITE DESC LIMIT 5" + ); + if (scalar(@churn_tables) > 0) { + my %churn_map; + foreach my $ct (@churn_tables) { + my ($schema, $table, $writes) = split(/\|/, $ct); + $churn_map{"$schema.$table"} = $writes if $schema && $table; + } + + my $matched_churn_frag = 0; + if (defined $result{'Tables'}{'Fragmented tables'}) { + foreach my $table_line ( @{ $result{'Tables'}{'Fragmented tables'} } ) { + my ( $table_schema, $table_name, $engine, $data_free ) = split /\t/msx, $table_line; + if (exists $churn_map{"$table_schema.$table_name"}) { + $matched_churn_frag++; + badprint "High-churn table `$table_schema`.`$table_name` is fragmented (free: " . hr_bytes($data_free) . ")"; + push @generalrec, "Defragment high-churn table `$table_schema`.`$table_name` to optimize DML space reclamation."; + } + } + } + if ($matched_churn_frag > 0) { + infoprint "Identified $matched_churn_frag high-churn fragmented tables."; + } + } + } + + # 4. Auto-Increment Exhaustion Audit + my @auto_inc_cols = select_array( + "SELECT t.TABLE_SCHEMA, t.TABLE_NAME, c.COLUMN_NAME, c.DATA_TYPE, t.AUTO_INCREMENT FROM information_schema.tables t JOIN information_schema.columns c ON t.table_schema = c.table_schema AND t.table_name = c.table_name WHERE c.extra = 'auto_increment' AND t.auto_increment IS NOT NULL AND t.table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')" + ); + + if (scalar(@auto_inc_cols) > 0) { + foreach my $col_info (@auto_inc_cols) { + my ($schema, $table, $col, $type, $curr_val) = split(/\|/, $col_info); + $type = lc($type // ''); + $curr_val //= 0; + + my $max_val = 0; + my $col_type = select_one("SELECT COLUMN_TYPE FROM information_schema.columns WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$col'") // ''; + my $is_unsigned = ($col_type =~ /unsigned/i) ? 1 : 0; + + if ($type eq 'tinyint') { + $max_val = $is_unsigned ? 255 : 127; + } elsif ($type eq 'smallint') { + $max_val = $is_unsigned ? 65535 : 32767; + } elsif ($type eq 'mediumint') { + $max_val = $is_unsigned ? 16777215 : 8388607; + } elsif ($type eq 'int' || $type eq 'integer') { + $max_val = $is_unsigned ? 4294967295 : 2147483647; + } elsif ($type eq 'bigint') { + $max_val = $is_unsigned ? 18446744073709551615 : 9223372036854775807; + } + + if ($max_val > 0) { + my $ratio = $curr_val / $max_val; + if ($ratio > 0.80) { + badprint "Auto-increment column `$schema`.`$table`.`$col` ($type) is near exhaustion: current value $curr_val (" . sprintf("%.1f%%", $ratio * 100) . " of max $max_val)"; + push @generalrec, "Danger of auto-increment overflow on `$schema`.`$table`.`$col`. Upgrade type to BIGINT or reset sequence."; + } elsif ($ratio > 0.50) { + infoprint "Auto-increment column `$schema`.`$table`.`$col` ($type) has reached " . sprintf("%.1f%%", $ratio * 100) . " capacity."; + } + } + } + } +} + sub generate_auto_fix_snippets { return if $opt{'silent'} || $opt{'json'} || $opt{'yaml'}; subheaderprint "Guided Auto-Fix Snippets"; @@ -15764,6 +15902,7 @@ sub dump_csv_files { \&get_replication_status, \&process_sysbench_metrics, \&historical_comparison, \&predictive_capacity_analysis, \&check_replication_advanced, \&check_security_2_0, + \&check_workload_traffic, \&generate_auto_fix_snippets, ); diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 4c0f11bca..48e8d4488 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -35,9 +35,9 @@ - feat(report): add verbose timings, step percentages, and snapshot summary - feat(report): finalize HTML report, YAML output, and historical comparison - feat(report): implement HA InnoDB Cluster diagnostics and Group Replication checks -- feat(report): implement advanced Galera Cluster 4 & PXC 8.0 diagnostics - feat(report): implement advanced Galera Cluster 4 and PXC 8.0 diagnostics - feat(report): implement phase 6 deep engine tuning and mark phase 8 completed +- feat(report): implement workload traffic profiling and query waits analysis - fix(main): add undefined/NULL guards to hr_num to resolve uninitialized warnings - fix(main): address PR #931 code review feedback and enhance test validations - fix(main): calculate query cache efficiency using Com_select on MariaDB (MDEV-4981) @@ -48,11 +48,12 @@ - test(test): add unit_galera_pxc.t to validate Phase 9 Galera cluster checks - test(test): add unit_ha_cluster.t to validate Phase 7 HA cluster checks - test(test): add unit_replication_internals.t to validate Phase 8 replication features +- test(test): add unit_workload_traffic.t to validate Phase 11 workload profiling - docs(metadata): promote HTML reports and add E2E examples - docs(metadata): remove timestamp from doc-sync generated files - docs(releases): regenerate release notes and changelog - docs(roadmap): link strategic technical evolutions specification and enforce changelog staging -- docs(roadmap): update Phase 7, Phase 9, and Phase 10 status to completed in ROADMAP.md +- docs(roadmap): update Phase 7, Phase 9, Phase 10, and Phase 11 status to completed in ROADMAP.md - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: regenerate release notes - docs: update potential issues log with Phase 9 Galera audit findings @@ -71,6 +72,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (a3b5ef6) - docs: regenerate release notes (5fe8d27) - docs: update potential issues log with Phase 9 Galera audit findings (af711cc) - docs: regenerate release notes (5d424b7) diff --git a/tests/unit_workload_traffic.t b/tests/unit_workload_traffic.t new file mode 100644 index 000000000..ae5681c16 --- /dev/null +++ b/tests/unit_workload_traffic.t @@ -0,0 +1,124 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; +use FindBin; +use lib "$FindBin::Bin/.."; + +# Mock MySQLTuner environment and helper modules +require 'mysqltuner.pl'; + +# Helper for resetting state +sub reset_workload_state { + no warnings 'once'; + %main::myvar = (); + %main::mystat = (); + @main::generalrec = (); + @main::adjvars = (); + $main::is_local_only = 0; + + # Defaults + $main::myvar{'performance_schema'} = 'ON'; +} + +# Subtest 1: Workload Characterization +subtest 'Workload Characterization' => sub { + no warnings 'once'; + # Test 1a: Read-heavy workload + reset_workload_state(); + $main::mystat{'Com_select'} = 900; + $main::mystat{'Com_insert'} = 50; + $main::mystat{'Com_update'} = 30; + $main::mystat{'Com_delete'} = 20; + main::check_workload_traffic(); + ok(!grep(/Write-heavy workload detected/, @main::generalrec), 'No write-heavy warning for read-heavy workload'); + + # Test 1b: Write-heavy workload + reset_workload_state(); + $main::mystat{'Com_select'} = 50; + $main::mystat{'Com_insert'} = 500; + $main::mystat{'Com_update'} = 300; + $main::mystat{'Com_delete'} = 150; + main::check_workload_traffic(); + ok(grep(/Write-heavy workload detected/, @main::generalrec), 'Warns and recommends on write-heavy workload'); +}; + +# Subtest 2: Wait Event Fingerprinting +subtest 'Wait Event Fingerprinting' => sub { + no warnings 'redefine'; + # Test 2a: Disk I/O bottleneck + reset_workload_state(); + local *main::select_array = sub { + my $sql = shift; + if ($sql =~ /events_waits_summary_global_by_event_name/i) { + return ( + 'wait/io/file/innodb/innodb_data_file|800000000000000', + 'wait/synch/mutex/innodb/buf_pool_mutex|100000000000000', + 'wait/io/socket/mysql/client_connection|5000000000000 +' + ); + } + return (); + }; + main::check_workload_traffic(); + ok(grep(/Primary database bottleneck is Disk I\/O/, @main::generalrec), 'Warns on Disk I/O bottleneck'); + + # Test 2b: Lock contention bottleneck + reset_workload_state(); + local *main::select_array = sub { + my $sql = shift; + if ($sql =~ /events_waits_summary_global_by_event_name/i) { + return ( + 'wait/synch/mutex/innodb/buf_pool_mutex|900000000000000', + 'wait/io/file/innodb/innodb_data_file|50000000000000' + ); + } + return (); + }; + main::check_workload_traffic(); + ok(grep(/Primary database bottleneck is Lock contention/, @main::generalrec), 'Warns on lock contention bottleneck'); +}; + +# Subtest 3: Table Churn & Fragmentation Alignment +subtest 'Table Churn & Fragmentation Alignment' => sub { + no warnings 'redefine', 'once'; + reset_workload_state(); + # Mock table churn queries + local *main::select_array = sub { + my $sql = shift; + if ($sql =~ /table_io_waits_summary_by_table/i) { + return ('test_db|active_table|15000'); + } + return (); + }; + # Mock fragmented tables list + $main::result{'Tables'}{'Fragmented tables'} = [ "test_db\tactive_table\tInnoDB\t104857600" ]; + main::check_workload_traffic(); + ok(grep(/Defragment high-churn table `test_db`.`active_table`/, @main::generalrec), 'Suggests defragmentation on high-churn table'); +}; + +# Subtest 4: Auto-Increment Exhaustion Audit +subtest 'Auto-Increment Exhaustion Audit' => sub { + no warnings 'redefine', 'once'; + reset_workload_state(); + # Mock auto increment columns query + local *main::select_array = sub { + my $sql = shift; + if ($sql =~ /information_schema\.tables/i) { + return ('test_db|large_table|id|int|3500000000'); + } + return (); + }; + # Mock column type details (unsigned) + local *main::select_one = sub { + my $sql = shift; + if ($sql =~ /COLUMN_TYPE/i) { + return 'int(11) unsigned'; + } + return ''; + }; + main::check_workload_traffic(); + ok(grep(/Danger of auto-increment overflow on `test_db`.`large_table`.`id`/, @main::generalrec), 'Warns when auto-increment is near exhaustion'); +}; + +done_testing(); From c2ec01e2db4289ba83c9a30ae592719df718a084 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:10:37 +0200 Subject: [PATCH 29/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 48e8d4488..eb60f0b86 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -72,6 +72,7 @@ ## 🛠️ Internal Commit History +- feat(report): implement workload traffic profiling and query waits analysis (89fa655) - docs: regenerate release notes (a3b5ef6) - docs: regenerate release notes (5fe8d27) - docs: update potential issues log with Phase 9 Galera audit findings (af711cc) From d5046d47e9a3baacc9fe08ef783c29e1247f1995 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:11:41 +0200 Subject: [PATCH 30/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index eb60f0b86..963ac3437 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -72,6 +72,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (c2ec01e) - feat(report): implement workload traffic profiling and query waits analysis (89fa655) - docs: regenerate release notes (a3b5ef6) - docs: regenerate release notes (5fe8d27) From 0842ba6853bf8b8eadd52d1fde8810d23f9b6129 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:25:49 +0200 Subject: [PATCH 31/97] docs: update potential issues log with Renovate dependency dashboard analysis --- Changelog | 1 + POTENTIAL_ISSUES.md | 4 ++-- releases/v2.9.1.md | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index c1c69ebb7..24a956f6b 100644 --- a/Changelog +++ b/Changelog @@ -52,6 +52,7 @@ - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: regenerate release notes - docs: update potential issues log with Phase 9 Galera audit findings +- docs: update potential issues log with Renovate dependency dashboard analysis - docs: update potential issues log with release v2.9.0 and v2.9.1 audit results - docs: update repository links to major/MySQLTuner-perl and add GitHub stars badge diff --git a/POTENTIAL_ISSUES.md b/POTENTIAL_ISSUES.md index 14afa1c4d..f0a0367e4 100644 --- a/POTENTIAL_ISSUES.md +++ b/POTENTIAL_ISSUES.md @@ -234,11 +234,11 @@ This file records anomalies discovered during laboratory testing (Perl warnings, - [x] **Upgraded Dev Dependencies**: Boosted `@commitlint/cli` and Conventional Commits toolings to patch levels. - [x] **GitHub Actions Pinning**: Pinned action digests to specific commit hashes (`actions/checkout@v7.0.0`, etc.). +- [x] **Renovate Dependency Audit**: Resolved dependency dashboard issue #587; analyzed risk of abandoned dependencies (cz-conventional-changelog, husky) and successfully pinned remaining GitHub Actions to secure hashes. - [x] **Phase 6 InnoDB Tuning**: Implemented I/O pressure warnings, read-ahead eviction ratio audit, purge lag alerts, SSD doublewrite/fdatasync alignment, and AHI optimization checks. - [x] **Performance Schema Analytics**: Added global lock deadlock count tracking via events errors. - [x] **HA InnoDB Cluster Diagnostics**: Implemented Group Replication member status, single-primary role validation, flow control queue tracking, certification conflicts, and MySQL Router connections awareness. - [x] **Advanced Galera & PXC Diagnostics**: Added streaming replication fragments audit, gcache size safety warnings, certification conflicts, flow control culprit node detection, and pxc_strict_mode audit. -- [x] **Workload & Traffic Profiling**: Implemented read/write ratio workload classification, wait event bottleneck fingerprinting, table churn defragmentation advisor, and auto-increment overflow warnings. -- [x] **Unit Testing Expansion**: Added dedicated tests `unit_innodb_internals.t`, `unit_replication_internals.t`, `unit_ha_cluster.t`, `unit_galera_pxc.t`, and `unit_workload_traffic.t`. All 100 test files (545 tests) passing cleanly. +- [x] **Unit Testing Expansion**: Added dedicated tests `unit_innodb_internals.t`, `unit_replication_internals.t`, `unit_ha_cluster.t`, and `unit_galera_pxc.t`. All 99 test files (541 tests) passing cleanly. diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 963ac3437..1c1daccfd 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -57,6 +57,7 @@ - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: regenerate release notes - docs: update potential issues log with Phase 9 Galera audit findings +- docs: update potential issues log with Renovate dependency dashboard analysis - docs: update potential issues log with release v2.9.0 and v2.9.1 audit results - docs: update repository links to major/MySQLTuner-perl and add GitHub stars badge ``` @@ -72,6 +73,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (d5046d4) - docs: regenerate release notes (c2ec01e) - feat(report): implement workload traffic profiling and query waits analysis (89fa655) - docs: regenerate release notes (a3b5ef6) From 0ec038fc7d7cf88842074944018e61c9fb941a87 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:25:49 +0200 Subject: [PATCH 32/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 1c1daccfd..9beaa333c 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -73,6 +73,7 @@ ## 🛠️ Internal Commit History +- docs: update potential issues log with Renovate dependency dashboard analysis (0842ba6) - docs: regenerate release notes (d5046d4) - docs: regenerate release notes (c2ec01e) - feat(report): implement workload traffic profiling and query waits analysis (89fa655) From c5fdc7dc346e8c05d580c1eb06465fcf9203860e Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:26:22 +0200 Subject: [PATCH 33/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 9beaa333c..e25689d5a 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -73,6 +73,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (0ec038f) - docs: update potential issues log with Renovate dependency dashboard analysis (0842ba6) - docs: regenerate release notes (d5046d4) - docs: regenerate release notes (c2ec01e) From c328efddf8d9d099b9c992283ecd0504ec303c8b Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:30:49 +0200 Subject: [PATCH 34/97] test(test): add unit tests for potential issues and split unit_coverage_boost3 --- Changelog | 4 + releases/v2.9.1.md | 5 + tests/unit_coverage_boost3.t | 583 ---------------------------- tests/unit_coverage_boost_plugins.t | 240 ++++++++++++ tests/unit_coverage_boost_queries.t | 232 +++++++++++ tests/unit_coverage_boost_system.t | 274 +++++++++++++ tests/unit_deadlocks_pfs.t | 47 +++ tests/unit_eol_warnings.t | 30 ++ tests/unit_version_helpers.t | 35 ++ 9 files changed, 867 insertions(+), 583 deletions(-) delete mode 100644 tests/unit_coverage_boost3.t create mode 100644 tests/unit_coverage_boost_plugins.t create mode 100644 tests/unit_coverage_boost_queries.t create mode 100644 tests/unit_coverage_boost_system.t create mode 100644 tests/unit_deadlocks_pfs.t create mode 100644 tests/unit_eol_warnings.t create mode 100644 tests/unit_version_helpers.t diff --git a/Changelog b/Changelog index 24a956f6b..87bfcce86 100644 --- a/Changelog +++ b/Changelog @@ -39,10 +39,14 @@ - fix: update documentation and code - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 +- test(test): add unit_deadlocks_pfs.t to validate InnoDB deadlock PFS checks +- test(test): add unit_eol_warnings.t to validate version support warnings - test(test): add unit_galera_pxc.t to validate Phase 9 Galera cluster checks - test(test): add unit_ha_cluster.t to validate Phase 7 HA cluster checks - test(test): add unit_replication_internals.t to validate Phase 8 replication features +- test(test): add unit_version_helpers.t to validate version caching and helpers - test(test): add unit_workload_traffic.t to validate Phase 11 workload profiling +- test(test): split unit_coverage_boost3.t into three smaller test files - ci: optimize pre-commit hook to only run unit tests when code, tests, or dependencies are modified - docs(metadata): promote HTML reports and add E2E examples - docs(metadata): remove timestamp from doc-sync generated files diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index e25689d5a..c078b25cf 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -45,10 +45,14 @@ - fix(main): only recommend increasing innodb_log_buffer_size when log waits occur - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 +- test(test): add unit_deadlocks_pfs.t to validate InnoDB deadlock PFS checks +- test(test): add unit_eol_warnings.t to validate version support warnings - test(test): add unit_galera_pxc.t to validate Phase 9 Galera cluster checks - test(test): add unit_ha_cluster.t to validate Phase 7 HA cluster checks - test(test): add unit_replication_internals.t to validate Phase 8 replication features +- test(test): add unit_version_helpers.t to validate version caching and helpers - test(test): add unit_workload_traffic.t to validate Phase 11 workload profiling +- test(test): split unit_coverage_boost3.t into three smaller test files - docs(metadata): promote HTML reports and add E2E examples - docs(metadata): remove timestamp from doc-sync generated files - docs(releases): regenerate release notes and changelog @@ -73,6 +77,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (c5fdc7d) - docs: regenerate release notes (0ec038f) - docs: update potential issues log with Renovate dependency dashboard analysis (0842ba6) - docs: regenerate release notes (d5046d4) diff --git a/tests/unit_coverage_boost3.t b/tests/unit_coverage_boost3.t deleted file mode 100644 index 756e19e46..000000000 --- a/tests/unit_coverage_boost3.t +++ /dev/null @@ -1,583 +0,0 @@ -#!/usr/bin/env perl -# PI-006 Coverage Boost Part 3: Deep-mocked diagnostic subroutines -# Targets: system_recommendations, mysql_indexes, mysql_plugins, -# check_query_anti_patterns, check_metadata_perf, mariadb_query_cache_info, -# process_sysbench_metrics, select_*_db, infocmd*, is_open_port, -# get_opened_ports, get_process_memory, historical_comparison -use strict; -use warnings; -no warnings 'once'; -use Test::More; -use File::Basename; -use File::Spec; -use File::Temp qw(tempfile tempdir); -use Cwd 'abs_path'; - -$SIG{__WARN__} = sub { warn $_[0] unless $_[0] =~ /redefined/ }; - -# Declare globals before loading script -our @adjvars; -our @generalrec; -our @modeling; -our @sysrec; -our @secrec; -our %opt; -our %myvar; -our %mystat; -our %mycalc; -our %result; -our @dblist; - -my $script_dir = dirname(abs_path(__FILE__)); -my $script = abs_path(File::Spec->catfile($script_dir, '..', 'mysqltuner.pl')); -{ - local @ARGV = (); - no warnings 'redefine'; - require $script; -} - -# --- Shared mock infrastructure --- -my @mock_output; -my %mock_queries; - -sub reset_mocks { - @mock_output = (); - @main::generalrec = (); - @main::adjvars = (); - @main::modeling = (); - @main::sysrec = (); - @main::secrec = (); - %main::result = (); - %mock_queries = (); -} - -{ - no warnings 'redefine'; - *main::infoprint = sub { push @mock_output, "INFO: $_[0]" }; - *main::badprint = sub { push @mock_output, "BAD: $_[0]" }; - *main::goodprint = sub { push @mock_output, "GOOD: $_[0]" }; - *main::debugprint = sub { }; - *main::subheaderprint = sub { push @mock_output, "HEADER: $_[0]" }; - *main::prettyprint = sub { push @mock_output, "PRETTY: $_[0]" }; - *main::select_array = sub { - my ($query) = @_; - foreach my $pattern (keys %mock_queries) { - if ($query =~ /$pattern/si) { - return @{$mock_queries{$pattern}}; - } - } - return (); - }; - *main::select_one = sub { - my ($query) = @_; - foreach my $pattern (keys %mock_queries) { - if ($query =~ /$pattern/si) { - my @res = @{$mock_queries{$pattern}}; - return $res[0] // ''; - } - } - return ''; - }; - *main::select_user_dbs = sub { return @dblist; }; -} - -# ===================================================================== -# 1. mysql_plugins -# ===================================================================== -subtest 'mysql_plugins - disabled via opt' => sub { - reset_mocks(); - $main::opt{plugininfo} = 0; - main::mysql_plugins(); - is(scalar @mock_output, 0, "No output when plugininfo=0"); -}; - -subtest 'mysql_plugins - with plugins' => sub { - reset_mocks(); - $main::opt{plugininfo} = 1; - %mock_queries = ( - 'PLUGIN_NAME.*PLUGIN_STATUS' => [ - "InnoDB\t5.7\tACTIVE\tSTORAGE ENGINE", - "MyISAM\t1.0\tACTIVE\tSTORAGE ENGINE", - ], - ); - main::mysql_plugins(); - ok(grep({ /InnoDB/ } @mock_output), "InnoDB plugin listed"); - ok(grep({ /MyISAM/ } @mock_output), "MyISAM plugin listed"); -}; - -subtest 'mysql_plugins - no plugins' => sub { - reset_mocks(); - $main::opt{plugininfo} = 1; - %mock_queries = (); - main::mysql_plugins(); - ok(grep({ /No ACTIVE plugins/ } @mock_output), "Reports no active plugins"); -}; - -# ===================================================================== -# 2. check_metadata_perf -# ===================================================================== -subtest 'check_metadata_perf - ON triggers badprint' => sub { - reset_mocks(); - $main::myvar{'innodb_stats_on_metadata'} = 'ON'; - %mock_queries = ( 'SET GLOBAL' => ['OK'] ); - my $ret = main::check_metadata_perf(); - is($ret, 1, "Returns 1 when ON"); - ok(grep({ /BAD:.*Stat are updated/ } @mock_output), "Badprint issued"); - ok(grep({ /innodb_stats_on_metadata/ } @main::adjvars), "Adjustment var pushed"); -}; - -subtest 'check_metadata_perf - OFF is good' => sub { - reset_mocks(); - $main::myvar{'innodb_stats_on_metadata'} = 'OFF'; - my $ret = main::check_metadata_perf(); - is($ret, 0, "Returns 0 when OFF"); - ok(grep({ /GOOD:.*No stat updates/ } @mock_output), "Goodprint issued"); -}; - -subtest 'check_metadata_perf - undefined var' => sub { - reset_mocks(); - delete $main::myvar{'innodb_stats_on_metadata'}; - my $ret = main::check_metadata_perf(); - is($ret, 0, "Returns 0 when var undefined"); -}; - -# ===================================================================== -# 3. mariadb_query_cache_info -# ===================================================================== -subtest 'mariadb_query_cache_info - not MariaDB' => sub { - reset_mocks(); - $main::myvar{'version'} = '8.0.35'; - $main::myvar{'version_comment'} = 'MySQL Community Server'; - main::mariadb_query_cache_info(); - ok(grep({ /Not a MariaDB/ } @mock_output), "Skips for MySQL"); -}; - -subtest 'mariadb_query_cache_info - MariaDB no plugin' => sub { - reset_mocks(); - $main::myvar{'version'} = '11.4.0-MariaDB'; - $main::myvar{'version_comment'} = 'MariaDB Server'; - %mock_queries = ( 'QUERY_CACHE_INFO' => ['DISABLED'] ); - main::mariadb_query_cache_info(); - ok(grep({ /not active or not installed/ } @mock_output), "Reports plugin not active"); -}; - -subtest 'mariadb_query_cache_info - MariaDB with active plugin' => sub { - reset_mocks(); - $main::myvar{'version'} = '11.4.0-MariaDB'; - $main::myvar{'version_comment'} = 'MariaDB Server'; - { - no warnings 'redefine'; - local *main::select_one = sub { - my ($query) = @_; - return 'ACTIVE' if $query =~ /QUERY_CACHE_INFO/; - return ''; - }; - %mock_queries = ( - 'query_cache_info' => ['mydb;;SELECT * FROM t1;;5;;1024'], - ); - main::mariadb_query_cache_info(); - } - ok(grep({ /GOOD:.*QUERY_CACHE_INFO plugin is installed/ } @mock_output), "Plugin active reported"); -}; - -# ===================================================================== -# 4. check_query_anti_patterns -# ===================================================================== -subtest 'check_query_anti_patterns - old version skips' => sub { - reset_mocks(); - $main::myvar{'version'} = '5.5.0'; - { - no warnings 'redefine'; - local *main::mysql_version_ge = sub { return 0; }; - main::check_query_anti_patterns(); - } - ok(grep({ /Skipped.*5\.6/ } @mock_output), "Skips for version < 5.6"); -}; - -subtest 'check_query_anti_patterns - PFS disabled' => sub { - reset_mocks(); - { - no warnings 'redefine'; - local *main::mysql_version_ge = sub { return 1; }; - %mock_queries = ( 'performance_schema' => [undef] ); - # select_one for SHOW VARIABLES LIKE 'performance_schema' returns non-ON - local *main::select_one = sub { return ''; }; - main::check_query_anti_patterns(); - } - ok(grep({ /Performance Schema is disabled/ } @mock_output), "Skips when PFS disabled"); -}; - -subtest 'check_query_anti_patterns - full scans detected' => sub { - reset_mocks(); - { - no warnings 'redefine'; - local *main::mysql_version_ge = sub { return 1; }; - local *main::select_one = sub { return 'ON'; }; - %mock_queries = ( - 'sum_no_index_used' => [ - "SELECT * FROM users WHERE name LIKE '%test%'\t1000\t500\t0", - "SELECT * FROM orders\t2000\t1500\t0", - ], - 'sum_created_tmp_disk_tables' => [], - ); - main::check_query_anti_patterns(); - } - ok(grep({ /BAD:.*Found 2 query digests/ } @mock_output), "Detects full scan queries"); - ok(grep({ /Optimize queries/ } @main::generalrec), "Recommendation pushed"); -}; - -subtest 'check_query_anti_patterns - no issues' => sub { - reset_mocks(); - { - no warnings 'redefine'; - local *main::mysql_version_ge = sub { return 1; }; - local *main::select_one = sub { return 'ON'; }; - %mock_queries = ( - 'sum_no_index_used' => [], - 'sum_created_tmp_disk_tables' => [], - ); - main::check_query_anti_patterns(); - } - ok(grep({ /GOOD:.*No major full table scan/ } @mock_output), "Reports clean scan status"); -}; - -# ===================================================================== -# 5. mysql_indexes -# ===================================================================== -subtest 'mysql_indexes - disabled via opt' => sub { - reset_mocks(); - $main::opt{idxstat} = 0; - main::mysql_indexes(); - is(scalar @mock_output, 0, "No output when idxstat=0"); -}; - -subtest 'mysql_indexes - old version skips' => sub { - reset_mocks(); - $main::opt{idxstat} = 1; - { - no warnings 'redefine'; - local *main::mysql_version_ge = sub { return 0; }; - main::mysql_indexes(); - } - ok(grep({ /Index metrics.*missing/ } @mock_output), "Skips for old version"); -}; - -subtest 'mysql_indexes - with selectivity data' => sub { - reset_mocks(); - $main::opt{idxstat} = 1; - $main::opt{'ignore-tables'} = ''; - $main::myvar{'performance_schema'} = 'OFF'; - @main::dblist = ('testdb'); - { - no warnings 'redefine'; - local *main::mysql_version_ge = sub { return 1; }; - %mock_queries = ( - 'ORDER BY sel' => [ - "testdb.users idx_email(email) 1 2 500 10000 BTREE 5.00", - ], - 'GROUP BY table_name' => [ - "users.idx_email email 500 NULL BTREE", - ], - 'count.*BASE TABLE.*TABLE_SCHEMA' => ['3'], - ); - main::mysql_indexes(); - } - ok(grep({ /Worst selectivity/ } @mock_output), "Shows selectivity header"); - ok(grep({ /idx_email/ } @mock_output), "Index name appears in output"); -}; - -# ===================================================================== -# 6. system_recommendations - skip paths -# ===================================================================== -subtest 'system_recommendations - remote skip' => sub { - reset_mocks(); - { - no warnings 'redefine'; - local *main::is_remote = sub { return 1; }; - $main::is_cloud = 0; - main::system_recommendations(); - } - ok(grep({ /Skipping local system checks/ } @mock_output), "Skips for remote host"); -}; - -subtest 'system_recommendations - remote host recap' => sub { - reset_mocks(); - $main::is_cloud = 1; - $main::cloud_type = 'AWS RDS'; - $main::myvar{'hostname'} = 'my-rds-db'; - $main::myvar{'version_compile_os'} = 'Linux'; - $main::myvar{'version_compile_machine'} = 'x86_64'; - $main::physical_memory = 8589934592; # 8 GB - $main::mystat{'Uptime'} = 86400; # 1 day - { - no warnings 'redefine'; - local *main::is_remote = sub { return 1; }; - main::system_recommendations(); - } - ok(grep({ /Skipping local system checks/ } @mock_output), "Skips local checks message shown"); - ok(grep({ /Machine type.*Cloud instance \(AWS RDS\)/ } @mock_output), "Machine type shown"); - ok(grep({ /Host Name.*my-rds-db/ } @mock_output), "Host name shown"); - ok(grep({ /Operating System Type.*Linux/ } @mock_output), "Operating system shown"); - ok(grep({ /CPU Architecture.*x86_64/ } @mock_output), "CPU Architecture shown"); - ok(grep({ /Physical Memory \(RAM\).*8.0G/ } @mock_output), "RAM shown"); - ok(grep({ /Database Uptime.*1d 0h 0m/ } @mock_output), "Database Uptime shown"); - ok(grep({ /There is at least 1.5 Gb/ } @mock_output), "Physical RAM check run"); -}; - -subtest 'system_recommendations - sysstat disabled' => sub { - reset_mocks(); - $main::opt{sysstat} = 0; - { - no warnings 'redefine'; - local *main::is_remote = sub { return 0; }; - $main::is_cloud = 0; - main::system_recommendations(); - } - is(scalar @mock_output, 0, "No output when sysstat=0"); -}; - -subtest 'system_recommendations - non-Linux skip' => sub { - reset_mocks(); - $main::opt{sysstat} = 1; - { - no warnings 'redefine'; - local *main::is_remote = sub { return 0; }; - $main::is_cloud = 0; - local *main::execute_system_command = sub { return 'FreeBSD'; }; - $main::is_win = 0; - main::system_recommendations(); - } - ok(grep({ /Skipped due to non Linux/ } @mock_output), "Skips for non-Linux"); -}; - -# ===================================================================== -# 7. process_sysbench_metrics -# ===================================================================== -subtest 'process_sysbench_metrics - no file skips' => sub { - reset_mocks(); - $main::opt{'sysbench-file'} = ''; - main::process_sysbench_metrics(); - is(scalar @mock_output, 0, "No output without sysbench file"); -}; - -subtest 'process_sysbench_metrics - file not found' => sub { - reset_mocks(); - $main::opt{'sysbench-file'} = '/nonexistent/sysbench.log'; - main::process_sysbench_metrics(); - ok(grep({ /BAD:.*not found/ } @mock_output), "Reports file not found"); -}; - -subtest 'process_sysbench_metrics - parses valid output' => sub { - reset_mocks(); - my $tmpdir = tempdir(CLEANUP => 1); - my $path = "$tmpdir/sysbench.log"; - main::string2file($path, <<'EOF'); -SQL statistics: - queries: 50000 (2500.50 per sec.) - transactions: 25000 (1250.25 per sec.) -Latency (ms): - avg: 3.45 - 95th percentile: 7.89 - max: 125.67 -EOF - $main::opt{'sysbench-file'} = $path; - main::process_sysbench_metrics(); - ok(grep({ /GOOD:.*TPS: 1250\.25/ } @mock_output), "TPS parsed"); - ok(grep({ /GOOD:.*QPS: 2500\.50/ } @mock_output), "QPS parsed"); - is($main::result{'Sysbench'}{'TPS'}, '1250.25', "TPS stored in result hash"); - is($main::result{'Sysbench'}{'QPS'}, '2500.50', "QPS stored in result hash"); -}; - -# ===================================================================== -# 8. select_*_db functions (thin wrappers over select_array) -# ===================================================================== -subtest 'select_tables_db delegates to select_array' => sub { - reset_mocks(); - %mock_queries = ( "TABLE_SCHEMA='mydb'" => ['users', 'orders'] ); - my @tables = main::select_tables_db('mydb'); - is_deeply(\@tables, ['users', 'orders'], "select_tables_db returns tables"); -}; - -subtest 'select_indexes_db delegates to select_array' => sub { - reset_mocks(); - %mock_queries = ( "TABLE_SCHEMA='mydb'" => ['idx_email', 'PRIMARY'] ); - my @indexes = main::select_indexes_db('mydb'); - is_deeply(\@indexes, ['idx_email', 'PRIMARY'], "select_indexes_db returns indexes"); -}; - -subtest 'select_views_db delegates to select_array' => sub { - reset_mocks(); - %mock_queries = ( "TABLE_SCHEMA='mydb'" => ['v_active_users'] ); - my @views = main::select_views_db('mydb'); - is_deeply(\@views, ['v_active_users'], "select_views_db returns views"); -}; - -subtest 'select_triggers_db delegates to select_array' => sub { - reset_mocks(); - %mock_queries = ( "TRIGGER_SCHEMA='mydb'" => ['trg_audit'] ); - my @triggers = main::select_triggers_db('mydb'); - is_deeply(\@triggers, ['trg_audit'], "select_triggers_db returns triggers"); -}; - -subtest 'select_routines_db delegates to select_array' => sub { - reset_mocks(); - %mock_queries = ( "ROUTINE_SCHEMA='mydb'" => ['sp_cleanup'] ); - my @routines = main::select_routines_db('mydb'); - is_deeply(\@routines, ['sp_cleanup'], "select_routines_db returns routines"); -}; - -subtest 'select_table_indexes_db delegates to select_array' => sub { - reset_mocks(); - %mock_queries = ( "TABLE_SCHEMA='mydb'.*TABLE_NAME='users'" => ['PRIMARY', 'idx_email'] ); - my @indexes = main::select_table_indexes_db('mydb', 'users'); - is_deeply(\@indexes, ['PRIMARY', 'idx_email'], "select_table_indexes_db returns indexes"); -}; - -# ===================================================================== -# 9. infocmd / infocmd_tab / infocmd_one -# ===================================================================== -subtest 'infocmd executes and prints output' => sub { - reset_mocks(); - { - no warnings 'redefine'; - local *main::execute_system_command = sub { return ("result_line1\n", "result_line2\n"); }; - main::infocmd("echo test"); - } - ok(grep({ /result_line1/ } @mock_output), "infocmd prints command output"); -}; - -subtest 'infocmd_tab prints tabbed output' => sub { - reset_mocks(); - { - no warnings 'redefine'; - local *main::execute_system_command = sub { return ("tabbed_line\n"); }; - main::infocmd_tab("echo test"); - } - ok(grep({ /\ttabbed_line/ } @mock_output), "infocmd_tab adds tab prefix"); -}; - -subtest 'infocmd_one returns joined string' => sub { - reset_mocks(); - my $result; - { - no warnings 'redefine'; - local *main::execute_system_command = sub { return ("val1\n", "val2\n"); }; - $result = main::infocmd_one("echo test"); - } - like($result, qr/val1.*val2/, "infocmd_one joins output"); -}; - -# ===================================================================== -# 10. get_opened_ports / is_open_port (with mocked execute_system_command) -# ===================================================================== -subtest 'get_opened_ports parses netstat' => sub { - reset_mocks(); - $main::is_win = 0; - { - no warnings 'redefine'; - local *main::execute_system_command = sub { - return ( - "tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN\n", - "tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN\n", - "tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN\n", - ); - }; - my @ports = main::get_opened_ports(); - ok(grep({ $_ == 22 } @ports), "Port 22 detected"); - ok(grep({ $_ == 3306 } @ports), "Port 3306 detected"); - ok(grep({ $_ == 80 } @ports), "Port 80 detected"); - } -}; - -subtest 'is_open_port returns 1 for open port' => sub { - reset_mocks(); - $main::is_win = 0; - { - no warnings 'redefine'; - local *main::execute_system_command = sub { - return ( - "tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN\n", - "tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN\n", - ); - }; - is(main::is_open_port(3306), 1, "3306 is open"); - is(main::is_open_port(8080), 0, "8080 is not open"); - } -}; - -# ===================================================================== -# 11. get_process_memory -# ===================================================================== -subtest 'get_process_memory returns 0 on Windows' => sub { - local $main::is_win = 1; - my $mem = main::get_process_memory($$); - is($mem, 0, "Returns 0 on Windows"); -}; - -# ===================================================================== -# 12. historical_comparison - skip paths -# ===================================================================== -subtest 'historical_comparison - no file skips' => sub { - reset_mocks(); - $main::opt{'compare-file'} = ''; - main::historical_comparison(); - is(scalar @mock_output, 0, "No output without compare-file"); -}; - -subtest 'historical_comparison - file not found' => sub { - reset_mocks(); - $main::opt{'compare-file'} = '/nonexistent/old_results.json'; - main::historical_comparison(); - ok(grep({ /BAD:.*not found/ } @mock_output), "Reports file not found"); -}; - -# ===================================================================== -# 13. select_one_g / select_str_g (with mocked execute_system_command) -# ===================================================================== -subtest 'select_one_g extracts matching line' => sub { - reset_mocks(); - { - no warnings 'redefine'; - local *main::execute_system_command = sub { - return ( - " Variable_name: Threads_running\n", - " Value: 5\n", - ); - }; - $main::mysqlcmd = 'mysql'; - $main::mysqllogin = ''; - $main::devnull = '/dev/null'; - my $result = main::select_one_g('Value', 'SHOW STATUS LIKE "Threads_running"'); - like($result, qr/Value.*5/, "Extracts matching line"); - } -}; - -subtest 'select_str_g extracts value after colon' => sub { - reset_mocks(); - { - no warnings 'redefine'; - local *main::execute_system_command = sub { - return ( - " Variable_name: slow_query_log\n", - " Value: ON\n", - ); - }; - $main::mysqlcmd = 'mysql'; - $main::mysqllogin = ''; - $main::devnull = '/dev/null'; - my $result = main::select_str_g('Value', 'SHOW VARIABLES LIKE "slow_query_log"'); - like($result, qr/ON/, "Extracts value after colon"); - } -}; - -# ===================================================================== -# 14. select_csv_file (from get_state_file_path context) -# ===================================================================== -subtest 'select_csv_file smoke' => sub { - reset_mocks(); - # select_csv_file internally uses $basic_password_files or loads CSV - # Just verify it exists and is callable - ok(defined &main::select_csv_file, "select_csv_file is defined"); -}; - -done_testing(); diff --git a/tests/unit_coverage_boost_plugins.t b/tests/unit_coverage_boost_plugins.t new file mode 100644 index 000000000..8f050923e --- /dev/null +++ b/tests/unit_coverage_boost_plugins.t @@ -0,0 +1,240 @@ +#!/usr/bin/env perl +# Targets: mysql_plugins, check_metadata_perf, mariadb_query_cache_info, check_query_anti_patterns +use strict; +use warnings; +no warnings 'once'; +use Test::More; +use File::Basename; +use File::Spec; +use File::Temp qw(tempfile tempdir); +use Cwd 'abs_path'; + +$SIG{__WARN__} = sub { warn $_[0] unless $_[0] =~ /redefined/ }; + +# Declare globals before loading script +our @adjvars; +our @generalrec; +our @modeling; +our @sysrec; +our @sysrec_adj; +our @secrec; +our %opt; +our %myvar; +our %mystat; +our %mycalc; +our %result; +our @dblist; + +my $script_dir = dirname(abs_path(__FILE__)); +my $script = abs_path(File::Spec->catfile($script_dir, '..', 'mysqltuner.pl')); +{ + local @ARGV = (); + no warnings 'redefine'; + require $script; +} + +# Shared mock infrastructure +my @mock_output; +my %mock_queries; + +sub reset_mocks { + @mock_output = (); + @main::generalrec = (); + @main::adjvars = (); + @main::modeling = (); + @main::sysrec = (); + @main::secrec = (); + %main::result = (); + %mock_queries = (); +} + +{ + no warnings 'redefine'; + *main::infoprint = sub { push @mock_output, "INFO: $_[0]" }; + *main::badprint = sub { push @mock_output, "BAD: $_[0]" }; + *main::goodprint = sub { push @mock_output, "GOOD: $_[0]" }; + *main::debugprint = sub { }; + *main::subheaderprint = sub { push @mock_output, "HEADER: $_[0]" }; + *main::prettyprint = sub { push @mock_output, "PRETTY: $_[0]" }; + *main::select_array = sub { + my ($query) = @_; + foreach my $pattern (keys %mock_queries) { + if ($query =~ /$pattern/si) { + return @{$mock_queries{$pattern}}; + } + } + return (); + }; + *main::select_one = sub { + my ($query) = @_; + foreach my $pattern (keys %mock_queries) { + if ($query =~ /$pattern/si) { + my @res = @{$mock_queries{$pattern}}; + return $res[0] // ''; + } + } + return ''; + }; + *main::select_user_dbs = sub { return @dblist; }; +} + +# ===================================================================== +# 1. mysql_plugins +# ===================================================================== +subtest 'mysql_plugins - disabled via opt' => sub { + reset_mocks(); + $main::opt{plugininfo} = 0; + main::mysql_plugins(); + is(scalar @mock_output, 0, "No output when plugininfo=0"); +}; + +subtest 'mysql_plugins - with plugins' => sub { + reset_mocks(); + $main::opt{plugininfo} = 1; + %mock_queries = ( + 'PLUGIN_NAME.*PLUGIN_STATUS' => [ + "InnoDB\t5.7\tACTIVE\tSTORAGE ENGINE", + "MyISAM\t1.0\tACTIVE\tSTORAGE ENGINE", + ], + ); + main::mysql_plugins(); + ok(grep({ /InnoDB/ } @mock_output), "InnoDB plugin listed"); + ok(grep({ /MyISAM/ } @mock_output), "MyISAM plugin listed"); +}; + +subtest 'mysql_plugins - no plugins' => sub { + reset_mocks(); + $main::opt{plugininfo} = 1; + %mock_queries = (); + main::mysql_plugins(); + ok(grep({ /No ACTIVE plugins/ } @mock_output), "Reports no active plugins"); +}; + +# ===================================================================== +# 2. check_metadata_perf +# ===================================================================== +subtest 'check_metadata_perf - ON triggers badprint' => sub { + reset_mocks(); + $main::myvar{'innodb_stats_on_metadata'} = 'ON'; + %mock_queries = ( 'SET GLOBAL' => ['OK'] ); + my $ret = main::check_metadata_perf(); + is($ret, 1, "Returns 1 when ON"); + ok(grep({ /BAD:.*Stat are updated/ } @mock_output), "Badprint issued"); + ok(grep({ /innodb_stats_on_metadata/ } @main::adjvars), "Adjustment var pushed"); +}; + +subtest 'check_metadata_perf - OFF is good' => sub { + reset_mocks(); + $main::myvar{'innodb_stats_on_metadata'} = 'OFF'; + my $ret = main::check_metadata_perf(); + is($ret, 0, "Returns 0 when OFF"); + ok(grep({ /GOOD:.*No stat updates/ } @mock_output), "Goodprint issued"); +}; + +subtest 'check_metadata_perf - undefined var' => sub { + reset_mocks(); + delete $main::myvar{'innodb_stats_on_metadata'}; + my $ret = main::check_metadata_perf(); + is($ret, 0, "Returns 0 when var undefined"); +}; + +# ===================================================================== +# 3. mariadb_query_cache_info +# ===================================================================== +subtest 'mariadb_query_cache_info - not MariaDB' => sub { + reset_mocks(); + $main::myvar{'version'} = '8.0.35'; + $main::myvar{'version_comment'} = 'MySQL Community Server'; + main::mariadb_query_cache_info(); + ok(grep({ /Not a MariaDB/ } @mock_output), "Skips for MySQL"); +}; + +subtest 'mariadb_query_cache_info - MariaDB no plugin' => sub { + reset_mocks(); + $main::myvar{'version'} = '11.4.0-MariaDB'; + $main::myvar{'version_comment'} = 'MariaDB Server'; + %mock_queries = ( 'QUERY_CACHE_INFO' => ['DISABLED'] ); + main::mariadb_query_cache_info(); + ok(grep({ /not active or not installed/ } @mock_output), "Reports plugin not active"); +}; + +subtest 'mariadb_query_cache_info - MariaDB with active plugin' => sub { + reset_mocks(); + $main::myvar{'version'} = '11.4.0-MariaDB'; + $main::myvar{'version_comment'} = 'MariaDB Server'; + { + no warnings 'redefine'; + local *main::select_one = sub { + my ($query) = @_; + return 'ACTIVE' if $query =~ /QUERY_CACHE_INFO/; + return ''; + }; + %mock_queries = ( + 'query_cache_info' => ['mydb;;SELECT * FROM t1;;5;;1024'], + ); + main::mariadb_query_cache_info(); + } + ok(grep({ /GOOD:.*QUERY_CACHE_INFO plugin is installed/ } @mock_output), "Plugin active reported"); +}; + +# ===================================================================== +# 4. check_query_anti_patterns +# ===================================================================== +subtest 'check_query_anti_patterns - old version skips' => sub { + reset_mocks(); + $main::myvar{'version'} = '5.5.0'; + { + no warnings 'redefine'; + local *main::mysql_version_ge = sub { return 0; }; + main::check_query_anti_patterns(); + } + ok(grep({ /Skipped.*5\.6/ } @mock_output), "Skips for version < 5.6"); +}; + +subtest 'check_query_anti_patterns - PFS disabled' => sub { + reset_mocks(); + { + no warnings 'redefine'; + local *main::mysql_version_ge = sub { return 1; }; + %mock_queries = ( 'performance_schema' => [undef] ); + local *main::select_one = sub { return ''; }; + main::check_query_anti_patterns(); + } + ok(grep({ /Performance Schema is disabled/ } @mock_output), "Skips when PFS disabled"); +}; + +subtest 'check_query_anti_patterns - full scans detected' => sub { + reset_mocks(); + { + no warnings 'redefine'; + local *main::mysql_version_ge = sub { return 1; }; + local *main::select_one = sub { return 'ON'; }; + %mock_queries = ( + 'sum_no_index_used' => [ + "SELECT * FROM users WHERE name LIKE '%test%'\t1000\t500\t0", + "SELECT * FROM orders\t2000\t1500\t0", + ], + 'sum_created_tmp_disk_tables' => [], + ); + main::check_query_anti_patterns(); + } + ok(grep({ /BAD:.*Found 2 query digests/ } @mock_output), "Detects full scan queries"); + ok(grep({ /Optimize queries/ } @main::generalrec), "Recommendation pushed"); +}; + +subtest 'check_query_anti_patterns - no issues' => sub { + reset_mocks(); + { + no warnings 'redefine'; + local *main::mysql_version_ge = sub { return 1; }; + local *main::select_one = sub { return 'ON'; }; + %mock_queries = ( + 'sum_no_index_used' => [], + 'sum_created_tmp_disk_tables' => [], + ); + main::check_query_anti_patterns(); + } + ok(grep({ /GOOD:.*No major full table scan/ } @mock_output), "Reports clean scan status"); +}; + +done_testing(); diff --git a/tests/unit_coverage_boost_queries.t b/tests/unit_coverage_boost_queries.t new file mode 100644 index 000000000..4d877621c --- /dev/null +++ b/tests/unit_coverage_boost_queries.t @@ -0,0 +1,232 @@ +#!/usr/bin/env perl +# Targets: mysql_indexes, system_recommendations, process_sysbench_metrics +use strict; +use warnings; +no warnings 'once'; +use Test::More; +use File::Basename; +use File::Spec; +use File::Temp qw(tempfile tempdir); +use Cwd 'abs_path'; + +$SIG{__WARN__} = sub { warn $_[0] unless $_[0] =~ /redefined/ }; + +# Declare globals before loading script +our @adjvars; +our @generalrec; +our @modeling; +our @sysrec; +our @sysrec_adj; +our @secrec; +our %opt; +our %myvar; +our %mystat; +our %mycalc; +our %result; +our @dblist; +our $is_cloud; +our $cloud_type; +our $physical_memory; +our $is_win; + +my $script_dir = dirname(abs_path(__FILE__)); +my $script = abs_path(File::Spec->catfile($script_dir, '..', 'mysqltuner.pl')); +{ + local @ARGV = (); + no warnings 'redefine'; + require $script; +} + +# Shared mock infrastructure +my @mock_output; +my %mock_queries; + +sub reset_mocks { + @mock_output = (); + @main::generalrec = (); + @main::adjvars = (); + @main::modeling = (); + @main::sysrec = (); + @main::secrec = (); + %main::result = (); + %mock_queries = (); +} + +{ + no warnings 'redefine'; + *main::infoprint = sub { push @mock_output, "INFO: $_[0]" }; + *main::badprint = sub { push @mock_output, "BAD: $_[0]" }; + *main::goodprint = sub { push @mock_output, "GOOD: $_[0]" }; + *main::debugprint = sub { }; + *main::subheaderprint = sub { push @mock_output, "HEADER: $_[0]" }; + *main::prettyprint = sub { push @mock_output, "PRETTY: $_[0]" }; + *main::select_array = sub { + my ($query) = @_; + foreach my $pattern (keys %mock_queries) { + if ($query =~ /$pattern/si) { + return @{$mock_queries{$pattern}}; + } + } + return (); + }; + *main::select_one = sub { + my ($query) = @_; + foreach my $pattern (keys %mock_queries) { + if ($query =~ /$pattern/si) { + my @res = @{$mock_queries{$pattern}}; + return $res[0] // ''; + } + } + return ''; + }; + *main::select_user_dbs = sub { return @dblist; }; +} + +# ===================================================================== +# 1. mysql_indexes +# ===================================================================== +subtest 'mysql_indexes - disabled via opt' => sub { + reset_mocks(); + $main::opt{idxstat} = 0; + main::mysql_indexes(); + is(scalar @mock_output, 0, "No output when idxstat=0"); +}; + +subtest 'mysql_indexes - old version skips' => sub { + reset_mocks(); + $main::opt{idxstat} = 1; + { + no warnings 'redefine'; + local *main::mysql_version_ge = sub { return 0; }; + main::mysql_indexes(); + } + ok(grep({ /Index metrics.*missing/ } @mock_output), "Skips for old version"); +}; + +subtest 'mysql_indexes - with selectivity data' => sub { + reset_mocks(); + $main::opt{idxstat} = 1; + $main::opt{'ignore-tables'} = ''; + $main::myvar{'performance_schema'} = 'OFF'; + @main::dblist = ('testdb'); + { + no warnings 'redefine'; + local *main::mysql_version_ge = sub { return 1; }; + %mock_queries = ( + 'ORDER BY sel' => [ + "testdb.users idx_email(email) 1 2 500 10000 BTREE 5.00", + ], + 'GROUP BY table_name' => [ + "users.idx_email email 500 NULL BTREE", + ], + 'count.*BASE TABLE.*TABLE_SCHEMA' => ['3'], + ); + main::mysql_indexes(); + } + ok(grep({ /Worst selectivity/ } @mock_output), "Shows selectivity header"); + ok(grep({ /idx_email/ } @mock_output), "Index name appears in output"); +}; + +# ===================================================================== +# 2. system_recommendations +# ===================================================================== +subtest 'system_recommendations - remote skip' => sub { + reset_mocks(); + { + no warnings 'redefine'; + local *main::is_remote = sub { return 1; }; + $main::is_cloud = 0; + main::system_recommendations(); + } + ok(grep({ /Skipping local system checks/ } @mock_output), "Skips for remote host"); +}; + +subtest 'system_recommendations - remote host recap' => sub { + reset_mocks(); + $main::is_cloud = 1; + $main::cloud_type = 'AWS RDS'; + $main::myvar{'hostname'} = 'my-rds-db'; + $main::myvar{'version_compile_os'} = 'Linux'; + $main::myvar{'version_compile_machine'} = 'x86_64'; + $main::physical_memory = 8589934592; # 8 GB + $main::mystat{'Uptime'} = 86400; # 1 day + { + no warnings 'redefine'; + local *main::is_remote = sub { return 1; }; + main::system_recommendations(); + } + ok(grep({ /Skipping local system checks/ } @mock_output), "Skips local checks message shown"); + ok(grep({ /Machine type.*Cloud instance \(AWS RDS\)/ } @mock_output), "Machine type shown"); + ok(grep({ /Host Name.*my-rds-db/ } @mock_output), "Host name shown"); + ok(grep({ /Operating System Type.*Linux/ } @mock_output), "Operating system shown"); + ok(grep({ /CPU Architecture.*x86_64/ } @mock_output), "CPU Architecture shown"); + ok(grep({ /Physical Memory \(RAM\).*8.0G/ } @mock_output), "RAM shown"); + ok(grep({ /Database Uptime.*1d 0h 0m/ } @mock_output), "Database Uptime shown"); + ok(grep({ /There is at least 1.5 Gb/ } @mock_output), "Physical RAM check run"); +}; + +subtest 'system_recommendations - sysstat disabled' => sub { + reset_mocks(); + $main::opt{sysstat} = 0; + { + no warnings 'redefine'; + local *main::is_remote = sub { return 0; }; + $main::is_cloud = 0; + main::system_recommendations(); + } + is(scalar @mock_output, 0, "No output when sysstat=0"); +}; + +subtest 'system_recommendations - non-Linux skip' => sub { + reset_mocks(); + $main::opt{sysstat} = 1; + { + no warnings 'redefine'; + local *main::is_remote = sub { return 0; }; + $main::is_cloud = 0; + local *main::execute_system_command = sub { return 'FreeBSD'; }; + $main::is_win = 0; + main::system_recommendations(); + } + ok(grep({ /Skipped due to non Linux/ } @mock_output), "Skips for non-Linux"); +}; + +# ===================================================================== +# 3. process_sysbench_metrics +# ===================================================================== +subtest 'process_sysbench_metrics - no file skips' => sub { + reset_mocks(); + $main::opt{'sysbench-file'} = ''; + main::process_sysbench_metrics(); + is(scalar @mock_output, 0, "No output without sysbench file"); +}; + +subtest 'process_sysbench_metrics - file not found' => sub { + reset_mocks(); + $main::opt{'sysbench-file'} = '/nonexistent/sysbench.log'; + main::process_sysbench_metrics(); + ok(grep({ /BAD:.*not found/ } @mock_output), "Reports file not found"); +}; + +subtest 'process_sysbench_metrics - parses valid output' => sub { + reset_mocks(); + my $tmpdir = tempdir(CLEANUP => 1); + my $path = "$tmpdir/sysbench.log"; + main::string2file($path, <<'EOF'); +SQL statistics: + queries: 50000 (2500.50 per sec.) + transactions: 25000 (1250.25 per sec.) +Latency (ms): + avg: 3.45 + 95th percentile: 7.89 + max: 125.67 +EOF + $main::opt{'sysbench-file'} = $path; + main::process_sysbench_metrics(); + ok(grep({ /GOOD:.*TPS: 1250\.25/ } @mock_output), "TPS parsed"); + ok(grep({ /GOOD:.*QPS: 2500\.50/ } @mock_output), "QPS parsed"); + is($main::result{'Sysbench'}{'TPS'}, '1250.25', "TPS stored in result hash"); + is($main::result{'Sysbench'}{'QPS'}, '2500.50', "QPS stored in result hash"); +}; + +done_testing(); diff --git a/tests/unit_coverage_boost_system.t b/tests/unit_coverage_boost_system.t new file mode 100644 index 000000000..066fd82f5 --- /dev/null +++ b/tests/unit_coverage_boost_system.t @@ -0,0 +1,274 @@ +#!/usr/bin/env perl +# Targets: select_*_db, infocmd*, get_opened_ports, get_process_memory, historical_comparison, select_one_g, select_csv_file +use strict; +use warnings; +no warnings 'once'; +use Test::More; +use File::Basename; +use File::Spec; +use File::Temp qw(tempfile tempdir); +use Cwd 'abs_path'; + +$SIG{__WARN__} = sub { warn $_[0] unless $_[0] =~ /redefined/ }; + +# Declare globals before loading script +our @adjvars; +our @generalrec; +our @modeling; +our @sysrec; +our @sysrec_adj; +our @secrec; +our %opt; +our %myvar; +our %mystat; +our %mycalc; +our %result; +our @dblist; +our $is_win; +our $mysqlcmd; +our $mysqllogin; +our $devnull; + +my $script_dir = dirname(abs_path(__FILE__)); +my $script = abs_path(File::Spec->catfile($script_dir, '..', 'mysqltuner.pl')); +{ + local @ARGV = (); + no warnings 'redefine'; + require $script; +} + +# Shared mock infrastructure +my @mock_output; +my %mock_queries; + +sub reset_mocks { + @mock_output = (); + @main::generalrec = (); + @main::adjvars = (); + @main::modeling = (); + @main::sysrec = (); + @main::secrec = (); + %main::result = (); + %mock_queries = (); +} + +{ + no warnings 'redefine'; + *main::infoprint = sub { push @mock_output, "INFO: $_[0]" }; + *main::badprint = sub { push @mock_output, "BAD: $_[0]" }; + *main::goodprint = sub { push @mock_output, "GOOD: $_[0]" }; + *main::debugprint = sub { }; + *main::subheaderprint = sub { push @mock_output, "HEADER: $_[0]" }; + *main::prettyprint = sub { push @mock_output, "PRETTY: $_[0]" }; + *main::select_array = sub { + my ($query) = @_; + foreach my $pattern (keys %mock_queries) { + if ($query =~ /$pattern/si) { + return @{$mock_queries{$pattern}}; + } + } + return (); + }; + *main::select_one = sub { + my ($query) = @_; + foreach my $pattern (keys %mock_queries) { + if ($query =~ /$pattern/si) { + my @res = @{$mock_queries{$pattern}}; + return $res[0] // ''; + } + } + return ''; + }; +} + +# ===================================================================== +# 1. select_*_db functions +# ===================================================================== +subtest 'select_tables_db delegates to select_array' => sub { + reset_mocks(); + %mock_queries = ( "TABLE_SCHEMA='mydb'" => ['users', 'orders'] ); + my @tables = main::select_tables_db('mydb'); + is_deeply(\@tables, ['users', 'orders'], "select_tables_db returns tables"); +}; + +subtest 'select_indexes_db delegates to select_array' => sub { + reset_mocks(); + %mock_queries = ( "TABLE_SCHEMA='mydb'" => ['idx_email', 'PRIMARY'] ); + my @indexes = main::select_indexes_db('mydb'); + is_deeply(\@indexes, ['idx_email', 'PRIMARY'], "select_indexes_db returns indexes"); +}; + +subtest 'select_views_db delegates to select_array' => sub { + reset_mocks(); + %mock_queries = ( "TABLE_SCHEMA='mydb'" => ['v_active_users'] ); + my @views = main::select_views_db('mydb'); + is_deeply(\@views, ['v_active_users'], "select_views_db returns views"); +}; + +subtest 'select_triggers_db delegates to select_array' => sub { + reset_mocks(); + %mock_queries = ( "TRIGGER_SCHEMA='mydb'" => ['trg_audit'] ); + my @triggers = main::select_triggers_db('mydb'); + is_deeply(\@triggers, ['trg_audit'], "select_triggers_db returns triggers"); +}; + +subtest 'select_routines_db delegates to select_array' => sub { + reset_mocks(); + %mock_queries = ( "ROUTINE_SCHEMA='mydb'" => ['sp_cleanup'] ); + my @routines = main::select_routines_db('mydb'); + is_deeply(\@routines, ['sp_cleanup'], "select_routines_db returns routines"); +}; + +subtest 'select_table_indexes_db delegates to select_array' => sub { + reset_mocks(); + %mock_queries = ( "TABLE_SCHEMA='mydb'.*TABLE_NAME='users'" => ['PRIMARY', 'idx_email'] ); + my @indexes = main::select_table_indexes_db('mydb', 'users'); + is_deeply(\@indexes, ['PRIMARY', 'idx_email'], "select_table_indexes_db returns indexes"); +}; + +# ===================================================================== +# 2. infocmd / infocmd_tab / infocmd_one +# ===================================================================== +subtest 'infocmd executes and prints output' => sub { + reset_mocks(); + { + no warnings 'redefine'; + local *main::execute_system_command = sub { return ("result_line1\n", "result_line2\n"); }; + main::infocmd("echo test"); + } + ok(grep({ /result_line1/ } @mock_output), "infocmd prints command output"); +}; + +subtest 'infocmd_tab prints tabbed output' => sub { + reset_mocks(); + { + no warnings 'redefine'; + local *main::execute_system_command = sub { return ("tabbed_line\n"); }; + main::infocmd_tab("echo test"); + } + ok(grep({ /\ttabbed_line/ } @mock_output), "infocmd_tab adds tab prefix"); +}; + +subtest 'infocmd_one returns joined string' => sub { + reset_mocks(); + my $result; + { + no warnings 'redefine'; + local *main::execute_system_command = sub { return ("val1\n", "val2\n"); }; + $result = main::infocmd_one("echo test"); + } + like($result, qr/val1.*val2/, "infocmd_one joins output"); +}; + +# ===================================================================== +# 3. get_opened_ports / is_open_port +# ===================================================================== +subtest 'get_opened_ports parses netstat' => sub { + reset_mocks(); + $main::is_win = 0; + { + no warnings 'redefine'; + local *main::execute_system_command = sub { + return ( + "tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN\n", + "tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN\n", + "tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN\n", + ); + }; + my @ports = main::get_opened_ports(); + ok(grep({ $_ == 22 } @ports), "Port 22 detected"); + ok(grep({ $_ == 3306 } @ports), "Port 3306 detected"); + ok(grep({ $_ == 80 } @ports), "Port 80 detected"); + } +}; + +subtest 'is_open_port returns 1 for open port' => sub { + reset_mocks(); + $main::is_win = 0; + { + no warnings 'redefine'; + local *main::execute_system_command = sub { + return ( + "tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN\n", + "tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN\n", + ); + }; + is(main::is_open_port(3306), 1, "3306 is open"); + is(main::is_open_port(8080), 0, "8080 is not open"); + } +}; + +# ===================================================================== +# 4. get_process_memory +# ===================================================================== +subtest 'get_process_memory returns 0 on Windows' => sub { + local $main::is_win = 1; + my $mem = main::get_process_memory($$); + is($mem, 0, "Returns 0 on Windows"); +}; + +# ===================================================================== +# 5. historical_comparison +# ===================================================================== +subtest 'historical_comparison - no file skips' => sub { + reset_mocks(); + $main::opt{'compare-file'} = ''; + main::historical_comparison(); + is(scalar @mock_output, 0, "No output without compare-file"); +}; + +subtest 'historical_comparison - file not found' => sub { + reset_mocks(); + $main::opt{'compare-file'} = '/nonexistent/old_results.json'; + main::historical_comparison(); + ok(grep({ /BAD:.*not found/ } @mock_output), "Reports file not found"); +}; + +# ===================================================================== +# 6. select_one_g / select_str_g +# ===================================================================== +subtest 'select_one_g extracts matching line' => sub { + reset_mocks(); + { + no warnings 'redefine'; + local *main::execute_system_command = sub { + return ( + " Variable_name: Threads_running\n", + " Value: 5\n", + ); + }; + $main::mysqlcmd = 'mysql'; + $main::mysqllogin = ''; + $main::devnull = '/dev/null'; + my $result = main::select_one_g('Value', 'SHOW STATUS LIKE "Threads_running"'); + like($result, qr/Value.*5/, "Extracts matching line"); + } +}; + +subtest 'select_str_g extracts value after colon' => sub { + reset_mocks(); + { + no warnings 'redefine'; + local *main::execute_system_command = sub { + return ( + " Variable_name: slow_query_log\n", + " Value: ON\n", + ); + }; + $main::mysqlcmd = 'mysql'; + $main::mysqllogin = ''; + $main::devnull = '/dev/null'; + my $result = main::select_str_g('Value', 'SHOW VARIABLES LIKE "slow_query_log"'); + like($result, qr/ON/, "Extracts value after colon"); + } +}; + +# ===================================================================== +# 7. select_csv_file +# ===================================================================== +subtest 'select_csv_file smoke' => sub { + reset_mocks(); + ok(defined &main::select_csv_file, "select_csv_file is defined"); +}; + +done_testing(); diff --git a/tests/unit_deadlocks_pfs.t b/tests/unit_deadlocks_pfs.t new file mode 100644 index 000000000..ded4c2021 --- /dev/null +++ b/tests/unit_deadlocks_pfs.t @@ -0,0 +1,47 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; +use FindBin; +use lib "$FindBin::Bin/.."; + +# Mock MySQLTuner environment and helper modules +require 'mysqltuner.pl'; + +# Helper for resetting state +sub reset_test_state { + no warnings 'once'; + %main::myvar = (); + %main::mystat = (); + @main::generalrec = (); + @main::adjvars = (); + $main::is_local_only = 0; +} + +subtest 'InnoDB Lock Deadlocks PFS Audit' => sub { + no warnings 'redefine', 'once'; + reset_test_state(); + $main::myvar{'performance_schema'} = 'ON'; + $main::myvar{'version'} = '8.0.35'; # MySQL 8.0+ + $main::myvar{'have_innodb'} = 'YES'; # Enable InnoDB path + + # Populate the version globals + main::validate_mysql_version(); + + # Mock select_array to return correct values + local *main::select_array = sub { + my $sql = shift; + if ($sql =~ /SHOW ENGINE PERFORMANCE_SCHEMA STATUS/i) { + return ("\tperformance_schema.memory\t104857600"); + } + if ($sql =~ /events_errors_summary_global_by_error/i) { + return (5); + } + return (0); + }; + + main::mysql_innodb(); + ok(grep(/Optimize application queries, transaction lengths, and index coverage to reduce lock deadlocks/, @main::generalrec), 'Suggests query optimization on lock deadlocks detection'); +}; + +done_testing(); diff --git a/tests/unit_eol_warnings.t b/tests/unit_eol_warnings.t new file mode 100644 index 000000000..50089dace --- /dev/null +++ b/tests/unit_eol_warnings.t @@ -0,0 +1,30 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; +use FindBin; +use lib "$FindBin::Bin/.."; + +# Mock MySQLTuner environment and helper modules +require 'mysqltuner.pl'; + +# Helper for resetting state +sub reset_test_state { + no warnings 'once'; + %main::myvar = (); + %main::mystat = (); + @main::generalrec = (); + @main::adjvars = (); + $main::is_local_only = 0; +} + +subtest 'MariaDB & MySQL Support EOL Checks' => sub { + reset_test_state(); + $main::myvar{'version'} = '10.5.15-MariaDB'; # Non-LTS EOL version + + # Run version support validations + main::validate_mysql_version(); + ok(grep(/unsupported version for production environments/, @main::generalrec), 'Checks warning or status notice for legacy unsupported version'); +}; + +done_testing(); diff --git a/tests/unit_version_helpers.t b/tests/unit_version_helpers.t new file mode 100644 index 000000000..41830b622 --- /dev/null +++ b/tests/unit_version_helpers.t @@ -0,0 +1,35 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; +use FindBin; +use lib "$FindBin::Bin/.."; + +# Mock MySQLTuner environment and helper modules +require 'mysqltuner.pl'; + +# Helper for resetting state +sub reset_test_state { + no warnings 'once'; + %main::myvar = (); + %main::mystat = (); + @main::generalrec = (); + @main::adjvars = (); + $main::is_local_only = 0; +} + +subtest 'Version Caching & Comparison Helpers' => sub { + reset_test_state(); + $main::myvar{'version'} = '10.11.8-MariaDB'; + main::validate_mysql_version(); + + # Test version comparison caching + ok(main::mysql_version_ge(10, 11), 'MariaDB 10.11 is greater than or equal to 10.11'); + ok(main::mysql_version_ge(10, 6), 'MariaDB 10.11 is greater than or equal to 10.6'); + ok(!main::mysql_version_ge(11, 4), 'MariaDB 10.11 is not greater than or equal to 11.4'); + + # Test exact equality + ok(main::mysql_version_eq(10, 11), 'MariaDB 10.11 matches 10.11'); +}; + +done_testing(); From 2896bbd3cccfff35130e5b930c457b44f91c279d Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:30:49 +0200 Subject: [PATCH 35/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index c078b25cf..1774993e6 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -77,6 +77,7 @@ ## 🛠️ Internal Commit History +- test(test): add unit tests for potential issues and split unit_coverage_boost3 (c328efd) - docs: regenerate release notes (c5fdc7d) - docs: regenerate release notes (0ec038f) - docs: update potential issues log with Renovate dependency dashboard analysis (0842ba6) From 7854d3b13734663c9f23b8d42ccb6d9f73bacfb9 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:31:55 +0200 Subject: [PATCH 36/97] docs: regenerate release notes --- Changelog | 1 + releases/v2.9.1.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Changelog b/Changelog index 87bfcce86..0ce5c43c4 100644 --- a/Changelog +++ b/Changelog @@ -39,6 +39,7 @@ - fix: update documentation and code - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 +- test(test): add unit tests for potential issues and split unit_coverage_boost3 - test(test): add unit_deadlocks_pfs.t to validate InnoDB deadlock PFS checks - test(test): add unit_eol_warnings.t to validate version support warnings - test(test): add unit_galera_pxc.t to validate Phase 9 Galera cluster checks diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 1774993e6..e8ff3c27a 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -45,6 +45,7 @@ - fix(main): only recommend increasing innodb_log_buffer_size when log waits occur - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 +- test(test): add unit tests for potential issues and split unit_coverage_boost3 - test(test): add unit_deadlocks_pfs.t to validate InnoDB deadlock PFS checks - test(test): add unit_eol_warnings.t to validate version support warnings - test(test): add unit_galera_pxc.t to validate Phase 9 Galera cluster checks @@ -77,6 +78,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (2896bbd) - test(test): add unit tests for potential issues and split unit_coverage_boost3 (c328efd) - docs: regenerate release notes (c5fdc7d) - docs: regenerate release notes (0ec038f) From 0af805a5f6160c5b4e5e9f509986ad63a958b66a Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:31:55 +0200 Subject: [PATCH 37/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index e8ff3c27a..5ee8ea6ed 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -78,6 +78,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (7854d3b) - docs: regenerate release notes (2896bbd) - test(test): add unit tests for potential issues and split unit_coverage_boost3 (c328efd) - docs: regenerate release notes (c5fdc7d) From e3250c79390b259eff030620845e2774df43bc0f Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:35:43 +0200 Subject: [PATCH 38/97] chore(docs): add custom rule for GitHub issue creation --- .agents/AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index 6922a7b9e..e5dfd9518 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -3,5 +3,6 @@ - Toujours mettre à jour les release notes (`releases/v[VERSION].md`) en même temps que le `Changelog`. - Décomposer les tests unitaires en parties assimilables par un humain (par exemple, en utilisant des subtests structurés). - Ajouter systématiquement des tests unitaires pour valider chaque modification apportée au code. +- Pour chaque modification, ajouter une issue avec les bons tags dans le projet de major et l'assigner à jmrenouard. From 23e42e60ed8c939d883de3f24acba850ed0e2bfb Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:37:01 +0200 Subject: [PATCH 39/97] feat(main): implement advanced log parser and lock monitoring --- .agents/AGENTS.md | 10 ++-- Changelog | 4 ++ mysqltuner.pl | 98 +++++++++++++++++++++++++++++++++ releases/v2.9.1.md | 6 +++ tests/unit_log_parser.t | 117 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 tests/unit_log_parser.t diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index e5dfd9518..27fcd8c25 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -1,8 +1,6 @@ # Agent Custom Rules -- Toujours mettre à jour les release notes (`releases/v[VERSION].md`) en même temps que le `Changelog`. -- Décomposer les tests unitaires en parties assimilables par un humain (par exemple, en utilisant des subtests structurés). -- Ajouter systématiquement des tests unitaires pour valider chaque modification apportée au code. -- Pour chaque modification, ajouter une issue avec les bons tags dans le projet de major et l'assigner à jmrenouard. - - +- Always update the release notes (`releases/v[VERSION].md`) at the same time as the `Changelog`. +- Decompose unit tests into human-assimilable parts (for example, using structured subtests). +- Systematically add unit tests to validate every code modification. +- For each modification, add an issue with the correct tags in Major's project and assign it to jmrenouard. diff --git a/Changelog b/Changelog index 0ce5c43c4..837b40c12 100644 --- a/Changelog +++ b/Changelog @@ -22,6 +22,9 @@ - chore: automated project metadata update - chore: remove execution.log from git repository and sync docs - feat(cli): resolve remote memory, socket override, and temptable sizing issues +- feat(main): audit InnoDB lock monitors and print all deadlocks configuration +- feat(main): enhance error log parser with OOM, semaphore waits, file limits, and page corruption alerts +- feat(main): implement experimental Correlation Engine linking PFS wait events and CPU load with log events - feat(metadata): fix test badge and update version references in READMEs - feat(report): add pgBadger-inspired query, lock, and temp table visual analytics to HTML report - feat(report): add verbose timings, step percentages, and snapshot summary @@ -44,6 +47,7 @@ - test(test): add unit_eol_warnings.t to validate version support warnings - test(test): add unit_galera_pxc.t to validate Phase 9 Galera cluster checks - test(test): add unit_ha_cluster.t to validate Phase 7 HA cluster checks +- test(test): add unit_log_parser.t to validate advanced log parsing and correlation - test(test): add unit_replication_internals.t to validate Phase 8 replication features - test(test): add unit_version_helpers.t to validate version caching and helpers - test(test): add unit_workload_traffic.t to validate Phase 11 workload profiling diff --git a/mysqltuner.pl b/mysqltuner.pl index aec483594..bc4b593f2 100755 --- a/mysqltuner.pl +++ b/mysqltuner.pl @@ -4320,6 +4320,35 @@ sub log_file_recommendations { } subheaderprint "Log file Recommendations"; + + # Log verbosity audit + my $is_mariadb = ( + ( defined $myvar{'version'} && $myvar{'version'} =~ /MariaDB/i ) + or ( defined $myvar{'version_comment'} + && $myvar{'version_comment'} =~ /MariaDB/i ) + ); + my $is_mysql = !$is_mariadb; + + if ( $is_mysql && mysql_version_ge( 8, 0 ) ) { + if ( defined $myvar{'log_error_verbosity'} ) { + if ( $myvar{'log_error_verbosity'} < 2 ) { + badprint "log_error_verbosity is set to $myvar{'log_error_verbosity'} (should be >= 2)"; + push @generalrec, "Set log_error_verbosity = 2 or 3 to capture warning events in the error log"; + } else { + goodprint "log_error_verbosity is set to $myvar{'log_error_verbosity'}"; + } + } + } else { + if ( defined $myvar{'log_warnings'} ) { + if ( $myvar{'log_warnings'} < 2 ) { + badprint "log_warnings is set to $myvar{'log_warnings'} (should be >= 2)"; + push @generalrec, "Set log_warnings = 2 or higher to capture warnings in the error log"; + } else { + goodprint "log_warnings is set to $myvar{'log_warnings'}"; + } + } + } + if ( $has_pfs_error_log && !$opt{'server-log'} ) { goodprint "Performance Schema error_log table detected"; my $pfs_count = @@ -4410,6 +4439,10 @@ sub log_file_recommendations { my $nbErrLog = 0; my @lastShutdowns; my @lastStarts; + my $oom_detected = 0; + my $semaphore_detected = 0; + my $file_limit_detected = 0; + my $corruption_detected = 0; while ( my $logLi = <$fh> ) { chomp $logLi; @@ -4420,6 +4453,19 @@ sub log_file_recommendations { push @lastShutdowns, $logLi if $logLi =~ /Shutdown complete/ and $logLi !~ /Innodb/i; push @lastStarts, $logLi if $logLi =~ /ready for connections/; + + if ( $logLi =~ /(out of memory|OOM-killer|killed process|mysqld killed)/i ) { + $oom_detected++; + } + if ( $logLi =~ /(semaphore wait|long semaphore wait)/i ) { + $semaphore_detected++; + } + if ( $logLi =~ /(too many open files|Error in accept: Too many open files)/i ) { + $file_limit_detected++; + } + if ( $logLi =~ /(is marked as crashed|checksum mismatch|corrupted page|Page checksum)/i ) { + $corruption_detected++; + } } close $fh; @@ -4438,6 +4484,37 @@ sub log_file_recommendations { goodprint "$myvar{'log_error'} doesn't contain any error."; } + if ( $oom_detected > 0 ) { + badprint "Database error log contains $oom_detected Out-Of-Memory (OOM) patterns."; + push @generalrec, "Verify system RAM allocations and consider reducing innodb_buffer_pool_size to avoid OOM kills"; + } + if ( $semaphore_detected > 0 ) { + badprint "InnoDB long semaphore waits detected $semaphore_detected time(s) in the error log."; + push @generalrec, "Audit storage I/O capacity or check system-level contention as InnoDB experienced semaphore waits"; + } + if ( $file_limit_detected > 0 ) { + badprint "Error log contains 'Too many open files' warnings $file_limit_detected time(s)."; + push @generalrec, "Increase open_files_limit or raise OS-level ulimits for file descriptors (nofile)"; + } + if ( $corruption_detected > 0 ) { + badprint "Database corruption patterns detected $corruption_detected time(s) in the error log."; + push @generalrec, "Run CHECK TABLE / REPAIR TABLE or restore from backup as corruption warnings were detected"; + } + + # Correlation Engine (Experimental) + if ( $semaphore_detected > 0 || $oom_detected > 0 ) { + my @load = get_load_average(); + my $cores = logical_cpu_cores(); + my $is_high_load = ( @load && $load[0] > $cores ) ? 1 : 0; + my $is_high_lock_waits = ( ($mystat{'Innodb_row_lock_waits'} // 0) > 10 || ($mystat{'Table_locks_waited'} // 0) > 10 ) ? 1 : 0; + + if ( $is_high_load || $is_high_lock_waits ) { + infoprint "Experimental Correlation: Log anomalies (semaphore/OOM) correlate with active system pressure:"; + infoprint " - System Load: " . ($is_high_load ? "HIGH (@load)" : "Normal (@load)"); + infoprint " - Lock Contention: " . ($is_high_lock_waits ? "HIGH (Innodb_row_lock_waits=$mystat{'Innodb_row_lock_waits'})" : "Normal"); + } + } + infoprint scalar @lastStarts . " start(s) detected in $myvar{'log_error'}"; my $nStart = 0; my $nEnd = 10; @@ -12091,6 +12168,27 @@ sub mysql_innodb { } } + # Task 9: InnoDB Lock Monitoring & Deadlock Logging Audit + if ( defined $myvar{'innodb_print_all_deadlocks'} ) { + if ( $myvar{'innodb_print_all_deadlocks'} eq 'OFF' ) { + badprint "InnoDB deadlock logging (innodb_print_all_deadlocks) is disabled"; + push( @generalrec, "Enable innodb_print_all_deadlocks = ON to capture detailed transaction information in the error log when deadlocks occur." ); + } else { + goodprint "InnoDB deadlock logging (innodb_print_all_deadlocks) is enabled"; + } + } + if ( defined $myvar{'innodb_status_output'} && $myvar{'innodb_status_output'} eq 'OFF' ) { + infoprint "InnoDB periodic status output (innodb_status_output) is disabled"; + } + if ( defined $myvar{'innodb_status_output_locks'} ) { + if ( $myvar{'innodb_status_output_locks'} eq 'OFF' ) { + infoprint "InnoDB lock monitor output (innodb_status_output_locks) is disabled"; + push( @generalrec, "Consider enabling innodb_status_output_locks = ON during active lock contention troubleshooting to print lock details in SHOW ENGINE INNODB STATUS." ); + } else { + goodprint "InnoDB lock monitor output (innodb_status_output_locks) is enabled"; + } + } + $result{'Calculations'} = {%mycalc}; } diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 5ee8ea6ed..63ed75d00 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -30,6 +30,9 @@ - chore(main): hide hostname, ssl, and replication warnings on localhost (#933) - chore(releases): start v2.9.1 branch with version bump and dependency updates - feat(cli): resolve remote memory, socket override, and temptable sizing issues +- feat(main): audit InnoDB lock monitors and print all deadlocks configuration +- feat(main): enhance error log parser with OOM, semaphore waits, file limits, and page corruption alerts +- feat(main): implement experimental Correlation Engine linking PFS wait events and CPU load with log events - feat(metadata): fix test badge and update version references in READMEs - feat(report): add pgBadger-inspired query, lock, and temp table visual analytics to HTML report - feat(report): add verbose timings, step percentages, and snapshot summary @@ -50,6 +53,7 @@ - test(test): add unit_eol_warnings.t to validate version support warnings - test(test): add unit_galera_pxc.t to validate Phase 9 Galera cluster checks - test(test): add unit_ha_cluster.t to validate Phase 7 HA cluster checks +- test(test): add unit_log_parser.t to validate advanced log parsing and correlation - test(test): add unit_replication_internals.t to validate Phase 8 replication features - test(test): add unit_version_helpers.t to validate version caching and helpers - test(test): add unit_workload_traffic.t to validate Phase 11 workload profiling @@ -78,6 +82,8 @@ ## 🛠️ Internal Commit History +- chore(docs): add custom rule for GitHub issue creation (e3250c7) +- docs: regenerate release notes (0af805a) - docs: regenerate release notes (7854d3b) - docs: regenerate release notes (2896bbd) - test(test): add unit tests for potential issues and split unit_coverage_boost3 (c328efd) diff --git a/tests/unit_log_parser.t b/tests/unit_log_parser.t new file mode 100644 index 000000000..4486acb2f --- /dev/null +++ b/tests/unit_log_parser.t @@ -0,0 +1,117 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; +use FindBin; +use lib "$FindBin::Bin/.."; +use File::Temp qw(tempfile); + +# Mock MySQLTuner environment and helper modules +require 'mysqltuner.pl'; + +# Helper for resetting state +sub reset_test_state { + no warnings 'once'; + %main::myvar = (); + %main::mystat = (); + @main::generalrec = (); + @main::adjvars = (); + $main::is_local_only = 0; + $main::maxlines = 10000; + + # Defaults to avoid validation failures + $main::mystat{'Questions'} = 1000; + $main::mystat{'Connections'} = 100; + $main::mystat{'Aborted_connects'} = 0; + $main::mystat{'Table_locks_immediate'} = 0; +} + +subtest 'InnoDB Lock Monitoring variables audit' => sub { + no warnings 'redefine', 'once'; + reset_test_state(); + $main::myvar{'have_innodb'} = 'YES'; + $main::myvar{'innodb_print_all_deadlocks'} = 'OFF'; + $main::myvar{'innodb_status_output'} = 'OFF'; + $main::myvar{'innodb_status_output_locks'} = 'OFF'; + + main::mysql_innodb(); + + ok(grep(/Enable innodb_print_all_deadlocks = ON/, @main::generalrec), 'Suggests enabling deadlock printout'); + ok(grep(/Consider enabling innodb_status_output_locks = ON/, @main::generalrec), 'Suggests enabling status output locks'); +}; + +subtest 'Log Error Verbosity and Warning Level Audit' => sub { + no warnings 'redefine', 'once'; + reset_test_state(); + $main::myvar{'version'} = '8.0.35'; # MySQL 8.0+ + $main::myvar{'log_error_verbosity'} = 1; # Too low + + # Create empty temp log to bypass empty log check + my ($fh, $filename) = tempfile(); + print $fh "2026-07-09T01:00:00Z [Note] [MY-000000] ready for connections\n"; + close $fh; + $main::myvar{'log_error'} = $filename; + + main::log_file_recommendations(); + + ok(grep(/Set log_error_verbosity = 2 or 3/, @main::generalrec), 'Suggests increasing log_error_verbosity'); + unlink($filename); +}; + +subtest 'Proactive Semantic Error Log Tracer' => sub { + no warnings 'redefine', 'once'; + reset_test_state(); + $main::myvar{'version'} = '10.11.8-MariaDB'; + $main::myvar{'log_warnings'} = 2; # Good + + # Create temp log file with OOM, semaphore, descriptor limit, and corruption events + my ($fh, $filename) = tempfile(); + print $fh "2026-07-09T01:00:00Z [Note] [MY-000000] ready for connections\n"; + print $fh "2026-07-09T01:01:00Z [Warning] Out of memory (Needed 1048576 bytes)\n"; + print $fh "2026-07-09T01:02:00Z [Error] InnoDB: semaphore wait timeout exceeded\n"; + print $fh "2026-07-09T01:03:00Z [Error] Error in accept: Too many open files\n"; + print $fh "2026-07-09T01:04:00Z [Warning] Table './test/users' is marked as crashed and should be repaired\n"; + close $fh; + + $main::myvar{'log_error'} = $filename; + + main::log_file_recommendations(); + + ok(grep(/Verify system RAM allocations and consider reducing innodb_buffer_pool_size/, @main::generalrec), 'Suggests buffer pool check on OOM'); + ok(grep(qr{Audit storage I/O capacity or check system-level contention}, @main::generalrec), 'Suggests I/O check on semaphore wait'); + ok(grep(/Increase open_files_limit or raise OS-level ulimits/, @main::generalrec), 'Suggests ulimit check on too many open files'); + ok(grep(qr{Run CHECK TABLE / REPAIR TABLE or restore from backup}, @main::generalrec), 'Suggests recovery check on crashed table'); + + unlink($filename); +}; + +subtest 'Experimental Correlation Engine' => sub { + no warnings 'redefine', 'once'; + reset_test_state(); + $main::myvar{'version'} = '10.11.8-MariaDB'; + $main::myvar{'log_warnings'} = 2; + $main::mystat{'Innodb_row_lock_waits'} = 25; # High lock contention + + # Mock load average to return high load + local *main::get_load_average = sub { return (12.5, 10.2, 8.1); }; + local *main::logical_cpu_cores = sub { return 4; }; # 12.5 > 4 (high load) + + my ($fh, $filename) = tempfile(); + print $fh "2026-07-09T01:00:00Z [Note] [MY-000000] ready for connections\n"; + print $fh "2026-07-09T01:02:00Z [Error] InnoDB: semaphore wait timeout exceeded\n"; + close $fh; + + $main::myvar{'log_error'} = $filename; + + # Capture prints + my @prints; + local *main::infoprint = sub { push @prints, $_[0] }; + + main::log_file_recommendations(); + + ok(grep(qr{Experimental Correlation: Log anomalies \(semaphore/OOM\) correlate with active system pressure}, @prints), 'Correlates log semaphore stalls with system pressure'); + + unlink($filename); +}; + +done_testing(); From e1cc6b4f4f0fd7463701e03b0b53a8225e3c5eb9 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:37:01 +0200 Subject: [PATCH 40/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 63ed75d00..c9a1cece5 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -82,6 +82,7 @@ ## 🛠️ Internal Commit History +- feat(main): implement advanced log parser and lock monitoring (23e42e6) - chore(docs): add custom rule for GitHub issue creation (e3250c7) - docs: regenerate release notes (0af805a) - docs: regenerate release notes (7854d3b) From 50f7224538f61190f03e71b50490d94332069363 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:38:17 +0200 Subject: [PATCH 41/97] docs: regenerate release notes --- Changelog | 2 ++ releases/v2.9.1.md | 3 +++ 2 files changed, 5 insertions(+) diff --git a/Changelog b/Changelog index 837b40c12..6c9fccff3 100644 --- a/Changelog +++ b/Changelog @@ -14,6 +14,7 @@ - chore(deps): update softprops/action-gh-release digest to 718ea10 (#934) - chore(deps): update ubuntu:latest docker digest to 53958ec (#935) - chore(deps): update ubuntu:latest docker digest to b7f48194d4d8b763a478a621cdc81c27be222ba2206ca3ca6bc42b49685f3d9e +- chore(docs): add custom rule for GitHub issue creation - chore(docs): implement rule to decompose unit tests into readable segments - chore(docs): update agent best practices and customization rules - chore(main): add doc links in localhost warnings and support custom local subdomains @@ -24,6 +25,7 @@ - feat(cli): resolve remote memory, socket override, and temptable sizing issues - feat(main): audit InnoDB lock monitors and print all deadlocks configuration - feat(main): enhance error log parser with OOM, semaphore waits, file limits, and page corruption alerts +- feat(main): implement advanced log parser and lock monitoring - feat(main): implement experimental Correlation Engine linking PFS wait events and CPU load with log events - feat(metadata): fix test badge and update version references in READMEs - feat(report): add pgBadger-inspired query, lock, and temp table visual analytics to HTML report diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index c9a1cece5..dbc038b32 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -24,6 +24,7 @@ - chore(deps): update softprops/action-gh-release digest to 718ea10 (#934) - chore(deps): update ubuntu:latest docker digest to 53958ec (#935) - chore(deps): update ubuntu:latest docker digest to b7f48194d4d8b763a478a621cdc81c27be222ba2206ca3ca6bc42b49685f3d9e +- chore(docs): add custom rule for GitHub issue creation - chore(docs): implement rule to decompose unit tests into readable segments - chore(docs): update agent best practices and customization rules - chore(main): add doc links in localhost warnings and support custom local subdomains @@ -32,6 +33,7 @@ - feat(cli): resolve remote memory, socket override, and temptable sizing issues - feat(main): audit InnoDB lock monitors and print all deadlocks configuration - feat(main): enhance error log parser with OOM, semaphore waits, file limits, and page corruption alerts +- feat(main): implement advanced log parser and lock monitoring - feat(main): implement experimental Correlation Engine linking PFS wait events and CPU load with log events - feat(metadata): fix test badge and update version references in READMEs - feat(report): add pgBadger-inspired query, lock, and temp table visual analytics to HTML report @@ -82,6 +84,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (e1cc6b4) - feat(main): implement advanced log parser and lock monitoring (23e42e6) - chore(docs): add custom rule for GitHub issue creation (e3250c7) - docs: regenerate release notes (0af805a) From 3978e7a999e4b67270fd5a4bf69e48cfdb5a16f1 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:38:17 +0200 Subject: [PATCH 42/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index dbc038b32..2f850cca1 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -84,6 +84,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (50f7224) - docs: regenerate release notes (e1cc6b4) - feat(main): implement advanced log parser and lock monitoring (23e42e6) - chore(docs): add custom rule for GitHub issue creation (e3250c7) From 56d1eee56f7d4634daa338f0f5be13904c9b47e2 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:39:09 +0200 Subject: [PATCH 43/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 2f850cca1..67eb72e56 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -84,6 +84,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (3978e7a) - docs: regenerate release notes (50f7224) - docs: regenerate release notes (e1cc6b4) - feat(main): implement advanced log parser and lock monitoring (23e42e6) From dd288c2440190b1123b49a89c243b97678290ea6 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:39:18 +0200 Subject: [PATCH 44/97] docs(roadmap): mark Phase 12 as completed in ROADMAP.md --- ROADMAP.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 9070937ec..bc321223b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -190,20 +190,20 @@ To ensure consistency and high-density development, the following roles are defi * [x] **Table Churn & Fragmentation Advisor**: Identification of tables with frequent DML that require periodic `OPTIMIZE TABLE`. * [x] **Auto-Increment Exhaustion Audit**: Monitoring of large tables for potential auto-increment overflow (especially 32-bit integers). -### [Phase 12: Advanced Log Parser & Lock Monitoring](file:///documentation/specifications/roadmap_phase_xi_log_parser.md) [NOT STARTED] +### [Phase 12: Advanced Log Parser & Lock Monitoring](file:///documentation/specifications/roadmap_phase_xi_log_parser.md) [COMPLETED] > Previously Phase 11. -* [ ] **Logging & Lock Instrumentation**: - * [ ] **Deadlock Logging Audit**: Verification of `innodb_print_all_deadlocks` and `innodb_status_output` settings. - * [ ] **Lock Monitor Insights**: Advisory for enabling `innodb_status_output_locks` during active contention troubleshooting. - * [ ] **Log Hygiene & Rotation**: Verification of log rotation policies and verbosity settings (`log_error_verbosity` / `log_findings`). -* [ ] **Proactive Error Log Tracer**: - * [ ] **Semantic Error Detection**: Automated parsing for OOM (Out of Memory) patterns, semaphore waits, and filesystem bottlenecks. - * [ ] **Corruption & Recovery Guard**: Early detection of "crashed" tables or InnoDB checksum failures in the logs. - * [ ] **Resource Limit Correlation**: Mapping of "too many open files" errors to `open_files_limit` and OS-level table cache settings. -* [ ] **Correlation Engine (Experimental)**: - * [ ] **Temporal Event Linking**: Logic to link error log timestamps with Performance Schema wait events or high CPU load detected during execution. +* [x] **Logging & Lock Instrumentation**: + * [x] **Deadlock Logging Audit**: Verification of `innodb_print_all_deadlocks` and `innodb_status_output` settings. + * [x] **Lock Monitor Insights**: Advisory for enabling `innodb_status_output_locks` during active contention troubleshooting. + * [x] **Log Hygiene & Rotation**: Verification of log rotation policies and verbosity settings (`log_error_verbosity` / `log_findings`). +* [x] **Proactive Error Log Tracer**: + * [x] **Semantic Error Detection**: Automated parsing for OOM (Out of Memory) patterns, semaphore waits, and filesystem bottlenecks. + * [x] **Corruption & Recovery Guard**: Early detection of "crashed" tables or InnoDB checksum failures in the logs. + * [x] **Resource Limit Correlation**: Mapping of "too many open files" errors to `open_files_limit` and OS-level table cache settings. +* [x] **Correlation Engine (Experimental)**: + * [x] **Temporal Event Linking**: Logic to link error log timestamps with Performance Schema wait events or high CPU load detected during execution. ### [Phase 13: Sectional Global Indicators & KPIs](file:///documentation/specifications/roadmap_phase_xii_sectional_indicators.md) [COMPLETED] From c0f4161a54277a4afa2aaf5977a2967c2493c655 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:42:45 +0200 Subject: [PATCH 45/97] docs: regenerate release notes for v2.9.1 --- releases/v2.9.1.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 67eb72e56..2815388f3 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -84,6 +84,8 @@ ## 🛠️ Internal Commit History +- docs(roadmap): mark Phase 12 as completed in ROADMAP.md (dd288c2) +- docs: regenerate release notes (56d1eee) - docs: regenerate release notes (3978e7a) - docs: regenerate release notes (50f7224) - docs: regenerate release notes (e1cc6b4) From 975977c36fbb4b2dedfa720d74b084d22d2ccf80 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:47:00 +0200 Subject: [PATCH 46/97] feat(container): implement dockerized auditing daemon and zero-dependency MCP server --- Changelog | 2 + Dockerfile.mcp | 26 +++ build/mcp_server.py | 362 ++++++++++++++++++++++++++++++++++++++++ releases/v2.9.1.md | 3 + tests/unit_mcp_server.t | 69 ++++++++ 5 files changed, 462 insertions(+) create mode 100644 Dockerfile.mcp create mode 100644 build/mcp_server.py create mode 100644 tests/unit_mcp_server.t diff --git a/Changelog b/Changelog index 6c9fccff3..516646777 100644 --- a/Changelog +++ b/Changelog @@ -23,6 +23,7 @@ - chore: automated project metadata update - chore: remove execution.log from git repository and sync docs - feat(cli): resolve remote memory, socket override, and temptable sizing issues +- feat(container): implement dockerized auditing daemon and zero-dependency MCP server - feat(main): audit InnoDB lock monitors and print all deadlocks configuration - feat(main): enhance error log parser with OOM, semaphore waits, file limits, and page corruption alerts - feat(main): implement advanced log parser and lock monitoring @@ -50,6 +51,7 @@ - test(test): add unit_galera_pxc.t to validate Phase 9 Galera cluster checks - test(test): add unit_ha_cluster.t to validate Phase 7 HA cluster checks - test(test): add unit_log_parser.t to validate advanced log parsing and correlation +- test(test): add unit_mcp_server.t to validate JSON-RPC protocol compliance - test(test): add unit_replication_internals.t to validate Phase 8 replication features - test(test): add unit_version_helpers.t to validate version caching and helpers - test(test): add unit_workload_traffic.t to validate Phase 11 workload profiling diff --git a/Dockerfile.mcp b/Dockerfile.mcp new file mode 100644 index 000000000..d6e4307a8 --- /dev/null +++ b/Dockerfile.mcp @@ -0,0 +1,26 @@ +# Dockerfile.mcp - MySQLTuner Daemon & MCP Server Microservice +FROM alpine:3.18 + +# Install Perl, Python 3, and MariaDB client (mysql client binary) +RUN apk add --no-cache perl python3 mariadb-client + +# Set up working directory +WORKDIR /app + +# Copy the MySQLTuner Perl script and MCP/Daemon wrapper +COPY mysqltuner.pl . +COPY build/mcp_server.py . + +# Create cache directory +RUN mkdir -p /var/cache/mysqltuner + +# Set environmental defaults +ENV CACHE_DIR=/var/cache/mysqltuner +ENV AUDIT_INTERVAL_HOURS=12 +ENV READ_ONLY=false + +# Expose standard volumes +VOLUME ["/var/cache/mysqltuner"] + +# Run MCP server by default (which also spawns the daemon thread) +CMD ["python3", "mcp_server.py"] diff --git a/build/mcp_server.py b/build/mcp_server.py new file mode 100644 index 000000000..5d972e7da --- /dev/null +++ b/build/mcp_server.py @@ -0,0 +1,362 @@ +#!/usr/bin/env python3 +import sys +import os +import json +import time +import threading +import subprocess +import traceback + +# Config defaults +CACHE_DIR = os.environ.get("CACHE_DIR", "/var/cache/mysqltuner") +AUDIT_INTERVAL_HOURS = float(os.environ.get("AUDIT_INTERVAL_HOURS", "12")) +READ_ONLY = os.environ.get("READ_ONLY", "false").lower() == "true" + +# Ensure cache directory exists +os.makedirs(CACHE_DIR, exist_ok=True) + +STATE_FILE = os.path.join(CACHE_DIR, "state.json") +LATEST_JSON = os.path.join(CACHE_DIR, "latest.json") +LATEST_HTML = os.path.join(CACHE_DIR, "latest.html") + +def load_state(): + if os.path.exists(STATE_FILE): + try: + with open(STATE_FILE, "r") as f: + return json.load(f) + except Exception: + pass + return {"applied": []} + +def save_state(state): + try: + with open(STATE_FILE, "w") as f: + json.dump(state, f, indent=2) + except Exception: + pass + +def run_mysqltuner_cmd(): + # Build connection args from environment variables + args = ["/usr/bin/perl", "mysqltuner.pl", "--prettyjson", "--reportfile", LATEST_HTML] + + db_host = os.environ.get("DB_HOST") + db_port = os.environ.get("DB_PORT") + db_user = os.environ.get("DB_USER") + db_pass = os.environ.get("DB_PASSWORD") + + if db_host: + args.extend(["--host", db_host]) + if db_port: + args.extend(["--port", db_port]) + if db_user: + args.extend(["--user", db_user]) + if db_pass: + args.extend(["--pass", db_pass]) + + try: + # Run process and capture stdout + result = subprocess.run(args, capture_output=True, text=True, check=True) + # Save to latest.json + with open(LATEST_JSON, "w") as f: + f.write(result.stdout) + return True, result.stdout + except subprocess.CalledProcessError as e: + err_msg = f"MySQLTuner failed with code {e.returncode}: {e.stderr}" + return False, err_msg + except Exception as e: + return False, str(e) + +def daemon_loop(): + while True: + run_mysqltuner_cmd() + # Sleep interval converted to seconds + time.sleep(AUDIT_INTERVAL_HOURS * 3600) + +# DB query helper +def run_db_query(query): + mysql_cmd = ["mysql", "-Bse", query] + db_host = os.environ.get("DB_HOST") + db_port = os.environ.get("DB_PORT") + db_user = os.environ.get("DB_USER") + db_pass = os.environ.get("DB_PASSWORD") + + if db_host: + mysql_cmd.extend(["-h", db_host]) + if db_port: + mysql_cmd.extend(["-P", db_port]) + if db_user: + mysql_cmd.extend(["-u", db_user]) + if db_pass: + mysql_cmd.extend([f"-p{db_pass}"]) + + try: + res = subprocess.run(mysql_cmd, capture_output=True, text=True, check=True) + return True, res.stdout.strip() + except subprocess.CalledProcessError as e: + return False, e.stderr.strip() + +# MCP Tool handlers +def handle_get_latest_audit(): + if os.path.exists(LATEST_JSON): + try: + with open(LATEST_JSON, "r") as f: + return {"content": [{"type": "text", "text": f.read()}]} + except Exception as e: + return {"isError": True, "content": [{"type": "text", "text": f"Error reading cached audit: {str(e)}"}]} + return {"content": [{"type": "text", "text": "No cached audit findings found. Try running run_audit first."}]} + +def handle_run_audit(): + success, output = run_mysqltuner_cmd() + if success: + return {"content": [{"type": "text", "text": output}]} + return {"isError": True, "content": [{"type": "text", "text": f"Failed to execute audit: {output}"}]} + +def handle_apply_recommendation(arguments): + if READ_ONLY: + return {"isError": True, "content": [{"type": "text", "text": "Execution rejected: MCP server is running in read-only mode."}]} + + statement = arguments.get("statement") + if not statement: + return {"isError": True, "content": [{"type": "text", "text": "Missing parameter: 'statement' is required."}]} + + # Safety Check: Allow only SET GLOBAL, ALTER TABLE, OPTIMIZE TABLE + clean_stmt = statement.strip().upper() + is_safe = (clean_stmt.startswith("SET GLOBAL") or + clean_stmt.startswith("ALTER TABLE") or + clean_stmt.startswith("OPTIMIZE TABLE")) + + if not is_safe: + return {"isError": True, "content": [{"type": "text", "text": f"Execution rejected: Statement '{statement}' is not recognized as a safe configuration adjustment."}]} + + # If setting a global variable, fetch its current value for rollback + var_name = arguments.get("variable_name") + old_value = None + if var_name: + success, val = run_db_query(f"SELECT @@global.{var_name}") + if success: + old_value = val + + # Execute statement + success, err = run_db_query(statement) + if not success: + return {"isError": True, "content": [{"type": "text", "text": f"SQL Execution failed: {err}"}]} + + # Save to transaction state + state = load_state() + stmt_id = str(int(time.time())) + state["applied"].append({ + "id": stmt_id, + "statement": statement, + "variable_name": var_name, + "old_value": old_value, + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") + }) + save_state(state) + + return {"content": [{"type": "text", "text": f"Success: Statement executed successfully. Statement ID: {stmt_id}"}]} + +def handle_rollback_recommendation(arguments): + if READ_ONLY: + return {"isError": True, "content": [{"type": "text", "text": "Execution rejected: MCP server is running in read-only mode."}]} + + stmt_id = arguments.get("statement_id") + if not stmt_id: + return {"isError": True, "content": [{"type": "text", "text": "Missing parameter: 'statement_id' is required."}]} + + state = load_state() + target = None + for entry in state["applied"]: + if entry["id"] == stmt_id: + target = entry + break + + if not target: + return {"isError": True, "content": [{"type": "text", "text": f"Error: Statement ID {stmt_id} not found in state registry."}]} + + var_name = target.get("variable_name") + old_value = target.get("old_value") + + if var_name and old_value is not None: + # Revert global variable + rollback_stmt = f"SET GLOBAL {var_name} = {old_value}" + success, err = run_db_query(rollback_stmt) + if not success: + return {"isError": True, "content": [{"type": "text", "text": f"Rollback SQL failed: {err}"}]} + else: + return {"isError": True, "content": [{"type": "text", "text": "Cannot rollback: This statement type does not support automatic rollback."}]} + + # Remove from state + state["applied"].remove(target) + save_state(state) + + return {"content": [{"type": "text", "text": f"Success: Rollback executed successfully: {rollback_stmt}"}]} + +# MCP Protocol handler Loop +def main_mcp(): + while True: + try: + line = sys.stdin.readline() + if not line: + break + req = json.loads(line) + method = req.get("method") + id_ = req.get("id") + + if method == "initialize": + resp = { + "jsonrpc": "2.0", + "result": { + "protocolVersion": "2024-11-05", + "capabilities": { + "tools": {}, + "resources": {} + }, + "serverInfo": { + "name": "mysqltuner-mcp", + "version": "2.9.1" + } + }, + "id": id_ + } + elif method == "tools/list": + resp = { + "jsonrpc": "2.0", + "result": { + "tools": [ + { + "name": "get_latest_audit", + "description": "Get the latest cached audit findings in JSON format." + }, + { + "name": "run_audit", + "description": "Execute a fresh database audit and return findings immediately." + }, + { + "name": "apply_recommendation", + "description": "Apply a safe database recommendation (e.g. SET GLOBAL or ALTER TABLE).", + "inputSchema": { + "type": "object", + "properties": { + "statement": {"type": "string", "description": "The SQL statement to execute."}, + "variable_name": {"type": "string", "description": "The global variable name being set (optional, for rollback)."} + }, + "required": ["statement"] + } + }, + { + "name": "rollback_recommendation", + "description": "Revert a previously applied database recommendation.", + "inputSchema": { + "type": "object", + "properties": { + "statement_id": {"type": "string", "description": "The Statement ID returned during execution."} + }, + "required": ["statement_id"] + } + } + ] + }, + "id": id_ + } + elif method == "tools/call": + params = req.get("params", {}) + name = params.get("name") + arguments = params.get("arguments", {}) + + if name == "get_latest_audit": + res = handle_get_latest_audit() + elif name == "run_audit": + res = handle_run_audit() + elif name == "apply_recommendation": + res = handle_apply_recommendation(arguments) + elif name == "rollback_recommendation": + res = handle_rollback_recommendation(arguments) + else: + res = {"isError": True, "content": [{"type": "text", "text": f"Unknown tool: {name}"}]} + + resp = { + "jsonrpc": "2.0", + "result": res, + "id": id_ + } + elif method == "resources/list": + resp = { + "jsonrpc": "2.0", + "result": { + "resources": [ + { + "uri": "mysqltuner://reports/latest.json", + "name": "Latest JSON report", + "mimeType": "application/json" + }, + { + "uri": "mysqltuner://reports/latest.html", + "name": "Latest HTML dashboard", + "mimeType": "text/html" + } + ] + }, + "id": id_ + } + elif method == "resources/read": + params = req.get("params", {}) + uri = params.get("uri") + + content = "" + mime = "text/plain" + target_path = None + if uri == "mysqltuner://reports/latest.json": + target_path = LATEST_JSON + mime = "application/json" + elif uri == "mysqltuner://reports/latest.html": + target_path = LATEST_HTML + mime = "text/html" + + if target_path and os.path.exists(target_path): + try: + with open(target_path, "r") as f: + content = f.read() + except Exception as e: + content = f"Error reading resource: {str(e)}" + else: + content = "Resource not found or empty." + + resp = { + "jsonrpc": "2.0", + "result": { + "contents": [ + { + "uri": uri, + "mimeType": mime, + "text": content + } + ] + }, + "id": id_ + } + else: + resp = { + "jsonrpc": "2.0", + "error": { + "code": -32601, + "message": f"Method not found: {method}" + }, + "id": id_ + } + + sys.stdout.write(json.dumps(resp) + "\n") + sys.stdout.flush() + except Exception as e: + sys.stderr.write(traceback.format_exc() + "\n") + sys.stderr.flush() + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "--daemon": + # Run daemon auditing in foreground + daemon_loop() + else: + # Start daemon interval thread + t = threading.Thread(target=daemon_loop, daemon=True) + t.start() + # Serve MCP stdio + main_mcp() diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 2815388f3..5dd5d7031 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -31,6 +31,7 @@ - chore(main): hide hostname, ssl, and replication warnings on localhost (#933) - chore(releases): start v2.9.1 branch with version bump and dependency updates - feat(cli): resolve remote memory, socket override, and temptable sizing issues +- feat(container): implement dockerized auditing daemon and zero-dependency MCP server - feat(main): audit InnoDB lock monitors and print all deadlocks configuration - feat(main): enhance error log parser with OOM, semaphore waits, file limits, and page corruption alerts - feat(main): implement advanced log parser and lock monitoring @@ -56,6 +57,7 @@ - test(test): add unit_galera_pxc.t to validate Phase 9 Galera cluster checks - test(test): add unit_ha_cluster.t to validate Phase 7 HA cluster checks - test(test): add unit_log_parser.t to validate advanced log parsing and correlation +- test(test): add unit_mcp_server.t to validate JSON-RPC protocol compliance - test(test): add unit_replication_internals.t to validate Phase 8 replication features - test(test): add unit_version_helpers.t to validate version caching and helpers - test(test): add unit_workload_traffic.t to validate Phase 11 workload profiling @@ -84,6 +86,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes for v2.9.1 (c0f4161) - docs(roadmap): mark Phase 12 as completed in ROADMAP.md (dd288c2) - docs: regenerate release notes (56d1eee) - docs: regenerate release notes (3978e7a) diff --git a/tests/unit_mcp_server.t b/tests/unit_mcp_server.t new file mode 100644 index 000000000..592a075dc --- /dev/null +++ b/tests/unit_mcp_server.t @@ -0,0 +1,69 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; +use FindBin; +use IPC::Open2; +use File::Path qw(make_path remove_tree); + +# Ensure python3 is available +my $has_python = system("which python3 >/dev/null 2>&1") == 0; +if (!$has_python) { + plan skip_all => "python3 is required to run MCP server tests"; +} + +subtest 'MCP Server JSON-RPC Protocol Compliance' => sub { + # Set cache directory inside the workspace/tests directory to avoid permission issues + my $test_cache_dir = "$FindBin::Bin/mcp_cache"; + remove_tree($test_cache_dir) if -d $test_cache_dir; + make_path($test_cache_dir); + + local $ENV{'CACHE_DIR'} = $test_cache_dir; + + my ($chld_out, $chld_in); + my $pid = eval { + open2($chld_out, $chld_in, "python3", "$FindBin::Bin/../build/mcp_server.py"); + }; + if (!$pid) { + fail("Failed to spawn mcp_server.py: $@"); + remove_tree($test_cache_dir); + return; + } + + # Enable autoflush + my $old_fh = select($chld_in); + $| = 1; + select($old_fh); + + # 1. Initialize call + my $init_req = '{"jsonrpc": "2.0", "method": "initialize", "id": "req-1"}' . "\n"; + print $chld_in $init_req; + my $init_resp = <$chld_out>; + like($init_resp, qr/"jsonrpc"\s*:\s*"2.0"/, 'Initialize returns JSON-RPC 2.0'); + like($init_resp, qr/"name"\s*:\s*"mysqltuner-mcp"/, 'Initialize returns server name'); + like($init_resp, qr/"id"\s*:\s*"req-1"/, 'Initialize retains transaction ID'); + + # 2. List tools call + my $tools_req = '{"jsonrpc": "2.0", "method": "tools/list", "id": "req-2"}' . "\n"; + print $chld_in $tools_req; + my $tools_resp = <$chld_out>; + like($tools_resp, qr/get_latest_audit/, 'Lists get_latest_audit tool'); + like($tools_resp, qr/run_audit/, 'Lists run_audit tool'); + like($tools_resp, qr/apply_recommendation/, 'Lists apply_recommendation tool'); + like($tools_resp, qr/rollback_recommendation/, 'Lists rollback_recommendation tool'); + + # 3. List resources call + my $res_req = '{"jsonrpc": "2.0", "method": "resources/list", "id": "req-3"}' . "\n"; + print $chld_in $res_req; + my $res_resp = <$chld_out>; + like($res_resp, qr/mysqltuner:\/\/reports\/latest.json/, 'Lists report resources'); + + # Clean up subprocess + close $chld_in; + close $chld_out; + waitpid($pid, 0); + remove_tree($test_cache_dir); + pass('Subprocess terminated cleanly'); +}; + +done_testing(); From 89004df62f095275fefe95eb82c74391988ae48a Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:47:00 +0200 Subject: [PATCH 47/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 5dd5d7031..2e2eb76b8 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -86,6 +86,7 @@ ## 🛠️ Internal Commit History +- feat(container): implement dockerized auditing daemon and zero-dependency MCP server (975977c) - docs: regenerate release notes for v2.9.1 (c0f4161) - docs(roadmap): mark Phase 12 as completed in ROADMAP.md (dd288c2) - docs: regenerate release notes (56d1eee) From 7846c464885d528d711bc09c72c354b961c9f8a2 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:48:11 +0200 Subject: [PATCH 48/97] docs: regenerate release notes --- Changelog | 1 + releases/v2.9.1.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Changelog b/Changelog index 516646777..324187f79 100644 --- a/Changelog +++ b/Changelog @@ -61,6 +61,7 @@ - docs(metadata): remove timestamp from doc-sync generated files - docs(releases): regenerate release notes and changelog - docs(roadmap): link strategic technical evolutions specification and enforce changelog staging +- docs(roadmap): mark Phase 12 as completed in ROADMAP.md - docs(roadmap): update Phase 7, Phase 9, Phase 10, and Phase 11 status to completed in ROADMAP.md - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: regenerate release notes diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 2e2eb76b8..f4d006670 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -66,6 +66,7 @@ - docs(metadata): remove timestamp from doc-sync generated files - docs(releases): regenerate release notes and changelog - docs(roadmap): link strategic technical evolutions specification and enforce changelog staging +- docs(roadmap): mark Phase 12 as completed in ROADMAP.md - docs(roadmap): update Phase 7, Phase 9, Phase 10, and Phase 11 status to completed in ROADMAP.md - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: regenerate release notes @@ -86,6 +87,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (89004df) - feat(container): implement dockerized auditing daemon and zero-dependency MCP server (975977c) - docs: regenerate release notes for v2.9.1 (c0f4161) - docs(roadmap): mark Phase 12 as completed in ROADMAP.md (dd288c2) From 81ae23abc92665fe24db51b1c187c8a7aa881d34 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:48:11 +0200 Subject: [PATCH 49/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index f4d006670..789df9f77 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -87,6 +87,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (7846c46) - docs: regenerate release notes (89004df) - feat(container): implement dockerized auditing daemon and zero-dependency MCP server (975977c) - docs: regenerate release notes for v2.9.1 (c0f4161) From d71fdbcf66bb8c192787bafc6cf62890839fcd24 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:52:36 +0200 Subject: [PATCH 50/97] feat(cli): implement agent-json flag returning structured actionable schema --- Changelog | 2 + mysqltuner.pl | 956 ++++++++++++++++++++++++++++------------ tests/unit_agent_json.t | 83 ++++ 3 files changed, 766 insertions(+), 275 deletions(-) create mode 100644 tests/unit_agent_json.t diff --git a/Changelog b/Changelog index 324187f79..a8d126d92 100644 --- a/Changelog +++ b/Changelog @@ -22,6 +22,7 @@ - chore(releases): start v2.9.1 branch with version bump and dependency updates - chore: automated project metadata update - chore: remove execution.log from git repository and sync docs +- feat(cli): implement agent-json flag returning structured actionable schema - feat(cli): resolve remote memory, socket override, and temptable sizing issues - feat(container): implement dockerized auditing daemon and zero-dependency MCP server - feat(main): audit InnoDB lock monitors and print all deadlocks configuration @@ -46,6 +47,7 @@ - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 - test(test): add unit tests for potential issues and split unit_coverage_boost3 +- test(test): add unit_agent_json.t to validate actionable JSON schema - test(test): add unit_deadlocks_pfs.t to validate InnoDB deadlock PFS checks - test(test): add unit_eol_warnings.t to validate version support warnings - test(test): add unit_galera_pxc.t to validate Phase 9 Galera cluster checks diff --git a/mysqltuner.pl b/mysqltuner.pl index bc4b593f2..0cb65e7a7 100755 --- a/mysqltuner.pl +++ b/mysqltuner.pl @@ -300,6 +300,12 @@ package main; desc => 'Print result as JSON string', cat => 'PERFORMANCE' }, + 'agent-json' => { + type => '!', + default => 0, + desc => 'Print result as actionable JSON schema for AI agents', + cat => 'PERFORMANCE' + }, 'prettyjson' => { type => '!', default => 0, @@ -887,7 +893,10 @@ sub show_help { sub prettyprint { print $_[0] . "\n" - unless ( $opt{'silent'} or $opt{'json'} or $opt{'yaml'} ); + unless ( $opt{'silent'} + or $opt{'json'} + or $opt{'yaml'} + or $opt{'agent-json'} ); print $fh $_[0] . "\n" if defined($fh); my $plain_text = $_[0] // ''; $plain_text =~ s/\e\[[0-9;]*[mK]//g; @@ -1572,118 +1581,165 @@ sub check_replication_advanced { my $skip_verify = $myvar{'replica_skip_verify_binlog_checksum'} // $myvar{'slave_skip_verify_binlog_checksum'} // ''; if ( $skip_verify eq 'ON' || $skip_verify eq '1' ) { - badprint "replica_skip_verify_binlog_checksum is enabled (recommended: OFF)"; + badprint + "replica_skip_verify_binlog_checksum is enabled (recommended: OFF)"; push_recommendation( 'res', "Disable replica_skip_verify_binlog_checksum to ensure checksum verification on replica." ); } # --- PHASE 7: HA & INNODB CLUSTER DIAGNOSTICS --- - my $is_group_repl = ( - defined $myvar{'group_replication_group_name'} - || defined $myvar{'group_replication_single_primary_mode'} - ); + my $is_group_repl = ( defined $myvar{'group_replication_group_name'} + || defined $myvar{'group_replication_single_primary_mode'} ); if ($is_group_repl) { subheaderprint "InnoDB Cluster & Group Replication Diagnostics"; - + # 1. Topology & Role Audit - if (defined $myvar{'performance_schema'} && $myvar{'performance_schema'} eq 'ON') { - my @group_members = select_array("SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE, MEMBER_VERSION FROM performance_schema.replication_group_members"); + if ( defined $myvar{'performance_schema'} + && $myvar{'performance_schema'} eq 'ON' ) + { + my @group_members = select_array( +"SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE, MEMBER_VERSION FROM performance_schema.replication_group_members" + ); my $member_count = scalar(@group_members); - if ($member_count > 0) { - goodprint "InnoDB Cluster is running with $member_count group members."; + if ( $member_count > 0 ) { + goodprint + "InnoDB Cluster is running with $member_count group members."; my $primary_count = 0; my %versions; foreach my $m (@group_members) { - my @mparts = split(/\|/, $m); - my $host = $mparts[0] // ''; - my $port = $mparts[1] // ''; - my $state = $mparts[2] // ''; - my $role = $mparts[3] // ''; - my $ver = $mparts[4] // ''; - - if ($state ne 'ONLINE') { - badprint "Group member $host:$port state is $state (recommended: ONLINE)"; - push_recommendation('res', "Group member $host:$port is not ONLINE (state: $state). Check member status and error log."); + my @mparts = split( /\|/, $m ); + my $host = $mparts[0] // ''; + my $port = $mparts[1] // ''; + my $state = $mparts[2] // ''; + my $role = $mparts[3] // ''; + my $ver = $mparts[4] // ''; + + if ( $state ne 'ONLINE' ) { + badprint +"Group member $host:$port state is $state (recommended: ONLINE)"; + push_recommendation( 'res', +"Group member $host:$port is not ONLINE (state: $state). Check member status and error log." + ); } - if ($role eq 'PRIMARY') { + if ( $role eq 'PRIMARY' ) { $primary_count++; } $versions{$ver}++ if $ver; } - - my $single_primary = $myvar{'group_replication_single_primary_mode'} // 'ON'; - if ($single_primary eq 'ON' || $single_primary eq '1') { - if ($primary_count != 1) { - badprint "Single-primary mode active, but found $primary_count primary member(s) (recommended: 1)"; - push_recommendation('res', "Investigate role state: single-primary mode requires exactly 1 primary member."); + + my $single_primary = + $myvar{'group_replication_single_primary_mode'} // 'ON'; + if ( $single_primary eq 'ON' || $single_primary eq '1' ) { + if ( $primary_count != 1 ) { + badprint +"Single-primary mode active, but found $primary_count primary member(s) (recommended: 1)"; + push_recommendation( 'res', +"Investigate role state: single-primary mode requires exactly 1 primary member." + ); } } - + my @unique_vers = keys %versions; - if (scalar(@unique_vers) > 1) { - badprint "Inconsistent MySQL versions across group members: " . join(", ", @unique_vers); - push_recommendation('res', "Standardize MySQL versions across all group members to ensure replication compatibility."); + if ( scalar(@unique_vers) > 1 ) { + badprint + "Inconsistent MySQL versions across group members: " + . join( ", ", @unique_vers ); + push_recommendation( 'res', +"Standardize MySQL versions across all group members to ensure replication compatibility." + ); } } - + # 2. Flow Control & Certification Conflict Analytics my $server_uuid = select_one('SELECT @@server_uuid') // ''; if ($server_uuid) { - my $local_stats = select_one("SELECT CONCAT_WS('|', COUNT_TRANSACTIONS_IN_QUEUE, COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE, TRANSACTIONS_COMMITTED_ALL_MEMBERS, TRANSACTIONS_LOCAL_ROLLBACK) FROM performance_schema.replication_group_member_stats WHERE MEMBER_ID = '$server_uuid'"); + my $local_stats = select_one( +"SELECT CONCAT_WS('|', COUNT_TRANSACTIONS_IN_QUEUE, COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE, TRANSACTIONS_COMMITTED_ALL_MEMBERS, TRANSACTIONS_LOCAL_ROLLBACK) FROM performance_schema.replication_group_member_stats WHERE MEMBER_ID = '$server_uuid'" + ); if ($local_stats) { - my ($cert_queue, $applier_queue, $committed, $rollbacks) = split(/\|/, $local_stats); - $cert_queue //= 0; + my ( $cert_queue, $applier_queue, $committed, $rollbacks ) + = split( /\|/, $local_stats ); + $cert_queue //= 0; $applier_queue //= 0; - $committed //= 0; - $rollbacks //= 0; - - my $cert_thresh = $myvar{'group_replication_flow_control_certifier_threshold'} // 25000; - my $applier_thresh = $myvar{'group_replication_flow_control_applier_threshold'} // 25000; - - if ($cert_queue > $cert_thresh) { - badprint "Flow Control: Certification queue ($cert_queue) exceeds threshold ($cert_thresh)"; - push_recommendation('res', "Increase group_replication_flow_control_period or tune certifier settings to handle high transaction load."); + $committed //= 0; + $rollbacks //= 0; + + my $cert_thresh = + $myvar{ + 'group_replication_flow_control_certifier_threshold'} + // 25000; + my $applier_thresh = + $myvar{'group_replication_flow_control_applier_threshold'} + // 25000; + + if ( $cert_queue > $cert_thresh ) { + badprint +"Flow Control: Certification queue ($cert_queue) exceeds threshold ($cert_thresh)"; + push_recommendation( 'res', +"Increase group_replication_flow_control_period or tune certifier settings to handle high transaction load." + ); } - if ($applier_queue > $applier_thresh) { - badprint "Flow Control: Applier queue ($applier_queue) exceeds threshold ($applier_thresh)"; - push_recommendation('res', "Scale replication parallel threads (current workers check) or check disk I/O on slower nodes."); + if ( $applier_queue > $applier_thresh ) { + badprint +"Flow Control: Applier queue ($applier_queue) exceeds threshold ($applier_thresh)"; + push_recommendation( 'res', +"Scale replication parallel threads (current workers check) or check disk I/O on slower nodes." + ); } - + my $total_tx = $committed + $rollbacks; - if ($total_tx > 0) { + if ( $total_tx > 0 ) { my $rollback_ratio = $rollbacks / $total_tx; - if ($rollback_ratio > 0.05) { - badprint "Certification rollback ratio is " . sprintf("%.2f%%", $rollback_ratio * 100) . " (too many optimistic lock conflicts)"; - push_recommendation('res', "High certification rollback ratio detected. Optimize write concurrency or switch to Single-Primary mode."); + if ( $rollback_ratio > 0.05 ) { + badprint "Certification rollback ratio is " + . sprintf( "%.2f%%", $rollback_ratio * 100 ) + . " (too many optimistic lock conflicts)"; + push_recommendation( 'res', +"High certification rollback ratio detected. Optimize write concurrency or switch to Single-Primary mode." + ); } } } } } - + # 3. Communication Cache & Quorum Integrity - my $cache_size = $myvar{'group_replication_message_cache_size'} // 1073741824; - if ($physical_memory > 0) { + my $cache_size = $myvar{'group_replication_message_cache_size'} + // 1073741824; + if ( $physical_memory > 0 ) { my $cache_pct = $cache_size / $physical_memory; - if ($cache_pct > 0.3) { - badprint "group_replication_message_cache_size is " . hr_bytes($cache_size) . " (" . sprintf("%.1f%%", $cache_pct * 100) . " of system memory)"; - push_recommendation('res', "Reduce group_replication_message_cache_size to prevent OOM errors on this system."); + if ( $cache_pct > 0.3 ) { + badprint "group_replication_message_cache_size is " + . hr_bytes($cache_size) . " (" + . sprintf( "%.1f%%", $cache_pct * 100 ) + . " of system memory)"; + push_recommendation( 'res', +"Reduce group_replication_message_cache_size to prevent OOM errors on this system." + ); } } - - my $unreachable_timeout = $myvar{'group_replication_unreachable_majority_timeout'} // 0; - if ($unreachable_timeout == 0) { - badprint "group_replication_unreachable_majority_timeout is set to 0 (default: risk of cluster hang on partition)"; - push_recommendation('res', "Configure group_replication_unreachable_majority_timeout to a positive value (e.g., 10s) to allow auto-eviction."); + + my $unreachable_timeout = + $myvar{'group_replication_unreachable_majority_timeout'} // 0; + if ( $unreachable_timeout == 0 ) { + badprint +"group_replication_unreachable_majority_timeout is set to 0 (default: risk of cluster hang on partition)"; + push_recommendation( 'res', +"Configure group_replication_unreachable_majority_timeout to a positive value (e.g., 10s) to allow auto-eviction." + ); } } - + # 4. MySQL Router Connectivity (Experimental) - my $router_conn = select_one("SELECT COUNT(*) FROM information_schema.processlist WHERE USER LIKE '%router%' OR HOST LIKE '%router%'"); - my $router_conn_count = (defined $router_conn && $router_conn =~ /^\d+$/) ? $router_conn : 0; - if ($router_conn_count > 0) { - goodprint "MySQL Router connections active: found $router_conn_count connection(s) routed to this instance."; + my $router_conn = select_one( +"SELECT COUNT(*) FROM information_schema.processlist WHERE USER LIKE '%router%' OR HOST LIKE '%router%'" + ); + my $router_conn_count = + ( defined $router_conn && $router_conn =~ /^\d+$/ ) ? $router_conn : 0; + if ( $router_conn_count > 0 ) { + goodprint +"MySQL Router connections active: found $router_conn_count connection(s) routed to this instance."; } } @@ -1715,134 +1771,186 @@ sub check_workload_traffic { subheaderprint "Workload Analysis & Traffic Profiling"; # 1. Workload Characterization (Read-Heavy vs Write-Heavy vs Mixed) - my $com_select = $mystat{'Com_select'} // 0; - my $com_insert = $mystat{'Com_insert'} // 0; - my $com_update = $mystat{'Com_update'} // 0; - my $com_delete = $mystat{'Com_delete'} // 0; + my $com_select = $mystat{'Com_select'} // 0; + my $com_insert = $mystat{'Com_insert'} // 0; + my $com_update = $mystat{'Com_update'} // 0; + my $com_delete = $mystat{'Com_delete'} // 0; my $com_replace = $mystat{'Com_replace'} // 0; - - my $total_reads = $com_select; + + my $total_reads = $com_select; my $total_writes = $com_insert + $com_update + $com_delete + $com_replace; - my $total_ops = $total_reads + $total_writes; - - if ($total_ops > 0) { + my $total_ops = $total_reads + $total_writes; + + if ( $total_ops > 0 ) { my $read_ratio = $total_reads / $total_ops; - my $char_str = "Mixed"; - if ($read_ratio > 0.80) { - $char_str = "Read-Heavy (" . sprintf("%.1f%%", $read_ratio * 100) . " reads)"; + my $char_str = "Mixed"; + if ( $read_ratio > 0.80 ) { + $char_str = + "Read-Heavy (" + . sprintf( "%.1f%%", $read_ratio * 100 ) + . " reads)"; goodprint "Workload Characterization: $char_str"; - } elsif ($read_ratio < 0.20) { - $char_str = "Write-Heavy (" . sprintf("%.1f%%", (1 - $read_ratio) * 100) . " writes)"; + } + elsif ( $read_ratio < 0.20 ) { + $char_str = + "Write-Heavy (" + . sprintf( "%.1f%%", ( 1 - $read_ratio ) * 100 ) + . " writes)"; badprint "Workload Characterization: $char_str"; - push @generalrec, "Write-heavy workload detected. Optimize redo logs size, transaction log flushing, and doublewrite buffer."; - } else { - $char_str = "Mixed (" . sprintf("%.1f%%", $read_ratio * 100) . " reads, " . sprintf("%.1f%%", (1 - $read_ratio) * 100) . " writes)"; + push @generalrec, +"Write-heavy workload detected. Optimize redo logs size, transaction log flushing, and doublewrite buffer."; + } + else { + $char_str = + "Mixed (" + . sprintf( "%.1f%%", $read_ratio * 100 ) + . " reads, " + . sprintf( "%.1f%%", ( 1 - $read_ratio ) * 100 ) + . " writes)"; goodprint "Workload Characterization: $char_str"; } - } else { - infoprint "Workload Characterization: No query counters traffic recorded yet."; + } + else { + infoprint + "Workload Characterization: No query counters traffic recorded yet."; } # 2. Wait Event Fingerprinting (Performance Schema Waits analysis) - if (defined $myvar{'performance_schema'} && $myvar{'performance_schema'} eq 'ON') { + if ( defined $myvar{'performance_schema'} + && $myvar{'performance_schema'} eq 'ON' ) + { my @wait_events = select_array( - "SELECT EVENT_NAME, SUM_TIMER_WAIT FROM performance_schema.events_waits_summary_global_by_event_name WHERE SUM_TIMER_WAIT > 0 AND EVENT_NAME NOT LIKE 'idle%' AND EVENT_NAME NOT LIKE 'wait/io/table/%' ORDER BY SUM_TIMER_WAIT DESC LIMIT 5" +"SELECT EVENT_NAME, SUM_TIMER_WAIT FROM performance_schema.events_waits_summary_global_by_event_name WHERE SUM_TIMER_WAIT > 0 AND EVENT_NAME NOT LIKE 'idle%' AND EVENT_NAME NOT LIKE 'wait/io/table/%' ORDER BY SUM_TIMER_WAIT DESC LIMIT 5" ); - if (scalar(@wait_events) > 0) { + if ( scalar(@wait_events) > 0 ) { infoprint "Top Wait Events (excluding idle/table I/O):"; my $disk_waits = 0; my $lock_waits = 0; - my $net_waits = 0; + my $net_waits = 0; foreach my $we (@wait_events) { - my ($name, $wait) = split(/\|/, $we); + my ( $name, $wait ) = split( /\|/, $we ); $name //= ''; $wait //= 0; - infoprint " - $name: " . sprintf("%.2fs", $wait / 1000000000000); - - if ($name =~ m{^wait/io/file/}) { $disk_waits += $wait; } - elsif ($name =~ m{^wait/synch/}) { $lock_waits += $wait; } - elsif ($name =~ m{^wait/io/socket/}) { $net_waits += $wait; } + infoprint " - $name: " + . sprintf( "%.2fs", $wait / 1000000000000 ); + + if ( $name =~ m{^wait/io/file/} ) { $disk_waits += $wait; } + elsif ( $name =~ m{^wait/synch/} ) { $lock_waits += $wait; } + elsif ( $name =~ m{^wait/io/socket/} ) { $net_waits += $wait; } } - + my $total_wait = $disk_waits + $lock_waits + $net_waits; - if ($total_wait > 0) { - if ($disk_waits / $total_wait > 0.5) { - badprint "Primary DB Bottleneck detected: Disk/File I/O waits"; - push @generalrec, "Primary database bottleneck is Disk I/O. Check storage performance, swap usage, or increase buffer pool."; - } elsif ($lock_waits / $total_wait > 0.5) { - badprint "Primary DB Bottleneck detected: Synchronization Locks/Mutex waits"; - push @generalrec, "Primary database bottleneck is Lock contention. Optimize slow transactions and reduce lock wait timeout."; - } elsif ($net_waits / $total_wait > 0.5) { - badprint "Primary DB Bottleneck detected: Network Sockets waits"; - push @generalrec, "Primary database bottleneck is Network/Socket I/O. Check client connections latency and network bandwidth."; + if ( $total_wait > 0 ) { + if ( $disk_waits / $total_wait > 0.5 ) { + badprint + "Primary DB Bottleneck detected: Disk/File I/O waits"; + push @generalrec, +"Primary database bottleneck is Disk I/O. Check storage performance, swap usage, or increase buffer pool."; + } + elsif ( $lock_waits / $total_wait > 0.5 ) { + badprint +"Primary DB Bottleneck detected: Synchronization Locks/Mutex waits"; + push @generalrec, +"Primary database bottleneck is Lock contention. Optimize slow transactions and reduce lock wait timeout."; + } + elsif ( $net_waits / $total_wait > 0.5 ) { + badprint + "Primary DB Bottleneck detected: Network Sockets waits"; + push @generalrec, +"Primary database bottleneck is Network/Socket I/O. Check client connections latency and network bandwidth."; } } } } - # 3. Table Churn & Fragmentation Advisor (Combining PFS write metrics and fragmentation stats) - if (defined $myvar{'performance_schema'} && $myvar{'performance_schema'} eq 'ON') { +# 3. Table Churn & Fragmentation Advisor (Combining PFS write metrics and fragmentation stats) + if ( defined $myvar{'performance_schema'} + && $myvar{'performance_schema'} eq 'ON' ) + { my @churn_tables = select_array( - "SELECT OBJECT_SCHEMA, OBJECT_NAME, COUNT_WRITE FROM performance_schema.table_io_waits_summary_by_table WHERE OBJECT_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys') AND COUNT_WRITE > 1000 ORDER BY COUNT_WRITE DESC LIMIT 5" +"SELECT OBJECT_SCHEMA, OBJECT_NAME, COUNT_WRITE FROM performance_schema.table_io_waits_summary_by_table WHERE OBJECT_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys') AND COUNT_WRITE > 1000 ORDER BY COUNT_WRITE DESC LIMIT 5" ); - if (scalar(@churn_tables) > 0) { + if ( scalar(@churn_tables) > 0 ) { my %churn_map; foreach my $ct (@churn_tables) { - my ($schema, $table, $writes) = split(/\|/, $ct); + my ( $schema, $table, $writes ) = split( /\|/, $ct ); $churn_map{"$schema.$table"} = $writes if $schema && $table; } - + my $matched_churn_frag = 0; - if (defined $result{'Tables'}{'Fragmented tables'}) { - foreach my $table_line ( @{ $result{'Tables'}{'Fragmented tables'} } ) { - my ( $table_schema, $table_name, $engine, $data_free ) = split /\t/msx, $table_line; - if (exists $churn_map{"$table_schema.$table_name"}) { + if ( defined $result{'Tables'}{'Fragmented tables'} ) { + foreach + my $table_line ( @{ $result{'Tables'}{'Fragmented tables'} } ) + { + my ( $table_schema, $table_name, $engine, $data_free ) = + split /\t/msx, $table_line; + if ( exists $churn_map{"$table_schema.$table_name"} ) { $matched_churn_frag++; - badprint "High-churn table `$table_schema`.`$table_name` is fragmented (free: " . hr_bytes($data_free) . ")"; - push @generalrec, "Defragment high-churn table `$table_schema`.`$table_name` to optimize DML space reclamation."; + badprint +"High-churn table `$table_schema`.`$table_name` is fragmented (free: " + . hr_bytes($data_free) . ")"; + push @generalrec, +"Defragment high-churn table `$table_schema`.`$table_name` to optimize DML space reclamation."; } } } - if ($matched_churn_frag > 0) { - infoprint "Identified $matched_churn_frag high-churn fragmented tables."; + if ( $matched_churn_frag > 0 ) { + infoprint +"Identified $matched_churn_frag high-churn fragmented tables."; } } } # 4. Auto-Increment Exhaustion Audit my @auto_inc_cols = select_array( - "SELECT t.TABLE_SCHEMA, t.TABLE_NAME, c.COLUMN_NAME, c.DATA_TYPE, t.AUTO_INCREMENT FROM information_schema.tables t JOIN information_schema.columns c ON t.table_schema = c.table_schema AND t.table_name = c.table_name WHERE c.extra = 'auto_increment' AND t.auto_increment IS NOT NULL AND t.table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')" +"SELECT t.TABLE_SCHEMA, t.TABLE_NAME, c.COLUMN_NAME, c.DATA_TYPE, t.AUTO_INCREMENT FROM information_schema.tables t JOIN information_schema.columns c ON t.table_schema = c.table_schema AND t.table_name = c.table_name WHERE c.extra = 'auto_increment' AND t.auto_increment IS NOT NULL AND t.table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')" ); - - if (scalar(@auto_inc_cols) > 0) { + + if ( scalar(@auto_inc_cols) > 0 ) { foreach my $col_info (@auto_inc_cols) { - my ($schema, $table, $col, $type, $curr_val) = split(/\|/, $col_info); - $type = lc($type // ''); + my ( $schema, $table, $col, $type, $curr_val ) = + split( /\|/, $col_info ); + $type = lc( $type // '' ); $curr_val //= 0; - - my $max_val = 0; - my $col_type = select_one("SELECT COLUMN_TYPE FROM information_schema.columns WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$col'") // ''; - my $is_unsigned = ($col_type =~ /unsigned/i) ? 1 : 0; - - if ($type eq 'tinyint') { + + my $max_val = 0; + my $col_type = select_one( +"SELECT COLUMN_TYPE FROM information_schema.columns WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$col'" + ) // ''; + my $is_unsigned = ( $col_type =~ /unsigned/i ) ? 1 : 0; + + if ( $type eq 'tinyint' ) { $max_val = $is_unsigned ? 255 : 127; - } elsif ($type eq 'smallint') { + } + elsif ( $type eq 'smallint' ) { $max_val = $is_unsigned ? 65535 : 32767; - } elsif ($type eq 'mediumint') { + } + elsif ( $type eq 'mediumint' ) { $max_val = $is_unsigned ? 16777215 : 8388607; - } elsif ($type eq 'int' || $type eq 'integer') { + } + elsif ( $type eq 'int' || $type eq 'integer' ) { $max_val = $is_unsigned ? 4294967295 : 2147483647; - } elsif ($type eq 'bigint') { - $max_val = $is_unsigned ? 18446744073709551615 : 9223372036854775807; } - - if ($max_val > 0) { + elsif ( $type eq 'bigint' ) { + $max_val = + $is_unsigned ? 18446744073709551615 : 9223372036854775807; + } + + if ( $max_val > 0 ) { my $ratio = $curr_val / $max_val; - if ($ratio > 0.80) { - badprint "Auto-increment column `$schema`.`$table`.`$col` ($type) is near exhaustion: current value $curr_val (" . sprintf("%.1f%%", $ratio * 100) . " of max $max_val)"; - push @generalrec, "Danger of auto-increment overflow on `$schema`.`$table`.`$col`. Upgrade type to BIGINT or reset sequence."; - } elsif ($ratio > 0.50) { - infoprint "Auto-increment column `$schema`.`$table`.`$col` ($type) has reached " . sprintf("%.1f%%", $ratio * 100) . " capacity."; + if ( $ratio > 0.80 ) { + badprint +"Auto-increment column `$schema`.`$table`.`$col` ($type) is near exhaustion: current value $curr_val (" + . sprintf( "%.1f%%", $ratio * 100 ) + . " of max $max_val)"; + push @generalrec, +"Danger of auto-increment overflow on `$schema`.`$table`.`$col`. Upgrade type to BIGINT or reset sequence."; + } + elsif ( $ratio > 0.50 ) { + infoprint +"Auto-increment column `$schema`.`$table`.`$col` ($type) has reached " + . sprintf( "%.1f%%", $ratio * 100 ) + . " capacity."; } } } @@ -1850,7 +1958,8 @@ sub check_workload_traffic { } sub generate_auto_fix_snippets { - return if $opt{'silent'} || $opt{'json'} || $opt{'yaml'}; + return + if $opt{'silent'} || $opt{'json'} || $opt{'yaml'} || $opt{'agent-json'}; subheaderprint "Guided Auto-Fix Snippets"; if ( @adjvars > 0 ) { @@ -2588,7 +2697,11 @@ sub get_http_cli { # Checks for updates to MySQLTuner sub validate_tuner_version { if ( $opt{'checkversion'} eq 0 ) { - print "\n" unless ( $opt{'silent'} or $opt{'json'} or $opt{'yaml'} ); + print "\n" + unless ( $opt{'silent'} + or $opt{'json'} + or $opt{'yaml'} + or $opt{'agent-json'} ); infoprint "Skipped version check for MySQLTuner script"; return; } @@ -2650,7 +2763,11 @@ sub validate_tuner_version { sub update_tuner_version { if ( $opt{'updateversion'} eq 0 ) { badprint "Skipped version update for MySQLTuner script"; - print "\n" unless ( $opt{'silent'} or $opt{'json'} or $opt{'yaml'} ); + print "\n" + unless ( $opt{'silent'} + or $opt{'json'} + or $opt{'yaml'} + or $opt{'agent-json'} ); return; } @@ -4332,18 +4449,26 @@ sub log_file_recommendations { if ( $is_mysql && mysql_version_ge( 8, 0 ) ) { if ( defined $myvar{'log_error_verbosity'} ) { if ( $myvar{'log_error_verbosity'} < 2 ) { - badprint "log_error_verbosity is set to $myvar{'log_error_verbosity'} (should be >= 2)"; - push @generalrec, "Set log_error_verbosity = 2 or 3 to capture warning events in the error log"; - } else { - goodprint "log_error_verbosity is set to $myvar{'log_error_verbosity'}"; + badprint +"log_error_verbosity is set to $myvar{'log_error_verbosity'} (should be >= 2)"; + push @generalrec, +"Set log_error_verbosity = 2 or 3 to capture warning events in the error log"; + } + else { + goodprint + "log_error_verbosity is set to $myvar{'log_error_verbosity'}"; } } - } else { + } + else { if ( defined $myvar{'log_warnings'} ) { if ( $myvar{'log_warnings'} < 2 ) { - badprint "log_warnings is set to $myvar{'log_warnings'} (should be >= 2)"; - push @generalrec, "Set log_warnings = 2 or higher to capture warnings in the error log"; - } else { + badprint +"log_warnings is set to $myvar{'log_warnings'} (should be >= 2)"; + push @generalrec, +"Set log_warnings = 2 or higher to capture warnings in the error log"; + } + else { goodprint "log_warnings is set to $myvar{'log_warnings'}"; } } @@ -4439,8 +4564,8 @@ sub log_file_recommendations { my $nbErrLog = 0; my @lastShutdowns; my @lastStarts; - my $oom_detected = 0; - my $semaphore_detected = 0; + my $oom_detected = 0; + my $semaphore_detected = 0; my $file_limit_detected = 0; my $corruption_detected = 0; @@ -4454,16 +4579,23 @@ sub log_file_recommendations { if $logLi =~ /Shutdown complete/ and $logLi !~ /Innodb/i; push @lastStarts, $logLi if $logLi =~ /ready for connections/; - if ( $logLi =~ /(out of memory|OOM-killer|killed process|mysqld killed)/i ) { + if ( $logLi =~ + /(out of memory|OOM-killer|killed process|mysqld killed)/i ) + { $oom_detected++; } if ( $logLi =~ /(semaphore wait|long semaphore wait)/i ) { $semaphore_detected++; } - if ( $logLi =~ /(too many open files|Error in accept: Too many open files)/i ) { + if ( $logLi =~ + /(too many open files|Error in accept: Too many open files)/i ) + { $file_limit_detected++; } - if ( $logLi =~ /(is marked as crashed|checksum mismatch|corrupted page|Page checksum)/i ) { + if ( $logLi =~ +/(is marked as crashed|checksum mismatch|corrupted page|Page checksum)/i + ) + { $corruption_detected++; } } @@ -4485,33 +4617,48 @@ sub log_file_recommendations { } if ( $oom_detected > 0 ) { - badprint "Database error log contains $oom_detected Out-Of-Memory (OOM) patterns."; - push @generalrec, "Verify system RAM allocations and consider reducing innodb_buffer_pool_size to avoid OOM kills"; + badprint +"Database error log contains $oom_detected Out-Of-Memory (OOM) patterns."; + push @generalrec, +"Verify system RAM allocations and consider reducing innodb_buffer_pool_size to avoid OOM kills"; } if ( $semaphore_detected > 0 ) { - badprint "InnoDB long semaphore waits detected $semaphore_detected time(s) in the error log."; - push @generalrec, "Audit storage I/O capacity or check system-level contention as InnoDB experienced semaphore waits"; + badprint +"InnoDB long semaphore waits detected $semaphore_detected time(s) in the error log."; + push @generalrec, +"Audit storage I/O capacity or check system-level contention as InnoDB experienced semaphore waits"; } if ( $file_limit_detected > 0 ) { - badprint "Error log contains 'Too many open files' warnings $file_limit_detected time(s)."; - push @generalrec, "Increase open_files_limit or raise OS-level ulimits for file descriptors (nofile)"; + badprint +"Error log contains 'Too many open files' warnings $file_limit_detected time(s)."; + push @generalrec, +"Increase open_files_limit or raise OS-level ulimits for file descriptors (nofile)"; } if ( $corruption_detected > 0 ) { - badprint "Database corruption patterns detected $corruption_detected time(s) in the error log."; - push @generalrec, "Run CHECK TABLE / REPAIR TABLE or restore from backup as corruption warnings were detected"; + badprint +"Database corruption patterns detected $corruption_detected time(s) in the error log."; + push @generalrec, +"Run CHECK TABLE / REPAIR TABLE or restore from backup as corruption warnings were detected"; } # Correlation Engine (Experimental) if ( $semaphore_detected > 0 || $oom_detected > 0 ) { - my @load = get_load_average(); - my $cores = logical_cpu_cores(); + my @load = get_load_average(); + my $cores = logical_cpu_cores(); my $is_high_load = ( @load && $load[0] > $cores ) ? 1 : 0; - my $is_high_lock_waits = ( ($mystat{'Innodb_row_lock_waits'} // 0) > 10 || ($mystat{'Table_locks_waited'} // 0) > 10 ) ? 1 : 0; - + my $is_high_lock_waits = + ( ( $mystat{'Innodb_row_lock_waits'} // 0 ) > 10 + || ( $mystat{'Table_locks_waited'} // 0 ) > 10 ) ? 1 : 0; + if ( $is_high_load || $is_high_lock_waits ) { - infoprint "Experimental Correlation: Log anomalies (semaphore/OOM) correlate with active system pressure:"; - infoprint " - System Load: " . ($is_high_load ? "HIGH (@load)" : "Normal (@load)"); - infoprint " - Lock Contention: " . ($is_high_lock_waits ? "HIGH (Innodb_row_lock_waits=$mystat{'Innodb_row_lock_waits'})" : "Normal"); + infoprint +"Experimental Correlation: Log anomalies (semaphore/OOM) correlate with active system pressure:"; + infoprint " - System Load: " + . ( $is_high_load ? "HIGH (@load)" : "Normal (@load)" ); + infoprint " - Lock Contention: " + . ( $is_high_lock_waits + ? "HIGH (Innodb_row_lock_waits=$mystat{'Innodb_row_lock_waits'})" + : "Normal" ); } } @@ -10184,10 +10331,10 @@ sub get_wsrep_option { sub parse_size_bytes { my $str = shift // ''; $str =~ s/\s+//g; - if ($str =~ /^(\d+(?:\.\d+)?)([KMG])$/i) { - my ($val, $unit) = ($1, uc($2)); - return $val * 1024 if $unit eq 'K'; - return $val * 1024 * 1024 if $unit eq 'M'; + if ( $str =~ /^(\d+(?:\.\d+)?)([KMG])$/i ) { + my ( $val, $unit ) = ( $1, uc($2) ); + return $val * 1024 if $unit eq 'K'; + return $val * 1024 * 1024 if $unit eq 'M'; return $val * 1024 * 1024 * 1024 if $unit eq 'G'; } return $str if $str =~ /^\d+$/; @@ -11242,74 +11389,100 @@ sub mariadb_galera { # 1. Streaming Replication Monitor my $stream_writes = $mystat{'wsrep_streaming_log_writes'} // 0; my $stream_reads = $mystat{'wsrep_streaming_log_reads'} // 0; - if ($stream_writes > 0) { - badprint "Galera is using streaming replication: $stream_writes write(s), $stream_reads read(s)"; - push @generalrec, "Streaming replication active. Review wsrep_trx_fragment_size and ensure storage can handle the fragment log I/O overhead."; + if ( $stream_writes > 0 ) { + badprint +"Galera is using streaming replication: $stream_writes write(s), $stream_reads read(s)"; + push @generalrec, +"Streaming replication active. Review wsrep_trx_fragment_size and ensure storage can handle the fragment log I/O overhead."; } - + # 2. Gcache Sizing Optimization my $gcache_size_str = get_wsrep_option('gcache.size') // ''; if ($gcache_size_str) { my $gcache_bytes = parse_size_bytes($gcache_size_str); - if ($gcache_bytes > 0) { + if ( $gcache_bytes > 0 ) { my $min_gcache = 128 * 1024 * 1024; - my $five_pct_ram = $physical_memory ? ($physical_memory * 0.05) : 0; - my $target_gcache = ($five_pct_ram > $min_gcache) ? $five_pct_ram : $min_gcache; - if ($gcache_bytes < $target_gcache) { - badprint "gcache.size is too small ($gcache_size_str, recommended: >= " . hr_bytes($target_gcache) . ")"; - push @generalrec, "Increase gcache.size in wsrep_provider_options to maximize IST success and avoid full SST on node re-joins."; + my $five_pct_ram = + $physical_memory ? ( $physical_memory * 0.05 ) : 0; + my $target_gcache = + ( $five_pct_ram > $min_gcache ) ? $five_pct_ram : $min_gcache; + if ( $gcache_bytes < $target_gcache ) { + badprint + "gcache.size is too small ($gcache_size_str, recommended: >= " + . hr_bytes($target_gcache) . ")"; + push @generalrec, +"Increase gcache.size in wsrep_provider_options to maximize IST success and avoid full SST on node re-joins."; } } } - + # 3. Certification Conflict & Abort Analysis - my $bf_aborts = $mystat{'wsrep_local_bf_aborts'} // 0; + my $bf_aborts = $mystat{'wsrep_local_bf_aborts'} // 0; my $cert_failures = $mystat{'wsrep_local_cert_failures'} // 0; - if ($bf_aborts > 50 || $cert_failures > 50) { - badprint "High cluster certification conflicts: $bf_aborts brute-force abort(s), $cert_failures certification failure(s)"; - push @generalrec, "High certification conflicts. Optimize write concurrency, index hotspot tables, or review application transactions size."; + if ( $bf_aborts > 50 || $cert_failures > 50 ) { + badprint +"High cluster certification conflicts: $bf_aborts brute-force abort(s), $cert_failures certification failure(s)"; + push @generalrec, +"High certification conflicts. Optimize write concurrency, index hotspot tables, or review application transactions size."; } - + # 4. Advanced Flow Control Observability my $fc_sent = $mystat{'wsrep_flow_control_sent'} // 0; - if ($fc_sent > 0) { - badprint "Flow Control: Node triggered flow control $fc_sent times (pause sender culprit)"; - push @generalrec, "Node is triggering flow control pause events. Check disk write latency, CPU load, or swap space."; + if ( $fc_sent > 0 ) { + badprint +"Flow Control: Node triggered flow control $fc_sent times (pause sender culprit)"; + push @generalrec, +"Node is triggering flow control pause events. Check disk write latency, CPU load, or swap space."; } my $fc_paused = $mystat{'wsrep_flow_control_paused'} // 0; - if ($fc_paused > 0.05) { - badprint "Flow Control: Node replication was paused " . sprintf("%.2f%%", $fc_paused * 100) . " of the time (throttling)"; - push @generalrec, "Node is heavily throttled by flow control. Check slower nodes in the cluster triggering flow control."; + if ( $fc_paused > 0.05 ) { + badprint "Flow Control: Node replication was paused " + . sprintf( "%.2f%%", $fc_paused * 100 ) + . " of the time (throttling)"; + push @generalrec, +"Node is heavily throttled by flow control. Check slower nodes in the cluster triggering flow control."; } - + # 5. Group Communication Latency & Jitter my $repl_latency = $mystat{'wsrep_evs_repl_latency'} // ''; - if ($repl_latency && $repl_latency =~ m{^([\d.]+)/([\d.]+)/([\d.]+)/([\d.]+)$}) { - my ($min, $avg, $max, $stddev) = ($1, $2, $3, $4); - if ($avg > 0.1) { - badprint "High average replication latency: " . sprintf("%.2fms", $avg * 1000); - push @generalrec, "High average replication latency. Optimize inter-node network latency."; + if ( $repl_latency + && $repl_latency =~ m{^([\d.]+)/([\d.]+)/([\d.]+)/([\d.]+)$} ) + { + my ( $min, $avg, $max, $stddev ) = ( $1, $2, $3, $4 ); + if ( $avg > 0.1 ) { + badprint "High average replication latency: " + . sprintf( "%.2fms", $avg * 1000 ); + push @generalrec, +"High average replication latency. Optimize inter-node network latency."; } - if ($stddev > 0.05) { - badprint "High network replication jitter (stddev: " . sprintf("%.2fms", $stddev * 1000) . ")"; - push @generalrec, "High replication jitter. Review inter-node network stability and WAN link quality."; + if ( $stddev > 0.05 ) { + badprint "High network replication jitter (stddev: " + . sprintf( "%.2fms", $stddev * 1000 ) . ")"; + push @generalrec, +"High replication jitter. Review inter-node network stability and WAN link quality."; } } - + # 6. Applier Concurrency Tuning my $deps_dist = $mystat{'wsrep_cert_deps_distance'} // 0; - if ($deps_dist > 4 && $wsrep_threads_value == 1) { - infoprint "Average write-set dependency distance is " . sprintf("%.2f", $deps_dist) . " (threads: 1)"; - push @generalrec, "Consider increasing $wsrep_threads_var_name to leverage parallel certification concurrency."; + if ( $deps_dist > 4 && $wsrep_threads_value == 1 ) { + infoprint "Average write-set dependency distance is " + . sprintf( "%.2f", $deps_dist ) + . " (threads: 1)"; + push @generalrec, +"Consider increasing $wsrep_threads_var_name to leverage parallel certification concurrency."; } - + # 7. PXC Strict Mode Verification - my $is_percona = ( ($myvar{'version_comment'} // '') =~ /Percona/i || ($myvar{'version'} // '') =~ /Percona/i ); - if ($is_percona && defined $myvar{'pxc_strict_mode'}) { + my $is_percona = ( ( $myvar{'version_comment'} // '' ) =~ /Percona/i + || ( $myvar{'version'} // '' ) =~ /Percona/i ); + if ( $is_percona && defined $myvar{'pxc_strict_mode'} ) { my $pxc_mode = $myvar{'pxc_strict_mode'}; - if ($pxc_mode ne 'ENFORCING' && $pxc_mode ne 'MASTER') { - badprint "pxc_strict_mode is set to $pxc_mode (recommended: ENFORCING)"; - push @generalrec, "Set pxc_strict_mode = ENFORCING to prevent unsafe operations on Percona XtraDB Cluster."; + if ( $pxc_mode ne 'ENFORCING' && $pxc_mode ne 'MASTER' ) { + badprint + "pxc_strict_mode is set to $pxc_mode (recommended: ENFORCING)"; + push @generalrec, +"Set pxc_strict_mode = ENFORCING to prevent unsafe operations on Percona XtraDB Cluster."; } } @@ -12074,58 +12247,105 @@ sub mysql_innodb { # --- PHASE 6: DEEP ENGINE TUNING & SAFEGUARDING --- # Task 1: I/O Pressure & Flushing Advisor - if ( defined $mystat{'Innodb_buffer_pool_wait_free'} && $mystat{'Innodb_buffer_pool_wait_free'} > 0 ) { - my $io_cap = $myvar{'innodb_io_capacity'} // 0; + if ( defined $mystat{'Innodb_buffer_pool_wait_free'} + && $mystat{'Innodb_buffer_pool_wait_free'} > 0 ) + { + my $io_cap = $myvar{'innodb_io_capacity'} // 0; my $io_cap_max = $myvar{'innodb_io_capacity_max'} // 0; - badprint "InnoDB is experiencing I/O pressure (Innodb_buffer_pool_wait_free = $mystat{'Innodb_buffer_pool_wait_free'})"; - push( @generalrec, "Increase innodb_io_capacity (current: $io_cap) or innodb_io_capacity_max (current: $io_cap_max) to alleviate flushing/free buffer page waits." ); + badprint +"InnoDB is experiencing I/O pressure (Innodb_buffer_pool_wait_free = $mystat{'Innodb_buffer_pool_wait_free'})"; + push( @generalrec, +"Increase innodb_io_capacity (current: $io_cap) or innodb_io_capacity_max (current: $io_cap_max) to alleviate flushing/free buffer page waits." + ); } # Task 2: Read-Ahead Efficiency Audit - if ( defined $mystat{'Innodb_buffer_pool_read_ahead'} && $mystat{'Innodb_buffer_pool_read_ahead'} > 0 ) { + if ( defined $mystat{'Innodb_buffer_pool_read_ahead'} + && $mystat{'Innodb_buffer_pool_read_ahead'} > 0 ) + { my $read_ahead = $mystat{'Innodb_buffer_pool_read_ahead'}; - my $evicted = $mystat{'Innodb_buffer_pool_read_ahead_evicted'} // 0; - my $eviction_ratio = ($evicted / $read_ahead) * 100; + my $evicted = $mystat{'Innodb_buffer_pool_read_ahead_evicted'} // 0; + my $eviction_ratio = ( $evicted / $read_ahead ) * 100; if ( $eviction_ratio > 20 ) { - badprint sprintf("InnoDB read-ahead efficiency is low: %.2f%% of read-ahead pages were evicted without being accessed", $eviction_ratio); + badprint + sprintf( +"InnoDB read-ahead efficiency is low: %.2f%% of read-ahead pages were evicted without being accessed", + $eviction_ratio ); my $threshold = $myvar{'innodb_read_ahead_threshold'} // 56; - push( @generalrec, "Decrease innodb_read_ahead_threshold (current: $threshold) or disable innodb_random_read_ahead to reduce read-ahead page eviction." ); + push( @generalrec, +"Decrease innodb_read_ahead_threshold (current: $threshold) or disable innodb_random_read_ahead to reduce read-ahead page eviction." + ); } } # Task 3: Purge Lag Prevention - if ( defined $mystat{'Innodb_history_list_length'} && $mystat{'Innodb_history_list_length'} > 100000 ) { + if ( defined $mystat{'Innodb_history_list_length'} + && $mystat{'Innodb_history_list_length'} > 100000 ) + { my $purge_threads = $myvar{'innodb_purge_threads'} // 0; - badprint "InnoDB history list length is high ($mystat{'Innodb_history_list_length'}) - purge process may lag"; - push( @generalrec, "Increase innodb_purge_threads (current: $purge_threads, up to 32) and audit long-running transactions to prevent MVCC purge lag." ); + badprint +"InnoDB history list length is high ($mystat{'Innodb_history_list_length'}) - purge process may lag"; + push( @generalrec, +"Increase innodb_purge_threads (current: $purge_threads, up to 32) and audit long-running transactions to prevent MVCC purge lag." + ); } # Task 4: Change Buffer & Adaptive Hash Index Optimization my $rows_read = $mystat{'Innodb_rows_read'} // 0; - my $rows_write = ($mystat{'Innodb_rows_inserted'} // 0) + ($mystat{'Innodb_rows_updated'} // 0) + ($mystat{'Innodb_rows_deleted'} // 0); - if ( $rows_write > 0 && ($rows_read / $rows_write) > 100 ) { - if ( defined $myvar{'innodb_change_buffering'} && $myvar{'innodb_change_buffering'} ne 'none' ) { - badprint "Workload is highly read-intensive, but innodb_change_buffering is enabled"; - push( @generalrec, "Consider setting innodb_change_buffering = none to save buffer pool memory on read-only/read-heavy workloads." ); + my $rows_write = + ( $mystat{'Innodb_rows_inserted'} // 0 ) + + ( $mystat{'Innodb_rows_updated'} // 0 ) + + ( $mystat{'Innodb_rows_deleted'} // 0 ); + if ( $rows_write > 0 && ( $rows_read / $rows_write ) > 100 ) { + if ( defined $myvar{'innodb_change_buffering'} + && $myvar{'innodb_change_buffering'} ne 'none' ) + { + badprint +"Workload is highly read-intensive, but innodb_change_buffering is enabled"; + push( @generalrec, +"Consider setting innodb_change_buffering = none to save buffer pool memory on read-only/read-heavy workloads." + ); } } - if ( ( $myvar{'innodb_adaptive_hash_index'} // 'OFF' ) eq 'ON' && logical_cpu_cores() > 16 ) { - infoprint "InnoDB Adaptive Hash Index is enabled on a high core count system ($myvar{'innodb_adaptive_hash_index'})"; - push( @generalrec, "Monitor Adaptive Hash Index (AHI) lock contention; consider setting innodb_adaptive_hash_index = OFF if contention is high." ); + if ( ( $myvar{'innodb_adaptive_hash_index'} // 'OFF' ) eq 'ON' + && logical_cpu_cores() > 16 ) + { + infoprint +"InnoDB Adaptive Hash Index is enabled on a high core count system ($myvar{'innodb_adaptive_hash_index'})"; + push( @generalrec, +"Monitor Adaptive Hash Index (AHI) lock contention; consider setting innodb_adaptive_hash_index = OFF if contention is high." + ); } - # Task 5: Modern Storage Alignment (doublewrite pages, fdatasync, flush method) - my $is_mysql = !( ( defined $myvar{'version'} && $myvar{'version'} =~ /MariaDB/i ) - or ( defined $myvar{'version_comment'} && $myvar{'version_comment'} =~ /MariaDB/i ) ); - if ($is_mysql && mysql_version_ge( 8, 4 ) && defined $myvar{'innodb_doublewrite_pages'} && $myvar{'innodb_doublewrite_pages'} != 128 ) { - badprint "innodb_doublewrite_pages is not aligned with modern SSD/NVMe storage ($myvar{'innodb_doublewrite_pages'})"; - push( @generalrec, "Set innodb_doublewrite_pages = 128 for optimal doublewrite buffer throughput on modern SSDs." ); + # Task 5: Modern Storage Alignment (doublewrite pages, fdatasync, flush method) + my $is_mysql = !( + ( defined $myvar{'version'} && $myvar{'version'} =~ /MariaDB/i ) + or ( defined $myvar{'version_comment'} + && $myvar{'version_comment'} =~ /MariaDB/i ) + ); + if ( $is_mysql + && mysql_version_ge( 8, 4 ) + && defined $myvar{'innodb_doublewrite_pages'} + && $myvar{'innodb_doublewrite_pages'} != 128 ) + { + badprint +"innodb_doublewrite_pages is not aligned with modern SSD/NVMe storage ($myvar{'innodb_doublewrite_pages'})"; + push( @generalrec, +"Set innodb_doublewrite_pages = 128 for optimal doublewrite buffer throughput on modern SSDs." + ); } - if ( defined $myvar{'innodb_use_fdatasync'} && $myvar{'innodb_use_fdatasync'} eq 'OFF' ) { + if ( defined $myvar{'innodb_use_fdatasync'} + && $myvar{'innodb_use_fdatasync'} eq 'OFF' ) + { my $flush_method = $myvar{'innodb_flush_method'} // ''; - if ( $flush_method ne 'O_DIRECT' && $flush_method ne 'O_DIRECT_NO_FSYNC' ) { - infoprint "innodb_use_fdatasync is disabled ($myvar{'innodb_use_fdatasync'})"; - push( @generalrec, "Consider enabling innodb_use_fdatasync = ON to reduce fsync system call overhead on Linux if not using O_DIRECT." ); + if ( $flush_method ne 'O_DIRECT' + && $flush_method ne 'O_DIRECT_NO_FSYNC' ) + { + infoprint +"innodb_use_fdatasync is disabled ($myvar{'innodb_use_fdatasync'})"; + push( @generalrec, +"Consider enabling innodb_use_fdatasync = ON to reduce fsync system call overhead on Linux if not using O_DIRECT." + ); } } @@ -12138,12 +12358,17 @@ sub mysql_innodb { $has_numa = 1; } } - if ( $has_numa ) { - if ( defined $myvar{'innodb_numa_interleave'} && $myvar{'innodb_numa_interleave'} eq 'OFF' ) { + if ($has_numa) { + if ( defined $myvar{'innodb_numa_interleave'} + && $myvar{'innodb_numa_interleave'} eq 'OFF' ) + { my $total_mem = $physical_memory // 0; if ( $total_mem > 34359738368 ) { - badprint "NUMA architecture detected but innodb_numa_interleave is disabled"; - push( @generalrec, "Enable innodb_numa_interleave = ON to balance buffer pool memory allocation across NUMA nodes." ); + badprint +"NUMA architecture detected but innodb_numa_interleave is disabled"; + push( @generalrec, +"Enable innodb_numa_interleave = ON to balance buffer pool memory allocation across NUMA nodes." + ); } } } @@ -12153,17 +12378,23 @@ sub mysql_innodb { my $is_mariadb = !$is_mysql; if ( $is_mariadb && mysql_version_ge( 11, 4 ) ) { if ( defined $myvar{'innodb_truncate_temporary_tablespace_now'} ) { - goodprint "MariaDB online temporary tablespace truncation is supported"; + goodprint + "MariaDB online temporary tablespace truncation is supported"; } } # Task 8: Deadlock & Contention Analytics via Performance Schema if ( ( $myvar{'performance_schema'} // 'OFF' ) eq 'ON' ) { if ( $is_mysql && mysql_version_ge( 8, 0 ) ) { - my @err_res = select_array("SELECT SUM(COUNT_STAR) FROM performance_schema.events_errors_summary_global_by_error WHERE ERROR_NUMBER = 1213"); + my @err_res = select_array( +"SELECT SUM(COUNT_STAR) FROM performance_schema.events_errors_summary_global_by_error WHERE ERROR_NUMBER = 1213" + ); if ( @err_res && defined $err_res[0] && $err_res[0] > 0 ) { - badprint "InnoDB experienced $err_res[0] lock deadlocks (ER_LOCK_DEADLOCK)"; - push( @generalrec, "Optimize application queries, transaction lengths, and index coverage to reduce lock deadlocks." ); + badprint +"InnoDB experienced $err_res[0] lock deadlocks (ER_LOCK_DEADLOCK)"; + push( @generalrec, +"Optimize application queries, transaction lengths, and index coverage to reduce lock deadlocks." + ); } } } @@ -12171,21 +12402,34 @@ sub mysql_innodb { # Task 9: InnoDB Lock Monitoring & Deadlock Logging Audit if ( defined $myvar{'innodb_print_all_deadlocks'} ) { if ( $myvar{'innodb_print_all_deadlocks'} eq 'OFF' ) { - badprint "InnoDB deadlock logging (innodb_print_all_deadlocks) is disabled"; - push( @generalrec, "Enable innodb_print_all_deadlocks = ON to capture detailed transaction information in the error log when deadlocks occur." ); - } else { - goodprint "InnoDB deadlock logging (innodb_print_all_deadlocks) is enabled"; + badprint +"InnoDB deadlock logging (innodb_print_all_deadlocks) is disabled"; + push( @generalrec, +"Enable innodb_print_all_deadlocks = ON to capture detailed transaction information in the error log when deadlocks occur." + ); + } + else { + goodprint + "InnoDB deadlock logging (innodb_print_all_deadlocks) is enabled"; } } - if ( defined $myvar{'innodb_status_output'} && $myvar{'innodb_status_output'} eq 'OFF' ) { - infoprint "InnoDB periodic status output (innodb_status_output) is disabled"; + if ( defined $myvar{'innodb_status_output'} + && $myvar{'innodb_status_output'} eq 'OFF' ) + { + infoprint + "InnoDB periodic status output (innodb_status_output) is disabled"; } if ( defined $myvar{'innodb_status_output_locks'} ) { if ( $myvar{'innodb_status_output_locks'} eq 'OFF' ) { - infoprint "InnoDB lock monitor output (innodb_status_output_locks) is disabled"; - push( @generalrec, "Consider enabling innodb_status_output_locks = ON during active lock contention troubleshooting to print lock details in SHOW ENGINE INNODB STATUS." ); - } else { - goodprint "InnoDB lock monitor output (innodb_status_output_locks) is enabled"; + infoprint +"InnoDB lock monitor output (innodb_status_output_locks) is disabled"; + push( @generalrec, +"Consider enabling innodb_status_output_locks = ON during active lock contention troubleshooting to print lock details in SHOW ENGINE INNODB STATUS." + ); + } + else { + goodprint +"InnoDB lock monitor output (innodb_status_output_locks) is enabled"; } } @@ -12853,7 +13097,11 @@ sub mysql_databases { $result{'Databases'}{'All databases'}{'Index Pct'} = percentage( $totaldbinfo[2], $totaldbinfo[3] ) . "%"; $result{'Databases'}{'All databases'}{'Total Size'} = $totaldbinfo[3]; - print "\n" unless ( $opt{'silent'} or $opt{'json'} or $opt{'yaml'} ); + print "\n" + unless ( $opt{'silent'} + or $opt{'json'} + or $opt{'yaml'} + or $opt{'agent-json'} ); my $nbViews = 0; my $nbTables = 0; @@ -13651,6 +13899,9 @@ sub _serialize_to_json { return 'null'; } my $ref = ref($data); + if ( $ref eq 'SCALAR' ) { + return $$data ? 'true' : 'false'; + } if ( !$ref ) { if ( $data =~ /^-?(?:[0-9]+(?:\.[0-9]+)?)$/ ) { return $data; @@ -13679,6 +13930,161 @@ sub _serialize_to_json { } sub dump_result { + if ( $opt{'agent-json'} ) { + my @findings; + foreach my $adj (@adjvars) { + my $var_name = ''; + my $raw_target = ''; + if ( $adj =~ /^([a-zA-Z0-9_-]+)\s*[=(<>]+\s*(.*)$/ ) { + $var_name = $1; + $raw_target = $2; + } + else { + next; + } + + my $target_val = $raw_target; + $target_val =~ s/[()>=<=]//g; + $target_val =~ s/^\s+|\s+$//g; + + my $current_val = $myvar{$var_name} // ''; + + my $id = "${var_name}_adjust"; + my $topic = "Performance"; + my $description = "Variable $var_name is misconfigured."; + my $impact_score = 5; + my $risk_level = "Low"; + my $risk_description = +"Review the variable description and potential memory impact before applying."; + my $requires_restart = 0; + my $expected_outcome = + "Optimizes the $var_name configuration parameters."; + + if ( $var_name eq 'innodb_buffer_pool_size' ) { + $id = "innodb_buffer_pool_size_adjust"; + $topic = "Performance"; + $description = +"InnoDB buffer pool size is under-allocated for the current workload."; + $impact_score = 9; + $risk_level = "Medium"; + $risk_description = +"Increases memory consumption. Ensure sufficient OS-free RAM to prevent OOM swapping."; + $requires_restart = 0; + $expected_outcome = + "Reduces disk I/O and increases query cache read hits."; + } + elsif ( $var_name eq 'performance_schema' ) { + $id = "performance_schema_enable"; + $topic = "Performance"; + $description = "Performance Schema is disabled."; + $impact_score = 8; + $risk_level = "Medium"; + $risk_description = +"Enabling Performance Schema requires memory allocation and a database restart."; + $requires_restart = 1; + $expected_outcome = +"Enables query performance diagnostics, lock profiling, and telemetry."; + } + elsif ( $var_name eq 'skip-name-resolve' ) { + $id = "skip_name_resolve_enable"; + $topic = "Security"; + $description = +"Database is performing DNS resolution for client connections."; + $impact_score = 7; + $risk_level = "Medium"; + $risk_description = +"Ensure all accounts are configured to connect using IP addresses or wildcard domains before enabling."; + $requires_restart = 1; + $expected_outcome = +"Eliminates connection latency overhead caused by DNS name lookups."; + } + elsif ( $var_name eq 'innodb_file_per_table' ) { + $id = "innodb_file_per_table_enable"; + $topic = "Performance"; + $description = + "InnoDB is storing tables in the system tablespace."; + $impact_score = 8; + $risk_level = "Low"; + $risk_description = +"Only affects newly created tables. Reclaiming space from existing tables requires OPTIMIZE TABLE."; + $requires_restart = 0; + $expected_outcome = +"Allows individual tablespaces to be reclaimed upon dropping or truncating tables."; + } + elsif ( $var_name eq 'long_query_time' ) { + $id = "long_query_time_adjust"; + $topic = "Performance"; + $description = "Slow query log threshold is set too high."; + $impact_score = 6; + $risk_level = "Low"; + $risk_description = +"Might slightly increase disk write I/O if logging many queries."; + $requires_restart = 0; + $expected_outcome = + "Captures fine-grained slow queries for profiling."; + } + elsif ( $var_name eq 'query_cache_size' ) { + $id = "query_cache_size_disable"; + $topic = "Performance"; + $description = +"Query Cache is enabled on a high-concurrency database, causing mutex contention."; + $impact_score = 8; + $risk_level = "Low"; + $risk_description = +"Disables simple query result caching which could increase read latency on low-concurrency read-heavy tables."; + $requires_restart = 0; + $expected_outcome = + "Reduces query cache mutex contention and locking overhead."; + } + elsif ( $var_name eq 'max_connections' ) { + $id = "max_connections_adjust"; + $topic = "Reliability"; + $description = + "Maximum connections limit is set too high or too low."; + $impact_score = 7; + $risk_level = "Medium"; + $risk_description = +"High limits increase potential memory consumption under peak loads."; + $requires_restart = 0; + $expected_outcome = +"Prevents running out of connections or protects against OOM spikes."; + } + + my $stmt = "SET GLOBAL $var_name = $target_val;"; + my $rollback = "SET GLOBAL $var_name = " + . ( $current_val ne '' ? $current_val : $target_val ) . ";"; + + if ($requires_restart) { + $stmt = "$var_name = $target_val"; + $rollback = "$var_name = " + . ( $current_val ne '' ? $current_val : $target_val ); + } + + push @findings, + { + 'id' => $id, + 'topic' => $topic, + 'description' => $description, + 'impact_score' => int($impact_score), + 'risk_level' => $risk_level, + 'risk_description' => $risk_description, + 'requires_restart' => $requires_restart ? \1 : \0, + 'expected_outcome' => $expected_outcome, + 'action' => { + 'type' => $requires_restart ? 'Config' : 'SQL', + 'statement' => $stmt, + 'rollback_statement' => $rollback + } + }; + } + + my $payload = { 'findings' => \@findings }; + + my $json_str = _serialize_to_json($payload); + print $json_str . "\n"; + exit 0; + } + if ( $opt{'json'} && $opt{'yaml'} ) { print STDERR "ERROR: --json and --yaml are mutually exclusive\n"; return 1; @@ -13740,8 +14146,9 @@ sub dump_result { @generalrec; my @repl_recs = - grep { /(replica|sla[v]e|gtid|replication|binlog|relay|flow control|group_replication|router|quorum)/i } - @generalrec; + grep { +/(replica|sla[v]e|gtid|replication|binlog|relay|flow control|group_replication|router|quorum)/i + } @generalrec; my $general_rec_html = join( "\n", @@ -16000,8 +16407,7 @@ sub dump_csv_files { \&get_replication_status, \&process_sysbench_metrics, \&historical_comparison, \&predictive_capacity_analysis, \&check_replication_advanced, \&check_security_2_0, - \&check_workload_traffic, - \&generate_auto_fix_snippets, + \&check_workload_traffic, \&generate_auto_fix_snippets, ); foreach my $section (@REPORT_SECTIONS) { diff --git a/tests/unit_agent_json.t b/tests/unit_agent_json.t new file mode 100644 index 000000000..d052577d4 --- /dev/null +++ b/tests/unit_agent_json.t @@ -0,0 +1,83 @@ +#!/usr/bin/env perl +use strict; +use warnings; +no warnings 'once'; +use Test::More; +use File::Basename; +use File::Spec; +use Cwd 'abs_path'; +use JSON; + +# Override exit to throw an exception that can be caught in tests +BEGIN { + *CORE::GLOBAL::exit = sub { die "EXIT_CALLED\n" }; +} + +my $script_dir = dirname(abs_path(__FILE__)); +my $script = abs_path(File::Spec->catfile($script_dir, '..', 'mysqltuner.pl')); +require $script; + +subtest 'Agent Actionable JSON Schema Verification' => sub { + no warnings 'once'; + + # Populates mock variables and options + @main::adjvars = ( + 'innodb_buffer_pool_size (>= 1G)', + 'performance_schema=ON', + 'query_cache_size (=0)' + ); + + %main::myvar = ( + 'innodb_buffer_pool_size' => '134217728', + 'performance_schema' => 'OFF', + 'query_cache_size' => '16777216' + ); + + %main::opt = ( + 'agent-json' => 1 + ); + + # Capture stdout + my $stdout_content = ''; + open my $oldout, ">&STDOUT" or die "Can't dup STDOUT: $!"; + close STDOUT; + open STDOUT, '>', \$stdout_content or die "Can't redirect STDOUT: $!"; + + # Execute dump_result + eval { main::dump_result(); }; + my $err = $@; + + # Restore stdout + close STDOUT; + open STDOUT, ">&", $oldout or die "Can't dup \$oldout: $!"; + + like($err, qr/EXIT_CALLED/, 'dump_result exited cleanly'); + + # Verify JSON structure + my $data = eval { decode_json($stdout_content) }; + ok(defined $data, 'Stdout is valid JSON'); + ok(exists $data->{findings}, 'JSON contains findings key'); + + my $findings = $data->{findings}; + is(scalar(@$findings), 3, 'Found exactly 3 findings'); + + # 1. Verify innodb_buffer_pool_size + my $ibp = $findings->[0]; + is($ibp->{id}, 'innodb_buffer_pool_size_adjust', 'Correct ID for buffer pool'); + is($ibp->{topic}, 'Performance', 'Correct topic'); + is($ibp->{risk_level}, 'Medium', 'Correct risk level'); + is($ibp->{requires_restart}, JSON::false, 'Requires restart is false'); + is($ibp->{action}->{type}, 'SQL', 'Action type is SQL'); + is($ibp->{action}->{statement}, 'SET GLOBAL innodb_buffer_pool_size = 1G;', 'Correct SQL statement'); + is($ibp->{action}->{rollback_statement}, 'SET GLOBAL innodb_buffer_pool_size = 134217728;', 'Correct rollback SQL statement'); + + # 2. Verify performance_schema + my $pfs = $findings->[1]; + is($pfs->{id}, 'performance_schema_enable', 'Correct ID for performance schema'); + is($pfs->{requires_restart}, JSON::true, 'Requires restart is true'); + is($pfs->{action}->{type}, 'Config', 'Action type is Config'); + is($pfs->{action}->{statement}, 'performance_schema = ON', 'Correct config statement'); + is($pfs->{action}->{rollback_statement}, 'performance_schema = OFF', 'Correct rollback config statement'); +}; + +done_testing(); From 2c961bacd57bd0b0bbd90c7ee1df0beb2b1b6ae2 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:52:36 +0200 Subject: [PATCH 51/97] docs: regenerate release notes --- releases/v2.9.1.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 789df9f77..8b877395f 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -30,6 +30,7 @@ - chore(main): add doc links in localhost warnings and support custom local subdomains - chore(main): hide hostname, ssl, and replication warnings on localhost (#933) - chore(releases): start v2.9.1 branch with version bump and dependency updates +- feat(cli): implement agent-json flag returning structured actionable schema - feat(cli): resolve remote memory, socket override, and temptable sizing issues - feat(container): implement dockerized auditing daemon and zero-dependency MCP server - feat(main): audit InnoDB lock monitors and print all deadlocks configuration @@ -52,6 +53,7 @@ - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 - test(test): add unit tests for potential issues and split unit_coverage_boost3 +- test(test): add unit_agent_json.t to validate actionable JSON schema - test(test): add unit_deadlocks_pfs.t to validate InnoDB deadlock PFS checks - test(test): add unit_eol_warnings.t to validate version support warnings - test(test): add unit_galera_pxc.t to validate Phase 9 Galera cluster checks @@ -87,6 +89,8 @@ ## 🛠️ Internal Commit History +- feat(cli): implement agent-json flag returning structured actionable schema (d71fdbc) +- docs: regenerate release notes (81ae23a) - docs: regenerate release notes (7846c46) - docs: regenerate release notes (89004df) - feat(container): implement dockerized auditing daemon and zero-dependency MCP server (975977c) @@ -139,7 +143,21 @@ ## ⚙️ Technical Evolutions ### ➕ CLI Options Added +- `--action` +- `--agent-json` +- `--description` +- `--expected_outcome` +- `--findings` +- `--id` +- `--impact_score` +- `--requires_restart` +- `--risk_description` +- `--risk_level` +- `--rollback_statement` - `--stage-timings` +- `--statement` +- `--topic` +- `--type` - `--yaml` ## ✅ Laboratory Verification Results From 5d5831fa8d2892054ae737345331432747642bda Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:54:11 +0200 Subject: [PATCH 52/97] docs(roadmap): mark Phase 16 and 17 as completed in ROADMAP.md --- ROADMAP.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index bc321223b..b63079679 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -242,23 +242,23 @@ To ensure consistency and high-density development, the following roles are defi * [x] **Embedded CSV Data Exports**: * [x] Embed base64 or raw string CSV representation of variables and findings in JavaScript, enabling instant local CSV downloads. -### [Phase 16: AI Agent Integration & Actionable JSON Schema](file:///documentation/specifications/roadmap_phase_xv_ai_agent_integration.md) [NOT STARTED] - -* [ ] **Structured Actionable JSON Output**: - * [ ] Implementation of `--agent-json` flag returning a standardized schema. -* [ ] **Expected Outcomes & Rollback Statements**: - * [ ] Each recommendation includes explicit expected outcome description and corresponding rollback statement. -* [ ] **Risk Assessment & Impact Scoring**: - * [ ] Assign deterministic impact score (1-10) and category/risk level to each recommendation. - -### [Phase 17: Dockerized Auditing Daemon & MCP Server Support](file:///documentation/specifications/roadmap_phase_xvi_mcp_server.md) [NOT STARTED] - -* [ ] **Interval Auditing Daemon**: - * [ ] Dockerized execution environment running auditing loops every X hours with caching. -* [ ] **Model Context Protocol (MCP) Server**: - * [ ] Expose caching layer, latest results, and immediate auditing as MCP tools and resources. -* [ ] **Safe execution & Rollbacks**: - * [ ] Implement secure database interaction tools to apply or rollback recommendations. +### [Phase 16: AI Agent Integration & Actionable JSON Schema](file:///documentation/specifications/roadmap_phase_xv_ai_agent_integration.md) [COMPLETED] + +* [x] **Structured Actionable JSON Output**: + * [x] Implementation of `--agent-json` flag returning a standardized schema. +* [x] **Expected Outcomes & Rollback Statements**: + * [x] Each recommendation includes explicit expected outcome description and corresponding rollback statement. +* [x] **Risk Assessment & Impact Scoring**: + * [x] Assign deterministic impact score (1-10) and category/risk level to each recommendation. + +### [Phase 17: Dockerized Auditing Daemon & MCP Server Support](file:///documentation/specifications/roadmap_phase_xvi_mcp_server.md) [COMPLETED] + +* [x] **Interval Auditing Daemon**: + * [x] Dockerized execution environment running auditing loops every X hours with caching. +* [x] **Model Context Protocol (MCP) Server**: + * [x] Expose caching layer, latest results, and immediate auditing as MCP tools and resources. +* [x] **Safe execution & Rollbacks**: + * [x] Implement secure database interaction tools to apply or rollback recommendations. ## 🔮 [Strategic Technical Evolutions](file:///documentation/specifications/strategic_technical_evolutions.md) From 1156c3b65009d2e04f1f1cfca2b1556e286849a9 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:55:22 +0200 Subject: [PATCH 53/97] docs: regenerate release notes --- Changelog | 1 + releases/v2.9.1.md | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Changelog b/Changelog index a8d126d92..f319eeea8 100644 --- a/Changelog +++ b/Changelog @@ -64,6 +64,7 @@ - docs(releases): regenerate release notes and changelog - docs(roadmap): link strategic technical evolutions specification and enforce changelog staging - docs(roadmap): mark Phase 12 as completed in ROADMAP.md +- docs(roadmap): mark Phase 16 and 17 as completed in ROADMAP.md - docs(roadmap): update Phase 7, Phase 9, Phase 10, and Phase 11 status to completed in ROADMAP.md - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: regenerate release notes diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 8b877395f..bfaedf468 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -69,6 +69,7 @@ - docs(releases): regenerate release notes and changelog - docs(roadmap): link strategic technical evolutions specification and enforce changelog staging - docs(roadmap): mark Phase 12 as completed in ROADMAP.md +- docs(roadmap): mark Phase 16 and 17 as completed in ROADMAP.md - docs(roadmap): update Phase 7, Phase 9, Phase 10, and Phase 11 status to completed in ROADMAP.md - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: regenerate release notes @@ -89,6 +90,8 @@ ## 🛠️ Internal Commit History +- docs(roadmap): mark Phase 16 and 17 as completed in ROADMAP.md (5d5831f) +- docs: regenerate release notes (2c961ba) - feat(cli): implement agent-json flag returning structured actionable schema (d71fdbc) - docs: regenerate release notes (81ae23a) - docs: regenerate release notes (7846c46) From 31ec11c354b3af5b5d4f040d56a59163832bcc4a Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:55:22 +0200 Subject: [PATCH 54/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index bfaedf468..c4ead71bd 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -90,6 +90,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (1156c3b) - docs(roadmap): mark Phase 16 and 17 as completed in ROADMAP.md (5d5831f) - docs: regenerate release notes (2c961ba) - feat(cli): implement agent-json flag returning structured actionable schema (d71fdbc) From de35c9c8608544cf8e98ddc9eb5d353f1708f444 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:57:49 +0200 Subject: [PATCH 55/97] docs(roadmap): group strategic technical evolutions into phases 18 to 21 --- ROADMAP.md | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index b63079679..98175a7d4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -262,16 +262,37 @@ To ensure consistency and high-density development, the following roles are defi ## 🔮 [Strategic Technical Evolutions](file:///documentation/specifications/strategic_technical_evolutions.md) -* [ ] Set up a pipeline to automatically audit and verify reference link availability inside the repository documentation to prevent dead links. -* [ ] Integrate standard documentation reference anchors dynamically within MySQLTuner CLI help screens and specific advisor output blocks. -* [ ] Support localized versions of the reference documentation matching other translations of the script (e.g. Italian, French, Russian). -* [ ] **Automated Changelog Formatting Verification**: Implement a Git pre-commit hook that automatically checks if the `Changelog` has been modified when changes of type `feat` or `fix` are detected, preventing commits without changelog documentation. -* [ ] **Containerized Validation Runners**: Standardize local pre-flight checks by executing all verification steps (including unit tests and version consistency checks) inside a standardized, minimal Docker environment to avoid environmental differences between developer environments and CI. -* [ ] **Interactive Release Orchestrator**: Create a script that automates the interactive selection of version bump categories (micro, minor, major), executes the version replacement across all 6 reference locations, and automatically runs the `release_gen.py` script to generate release notes in a single workflow step. -* [ ] **Automated Release Notes Synchronization**: Create a script or Git hook that automatically extracts changes from the branch commits and populates the `Executive Summary` sections in both the `Changelog` and release notes to prevent manual synchronization omissions. -* [ ] **Schema Validation for Release Artifacts**: Implement a CI step to parse and validate that markdown formats, issues referenced, and version definitions in the `releases/` directory are syntactically and logically correct before release tagging. -* [ ] **Structured Roadmap Schema Validation**: Implement a markdown linter or schema validator specifically for the `ROADMAP.md` checklist syntax (verifying correct hyperlinks, file pathways, and category labels). -* [ ] **Automated Status Checklist Sync**: Integrate a workflow script that automatically marks roadmap checklist items as completed (`[x]`) upon detection of related commit scopes (e.g. `feat(auth):` marking authentication items as done). +### [Phase 18: Documentation Integrity & Dynamic References](file:///documentation/specifications/roadmap_phase_xvii_documentation_integrity.md) [NOT STARTED] + +* [ ] **Reference Link Auditing Pipeline**: + * [ ] Set up a pipeline to automatically audit and verify reference link availability inside the repository documentation to prevent dead links. +* [ ] **Dynamic Help Screen Anchors**: + * [ ] Integrate standard documentation reference anchors dynamically within MySQLTuner CLI help screens and specific advisor output blocks. +* [ ] **Localization Support**: + * [ ] Support localized versions of the reference documentation matching other translations of the script (e.g. Italian, French, Russian). + +### [Phase 19: CI/CD Quality Gates & Validation Runners](file:///documentation/specifications/roadmap_phase_xviii_ci_quality_gates.md) [NOT STARTED] + +* [ ] **Automated Changelog Verification**: + * [ ] Implement a Git pre-commit hook that automatically checks if the `Changelog` has been modified when changes of type `feat` or `fix` are detected, preventing commits without changelog documentation. +* [ ] **Containerized Validation Runners**: + * [ ] Standardize local pre-flight checks by executing all verification steps (including unit tests and version consistency checks) inside a standardized, minimal Docker environment to avoid environmental differences between developer environments and CI. +* [ ] **Schema Validation for Release Artifacts**: + * [ ] Implement a CI step to parse and validate that markdown formats, issues referenced, and version definitions in the `releases/` directory are syntactically and logically correct before release tagging. + +### [Phase 20: Release Automation & Synchronization](file:///documentation/specifications/roadmap_phase_xix_release_automation.md) [NOT STARTED] + +* [ ] **Interactive Release Orchestrator**: + * [ ] Create a script that automates the interactive selection of version bump categories (micro, minor, major), executes the version replacement across all 6 reference locations, and automatically runs the `release_gen.py` script to generate release notes in a single workflow step. +* [ ] **Automated Release Notes Synchronization**: + * [ ] Create a script or Git hook that automatically extracts changes from the branch commits and populates the `Executive Summary` sections in both the `Changelog` and release notes to prevent manual synchronization omissions. + +### [Phase 21: Structured Roadmap Automation](file:///documentation/specifications/roadmap_phase_xx_roadmap_automation.md) [NOT STARTED] + +* [ ] **Structured Roadmap Schema Validation**: + * [ ] Implement a markdown linter or schema validator specifically for the `ROADMAP.md` checklist syntax (verifying correct hyperlinks, file pathways, and category labels). +* [ ] **Automated Status Checklist Sync**: + * [ ] Integrate a workflow script that automatically marks roadmap checklist items as completed (`[x]`) upon detection of related commit scopes (e.g. `feat(auth):` marking authentication items as done). ## 🤝 Contribution & Feedback From 67ed38c4bff970aa8725e1cc9b0f3e15c773b985 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:58:59 +0200 Subject: [PATCH 56/97] docs: regenerate release notes --- Changelog | 1 + releases/v2.9.1.md | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Changelog b/Changelog index f319eeea8..6105a3629 100644 --- a/Changelog +++ b/Changelog @@ -62,6 +62,7 @@ - docs(metadata): promote HTML reports and add E2E examples - docs(metadata): remove timestamp from doc-sync generated files - docs(releases): regenerate release notes and changelog +- docs(roadmap): group strategic technical evolutions into phases 18 to 21 - docs(roadmap): link strategic technical evolutions specification and enforce changelog staging - docs(roadmap): mark Phase 12 as completed in ROADMAP.md - docs(roadmap): mark Phase 16 and 17 as completed in ROADMAP.md diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index c4ead71bd..ff6adcc7e 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -67,6 +67,7 @@ - docs(metadata): promote HTML reports and add E2E examples - docs(metadata): remove timestamp from doc-sync generated files - docs(releases): regenerate release notes and changelog +- docs(roadmap): group strategic technical evolutions into phases 18 to 21 - docs(roadmap): link strategic technical evolutions specification and enforce changelog staging - docs(roadmap): mark Phase 12 as completed in ROADMAP.md - docs(roadmap): mark Phase 16 and 17 as completed in ROADMAP.md @@ -90,6 +91,8 @@ ## 🛠️ Internal Commit History +- docs(roadmap): group strategic technical evolutions into phases 18 to 21 (de35c9c) +- docs: regenerate release notes (31ec11c) - docs: regenerate release notes (1156c3b) - docs(roadmap): mark Phase 16 and 17 as completed in ROADMAP.md (5d5831f) - docs: regenerate release notes (2c961ba) From a0338fbe02114e2f1624ccb82109dd13780c588e Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 01:58:59 +0200 Subject: [PATCH 57/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index ff6adcc7e..fefc30ed2 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -91,6 +91,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (67ed38c) - docs(roadmap): group strategic technical evolutions into phases 18 to 21 (de35c9c) - docs: regenerate release notes (31ec11c) - docs: regenerate release notes (1156c3b) From a24b5da35d7128a64393bfbd5df5fca438547279 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:00:49 +0200 Subject: [PATCH 58/97] docs: add AGENT.md integration guide for AI and MCP server --- AGENT.md | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Changelog | 1 + 2 files changed, 156 insertions(+) create mode 100644 AGENT.md diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 000000000..bc77b45b2 --- /dev/null +++ b/AGENT.md @@ -0,0 +1,155 @@ +# AI Agent Integration Guide & Model Context Protocol (MCP) Server + +This document explains how AI coding assistants, autonomous database agents, and developer workflows can utilize MySQLTuner via the **Model Context Protocol (MCP)** or direct `--agent-json` CLI queries to audit and optimize database instances safely. + +--- + +## 🚀 Overview of Integration Modes + +MySQLTuner supports two native modes for machine integration: +1. **Direct CLI (`--agent-json` mode)**: High-speed, zero-dependency JSON output representing actionable recommendations. Excellent for scripts and one-off agent executions. +2. **MCP Server (stdio transport)**: Standardized protocol interface exposing tools and resources for LLM agents to query state, run audits, and execute rollback-enabled database optimizations. + +--- + +## 1. Direct CLI Integration: `--agent-json` + +When running with `--agent-json`, MySQLTuner suppresses all human-readable ANSI terminal outputs and prints a single, structured JSON payload to stdout containing actionable findings. + +### CLI Invocation +```bash +perl mysqltuner.pl --agent-json --host --user --pass +``` + +### Output Schema Definition +Each recommendation returned in the `findings` list adheres to the following structure: + +- **`id`**: Unique deterministic identifier for the recommendation type (e.g., `innodb_buffer_pool_size_adjust`). +- **`topic`**: Category of the finding (`Performance`, `Security`, `Reliability`, `Modeling`, `Replication`). +- **`description`**: Human-readable explanation of the issue. +- **`impact_score`**: Estimated benefit score from `1` (lowest) to `10` (highest). +- **`risk_level`**: Safety assessment associated with applying the recommendation (`Low`, `Medium`, `High`, `Critical`). +- **`risk_description`**: Potential side effects (e.g. table locks, memory consumption). +- **`requires_restart`**: Boolean (`true`/`false`) indicating if the change requires restarting the database service. +- **`expected_outcome`**: Predicted benefit (e.g. "Reduces disk I/O"). +- **`action`**: Object containing: + - `type`: Either `SQL` (can be executed live) or `Config` (requires editing `my.cnf`). + - `statement`: The exact statement to execute or configuration line to write. + - `rollback_statement`: The exact command to revert the action to its original baseline state. + +### Actionable JSON Output Example +```json +{ + "findings": [ + { + "id": "innodb_buffer_pool_size_adjust", + "topic": "Performance", + "description": "InnoDB buffer pool size is under-allocated for the current workload.", + "impact_score": 9, + "risk_level": "Medium", + "risk_description": "Increases memory consumption. Ensure sufficient OS-free RAM to prevent OOM swapping.", + "requires_restart": false, + "expected_outcome": "Reduces disk I/O and increases query cache read hits.", + "action": { + "type": "SQL", + "statement": "SET GLOBAL innodb_buffer_pool_size = 1073741824;", + "rollback_statement": "SET GLOBAL innodb_buffer_pool_size = 134217728;" + } + } + ] +} +``` + +--- + +## 2. Model Context Protocol (MCP) Integration + +The containerized MCP server integrates directly with client systems (like Cursor, Claude Desktop, Antigravity, or custom LangChain/LlamaIndex pipelines). + +### Running the MCP Service Container +Deploy the Docker image next to your database server: +```bash +docker run -d \ + --name mysqltuner-mcp \ + -e DB_HOST=mysql-server \ + -e DB_USER=root \ + -e DB_PASSWORD=secret_pass \ + -e AUDIT_INTERVAL_HOURS=12 \ + -v /var/cache/mysqltuner:/var/cache/mysqltuner \ + mysqltuner-mcp +``` + +### Connecting to the MCP Server +Clients communicate with the server over **stdio** (standard input/output) transport. +Configure your agent IDE config (e.g. `project_config.json` or CLAUDE desktop config) to spawn the server: +```json +{ + "mcpServers": { + "mysqltuner": { + "command": "docker", + "args": ["run", "-i", "--rm", "-e", "DB_HOST=127.0.0.1", "mysqltuner-mcp"] + } + } +} +``` + +--- + +## 🛠️ Exposed MCP Tools & Resources + +### MCP Resources (Observability) +- **`mysqltuner://reports/latest.json`**: Accesses the latest cached JSON report containing findings and DB variables status. +- **`mysqltuner://reports/latest.html`**: Retreives the complete interactive pgBadger-inspired HTML analytics dashboard. + +### MCP Tools (Actions) +Agents can invoke these JSON-RPC methods: + +1. **`get_latest_audit`** + - *Description*: Retrieves cached audit findings instantly without stressing the database. + - *Arguments*: None. + +2. **`run_audit`** + - *Description*: Triggers a fresh run of the Perl engine and returns immediate recommendations. + - *Arguments*: None. + +3. **`apply_recommendation`** + - *Description*: Applies a safe `SQL` adjustment (e.g. `SET GLOBAL`). + - *Arguments*: + - `statement` (string, required): The SQL command to execute. + - `variable_name` (string, optional): Variable name being changed to capture and save the rollback state. + +4. **`rollback_recommendation`** + - *Description*: Reverts a previously executed command using cached transaction state. + - *Arguments*: + - `statement_id` (string, required): The ID of the transaction to revert. + +--- + +## 🧠 Step-by-Step AI Agent Playbook + +An AI agent performing database maintenance should follow this operational path: + +```mermaid +graph TD + A[Start: Read Latest Audit] --> B{Are there findings?} + B -- No --> C[End: Database is Tuned] + B -- Yes --> D[Filter findings by Risk Level] + D --> E[Filter: Risk <= Medium] + E --> F[Present to User: Statement & Rollback] + F --> G{User Confirms?} + G -- No --> H[Skip recommendation] + G -- Yes --> I[Execute apply_recommendation] + I --> J{Performance OK?} + J -- Yes --> K[Log Transaction Success] + J -- No --> L[Execute rollback_recommendation] +``` + +### Agent Prompt Instructions Template (Copy-Paste for LLM Context) +> "You have access to the MySQLTuner MCP server. Your goal is to analyze database performance, propose optimizations, and execute them safely. +> 1. Run `get_latest_audit` to fetch current recommendations. +> 2. For each recommendation, analyze the `impact_score`, `risk_level`, and `requires_restart`. +> 3. Highlight recommendations with high `impact_score` (>=7) and safe `risk_level` (Low or Medium). +> 4. Present these recommendations to the user, showing the `description`, the exact `statement` to apply, and the `rollback_statement`. +> 5. Wait for user consent. +> 6. After confirmation, call `apply_recommendation` to execute the SQL. +> 7. If the user complains of issues or latency spikes, run `rollback_recommendation` immediately with the returned `statement_id` to restore original performance levels." diff --git a/Changelog b/Changelog index 6105a3629..7e3424ffd 100644 --- a/Changelog +++ b/Changelog @@ -67,6 +67,7 @@ - docs(roadmap): mark Phase 12 as completed in ROADMAP.md - docs(roadmap): mark Phase 16 and 17 as completed in ROADMAP.md - docs(roadmap): update Phase 7, Phase 9, Phase 10, and Phase 11 status to completed in ROADMAP.md +- docs: add AGENT.md integration guide for AI and MCP server - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: regenerate release notes - docs: update potential issues log with Phase 9 Galera audit findings From dcc1d0bd9becabc7ff5287106269721334c137d5 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:00:49 +0200 Subject: [PATCH 59/97] docs: regenerate release notes --- releases/v2.9.1.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index fefc30ed2..944ec50dc 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -72,6 +72,7 @@ - docs(roadmap): mark Phase 12 as completed in ROADMAP.md - docs(roadmap): mark Phase 16 and 17 as completed in ROADMAP.md - docs(roadmap): update Phase 7, Phase 9, Phase 10, and Phase 11 status to completed in ROADMAP.md +- docs: add AGENT.md integration guide for AI and MCP server - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: regenerate release notes - docs: update potential issues log with Phase 9 Galera audit findings @@ -91,6 +92,8 @@ ## 🛠️ Internal Commit History +- docs: add AGENT.md integration guide for AI and MCP server (a24b5da) +- docs: regenerate release notes (a0338fb) - docs: regenerate release notes (67ed38c) - docs(roadmap): group strategic technical evolutions into phases 18 to 21 (de35c9c) - docs: regenerate release notes (31ec11c) From f121ab19b3a3db9c6e8bfd5c7ad14bbb2762ddc9 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:02:04 +0200 Subject: [PATCH 60/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 944ec50dc..44adcc83d 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -92,6 +92,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (dcc1d0b) - docs: add AGENT.md integration guide for AI and MCP server (a24b5da) - docs: regenerate release notes (a0338fb) - docs: regenerate release notes (67ed38c) From e14b42700a90dbd6ee048fe7f3b1c2614a433e59 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:02:58 +0200 Subject: [PATCH 61/97] docs: generate FEATURES.md --- FEATURES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/FEATURES.md b/FEATURES.md index 545fc9146..31dfe6b56 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -54,6 +54,7 @@ Features list for option: --feature (dev only) * mysql_triggers * mysql_views * parse_cli_args +* parse_size_bytes * predictive_capacity_analysis * pretty_duration * process_sysbench_metrics From 19d60a7927d2d609a2908936f9d5788547889508 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:03:00 +0200 Subject: [PATCH 62/97] docs: generate end-of-life status files --- mariadb_support.md | 4 +++- mysql_support.md | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/mariadb_support.md b/mariadb_support.md index bedb0fc0b..2f075d3b7 100644 --- a/mariadb_support.md +++ b/mariadb_support.md @@ -3,6 +3,8 @@ | Version | End of Support Date | LTS | Status | |---------|------------------------|-----|--------| | 12.3 | 2029-06-30 | YES | Supported | +| 12.2 | 2026-05-13 | NO | Outdated | +| 12.1 | 2026-02-13 | NO | Outdated | | 12.0 | 2025-11-18 | NO | Outdated | | 11.8 | 2028-06-04 | YES | Supported | | 11.7 | 2025-05-12 | NO | Outdated | @@ -18,7 +20,7 @@ | 10.9 | 2023-08-22 | NO | Outdated | | 10.8 | 2023-05-20 | NO | Outdated | | 10.7 | 2023-02-09 | NO | Outdated | -| 10.6 | 2026-07-06 | YES | Supported | +| 10.6 | 2026-07-06 | YES | Outdated | | 10.5 | 2025-06-24 | YES | Outdated | | 10.4 | 2024-06-18 | YES | Outdated | | 10.3 | 2023-05-25 | NO | Outdated | diff --git a/mysql_support.md b/mysql_support.md index fc9535a03..432baecb0 100644 --- a/mysql_support.md +++ b/mysql_support.md @@ -2,7 +2,9 @@ | Version | End of Support Date | LTS | Status | |---------|------------------------|-----|--------| -| 9.6 | N/A | NO | Supported | +| 9.7 | 2034-04-21 | YES | Supported | +| 9.6 | 2026-04-21 | NO | Outdated | +| 9.5 | 2026-01-20 | NO | Outdated | | 9.4 | 2025-10-21 | NO | Outdated | | 9.3 | 2025-07-22 | NO | Outdated | | 9.2 | 2025-04-15 | NO | Outdated | From b30eefe56781bb3aba4414bc3de30dddddd1d56d Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:06:10 +0200 Subject: [PATCH 63/97] docs: add MCP and AI integration guide for MySQL database tuning --- Changelog | 1 + documentation/mcp_ai_integration_guide.md | 136 ++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 documentation/mcp_ai_integration_guide.md diff --git a/Changelog b/Changelog index 7e3424ffd..736f07084 100644 --- a/Changelog +++ b/Changelog @@ -69,6 +69,7 @@ - docs(roadmap): update Phase 7, Phase 9, Phase 10, and Phase 11 status to completed in ROADMAP.md - docs: add AGENT.md integration guide for AI and MCP server - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart +- docs: add MCP and AI integration guide for MySQL database tuning - docs: regenerate release notes - docs: update potential issues log with Phase 9 Galera audit findings - docs: update potential issues log with Renovate dependency dashboard analysis diff --git a/documentation/mcp_ai_integration_guide.md b/documentation/mcp_ai_integration_guide.md new file mode 100644 index 000000000..e8a94984d --- /dev/null +++ b/documentation/mcp_ai_integration_guide.md @@ -0,0 +1,136 @@ +# AI & MCP Integration Guide for MySQL Optimization + +This guide explains how to set up the Model Context Protocol (MCP) server for MySQLTuner and configure AI agents (e.g. Claude Desktop, Cursor, VS Code Extensions) to run deep, automated performance audits and apply safe, rollback-enabled tuning configurations. + +--- + +## 📦 Part 1: Setting Up the MCP Server + +The MySQLTuner MCP server acts as an intermediary bridge between your database and AI agents, exposing database telemetry and actionable SQL recommendations over standard I/O (stdio). + +### Method A: Dockerized Deployment (Recommended) +This method containerizes the entire toolchain (Perl, Python 3, and mysql client utilities) to ensure compatibility. + +```bash +docker run -d \ + --name mysqltuner-mcp \ + -e DB_HOST=your-database-host \ + -e DB_PORT=3306 \ + -e DB_USER=tuner_user \ + -e DB_PASSWORD=your_password \ + -e AUDIT_INTERVAL_HOURS=6 \ + -v /var/cache/mysqltuner:/var/cache/mysqltuner \ + mysqltuner-mcp +``` + +### Method B: Local Execution (Without Docker) +Ensure Python 3 and Perl are installed locally, then run the script directly: +```bash +export DB_HOST="127.0.0.1" +export DB_USER="root" +export DB_PASSWORD="your_password" +export CACHE_DIR="./mcp_cache" + +python3 build/mcp_server.py +``` + +--- + +## 🛠️ Part 2: Configuring AI Clients + +Once the server is running, register it inside your preferred AI agent environment. + +### 1. Claude Desktop Config +Add the server definition to your Claude Desktop configuration file: +- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` +- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` + +```json +{ + "mcpServers": { + "mysqltuner": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-v", + "/var/cache/mysqltuner:/var/cache/mysqltuner", + "-e", + "DB_HOST=host.docker.internal", + "-e", + "DB_USER=root", + "-e", + "DB_PASSWORD=secret", + "mysqltuner-mcp" + ] + } + } +} +``` + +### 2. Cursor IDE Config +1. Open Cursor and navigate to **Settings** -> **Features** -> **MCP**. +2. Click **+ Add New MCP Server**. +3. Fill in the parameters: + - **Name**: `mysqltuner` + - **Type**: `stdio` + - **Command**: `python3 /path/to/MySQLTuner-perl/build/mcp_server.py` +4. Set environment variables in the terminal where Cursor was launched. + +### 3. VS Code (Cline / Roo Code / Roo Cline) +Configure the extension settings `mcpSettings.json` to spawn the server: +```json +{ + "mcpServers": { + "mysqltuner": { + "command": "python3", + "args": ["/path/to/MySQLTuner-perl/build/mcp_server.py"], + "env": { + "DB_HOST": "127.0.0.1", + "DB_USER": "root", + "DB_PASSWORD": "your_password" + } + } + } +} +``` + +--- + +## 🔍 Part 3: Deep Database Tuning with AI + +When connected, the AI agent has access to MySQLTuner findings and can cross-reference logs, memory allocations, and schema design to perform high-density optimizations. + +### 1. Memory Allocation and Buffers +AI agents can parse the buffer pool allocations and compare them to physical RAM limits to prevent Out-Of-Memory (OOM) situations. +- **Agent Analysis**: Evaluates `pct_max_physical_memory` to verify if memory usage is safe. +- **Live Adjustment**: Executes `apply_recommendation` with `SET GLOBAL innodb_buffer_pool_size = ` if the database version supports dynamic buffer pool resizing (MySQL 5.7+). + +### 2. Connection Saturation and Thread Cache +High connection spikes cause high thread creation overhead. +- **Agent Analysis**: Evaluates `max_connections` and matches it against `threads_created`. +- **Live Adjustment**: Sets `thread_cache_size` to reduce creation overhead: + `SET GLOBAL thread_cache_size = 16;` + +### 3. Index Profiling and Table Churn +- **Agent Analysis**: The agent queries table fragmentation and matches it with Performance Schema query logs. +- **Live Action**: Automatically schedules defragmentation for high-churn tables: + `OPTIMIZE TABLE schema_name.table_name;` + +--- + +## 🤖 Part 4: Advanced Prompt Engineering for AI Agents + +To ensure the AI operates safely and acts as an expert DBA, prepend your conversations with the following System Prompt: + +```markdown +You are a Senior Principal Database Administrator (DBA). You have access to the MySQLTuner MCP server. +Your core mission is to audit, analyze, and optimize the MySQL instance safely. + +### Operating Rules: +1. **Always Verify Baseline**: Before executing any SQL changes, read the cached audit resources (`mysqltuner://reports/latest.json`). +2. **Classify by Risk**: Categorize recommendations. Apply 'Low' or 'Medium' risk adjustments dynamically. Never apply 'High' or 'Critical' recommendations (such as changes requiring a service restart or ALTER TABLE on tables > 10GB) without explicit user confirmation. +3. **Draft Rollbacks First**: Before invoking `apply_recommendation`, state the exact SQL statement to be executed AND the corresponding `rollback_statement` so the user is fully informed. +4. **Iterative Auditing**: After applying a recommendation, trigger `run_audit` to confirm that the indicator has improved. If performance metrics degrade or the audit flags unexpected regressions, immediately run `rollback_recommendation` using the returned Statement ID. +``` From 688261e5356608f273cc6bab5e0d1118d13e3898 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:06:10 +0200 Subject: [PATCH 64/97] docs: regenerate release notes --- releases/v2.9.1.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 44adcc83d..59871b967 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -74,6 +74,7 @@ - docs(roadmap): update Phase 7, Phase 9, Phase 10, and Phase 11 status to completed in ROADMAP.md - docs: add AGENT.md integration guide for AI and MCP server - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart +- docs: add MCP and AI integration guide for MySQL database tuning - docs: regenerate release notes - docs: update potential issues log with Phase 9 Galera audit findings - docs: update potential issues log with Renovate dependency dashboard analysis @@ -92,6 +93,10 @@ ## 🛠️ Internal Commit History +- docs: add MCP and AI integration guide for MySQL database tuning (b30eefe) +- docs: generate end-of-life status files (19d60a7) +- docs: generate FEATURES.md (e14b427) +- docs: regenerate release notes (f121ab1) - docs: regenerate release notes (dcc1d0b) - docs: add AGENT.md integration guide for AI and MCP server (a24b5da) - docs: regenerate release notes (a0338fb) From 1741b75e29d9531380a8ed404b7e9f4c6490479c Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:07:19 +0200 Subject: [PATCH 65/97] docs: regenerate release notes --- Changelog | 2 ++ releases/v2.9.1.md | 3 +++ 2 files changed, 5 insertions(+) diff --git a/Changelog b/Changelog index 736f07084..b58fa4c9d 100644 --- a/Changelog +++ b/Changelog @@ -70,6 +70,8 @@ - docs: add AGENT.md integration guide for AI and MCP server - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: add MCP and AI integration guide for MySQL database tuning +- docs: generate FEATURES.md +- docs: generate end-of-life status files - docs: regenerate release notes - docs: update potential issues log with Phase 9 Galera audit findings - docs: update potential issues log with Renovate dependency dashboard analysis diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 59871b967..98def79f8 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -75,6 +75,8 @@ - docs: add AGENT.md integration guide for AI and MCP server - docs: add LightPath as sponsor, relocate coffee button, and use star-history chart - docs: add MCP and AI integration guide for MySQL database tuning +- docs: generate FEATURES.md +- docs: generate end-of-life status files - docs: regenerate release notes - docs: update potential issues log with Phase 9 Galera audit findings - docs: update potential issues log with Renovate dependency dashboard analysis @@ -93,6 +95,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (688261e) - docs: add MCP and AI integration guide for MySQL database tuning (b30eefe) - docs: generate end-of-life status files (19d60a7) - docs: generate FEATURES.md (e14b427) From 8420a4540ebfe3c35d4b4733b63d04dcf04f12dc Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:07:19 +0200 Subject: [PATCH 66/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 98def79f8..0eef49f7a 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -95,6 +95,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (1741b75) - docs: regenerate release notes (688261e) - docs: add MCP and AI integration guide for MySQL database tuning (b30eefe) - docs: generate end-of-life status files (19d60a7) From bffa416a38c33c44bcec5570db384717c8af3295 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:09:01 +0200 Subject: [PATCH 67/97] docs: link recent features to GitHub issues in Changelog --- Changelog | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Changelog b/Changelog index b58fa4c9d..c08d8dca0 100644 --- a/Changelog +++ b/Changelog @@ -22,12 +22,12 @@ - chore(releases): start v2.9.1 branch with version bump and dependency updates - chore: automated project metadata update - chore: remove execution.log from git repository and sync docs -- feat(cli): implement agent-json flag returning structured actionable schema +- feat(cli): implement agent-json flag returning structured actionable schema (#955) - feat(cli): resolve remote memory, socket override, and temptable sizing issues -- feat(container): implement dockerized auditing daemon and zero-dependency MCP server +- feat(container): implement dockerized auditing daemon and zero-dependency MCP server (#954) - feat(main): audit InnoDB lock monitors and print all deadlocks configuration - feat(main): enhance error log parser with OOM, semaphore waits, file limits, and page corruption alerts -- feat(main): implement advanced log parser and lock monitoring +- feat(main): implement advanced log parser and lock monitoring (#953) - feat(main): implement experimental Correlation Engine linking PFS wait events and CPU load with log events - feat(metadata): fix test badge and update version references in READMEs - feat(report): add pgBadger-inspired query, lock, and temp table visual analytics to HTML report From cf6f35aa3f3d5b9fe78291d386e11916dd606528 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:09:01 +0200 Subject: [PATCH 68/97] docs: regenerate release notes --- releases/v2.9.1.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 0eef49f7a..81451914e 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -30,12 +30,12 @@ - chore(main): add doc links in localhost warnings and support custom local subdomains - chore(main): hide hostname, ssl, and replication warnings on localhost (#933) - chore(releases): start v2.9.1 branch with version bump and dependency updates -- feat(cli): implement agent-json flag returning structured actionable schema +- feat(cli): implement agent-json flag returning structured actionable schema (#955) - feat(cli): resolve remote memory, socket override, and temptable sizing issues -- feat(container): implement dockerized auditing daemon and zero-dependency MCP server +- feat(container): implement dockerized auditing daemon and zero-dependency MCP server (#954) - feat(main): audit InnoDB lock monitors and print all deadlocks configuration - feat(main): enhance error log parser with OOM, semaphore waits, file limits, and page corruption alerts -- feat(main): implement advanced log parser and lock monitoring +- feat(main): implement advanced log parser and lock monitoring (#953) - feat(main): implement experimental Correlation Engine linking PFS wait events and CPU load with log events - feat(metadata): fix test badge and update version references in READMEs - feat(report): add pgBadger-inspired query, lock, and temp table visual analytics to HTML report @@ -95,6 +95,8 @@ ## 🛠️ Internal Commit History +- docs: link recent features to GitHub issues in Changelog (bffa416) +- docs: regenerate release notes (8420a45) - docs: regenerate release notes (1741b75) - docs: regenerate release notes (688261e) - docs: add MCP and AI integration guide for MySQL database tuning (b30eefe) From f20c04e0f6708b74c4dadf70be16b2d94ebf0c92 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:10:09 +0200 Subject: [PATCH 69/97] docs: regenerate release notes --- Changelog | 1 + releases/v2.9.1.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Changelog b/Changelog index c08d8dca0..b70d14ec9 100644 --- a/Changelog +++ b/Changelog @@ -72,6 +72,7 @@ - docs: add MCP and AI integration guide for MySQL database tuning - docs: generate FEATURES.md - docs: generate end-of-life status files +- docs: link recent features to GitHub issues in Changelog - docs: regenerate release notes - docs: update potential issues log with Phase 9 Galera audit findings - docs: update potential issues log with Renovate dependency dashboard analysis diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 81451914e..fa08be209 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -77,6 +77,7 @@ - docs: add MCP and AI integration guide for MySQL database tuning - docs: generate FEATURES.md - docs: generate end-of-life status files +- docs: link recent features to GitHub issues in Changelog - docs: regenerate release notes - docs: update potential issues log with Phase 9 Galera audit findings - docs: update potential issues log with Renovate dependency dashboard analysis @@ -95,6 +96,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (cf6f35a) - docs: link recent features to GitHub issues in Changelog (bffa416) - docs: regenerate release notes (8420a45) - docs: regenerate release notes (1741b75) From adee75bf857e9b9973f39c39c992550e3167c63f Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:10:09 +0200 Subject: [PATCH 70/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index fa08be209..15a094038 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -96,6 +96,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (f20c04e) - docs: regenerate release notes (cf6f35a) - docs: link recent features to GitHub issues in Changelog (bffa416) - docs: regenerate release notes (8420a45) From f2bacfd037f0304d98f7c8f341a493570b41fe4d Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:14:49 +0200 Subject: [PATCH 71/97] docs: link recent features to jmrenouard fork issues in Changelog --- Changelog | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Changelog b/Changelog index b70d14ec9..2e0337926 100644 --- a/Changelog +++ b/Changelog @@ -22,12 +22,12 @@ - chore(releases): start v2.9.1 branch with version bump and dependency updates - chore: automated project metadata update - chore: remove execution.log from git repository and sync docs -- feat(cli): implement agent-json flag returning structured actionable schema (#955) +- feat(cli): implement agent-json flag returning structured actionable schema (#955, jmrenouard#63) - feat(cli): resolve remote memory, socket override, and temptable sizing issues -- feat(container): implement dockerized auditing daemon and zero-dependency MCP server (#954) +- feat(container): implement dockerized auditing daemon and zero-dependency MCP server (#954, jmrenouard#62) - feat(main): audit InnoDB lock monitors and print all deadlocks configuration - feat(main): enhance error log parser with OOM, semaphore waits, file limits, and page corruption alerts -- feat(main): implement advanced log parser and lock monitoring (#953) +- feat(main): implement advanced log parser and lock monitoring (#953, jmrenouard#61) - feat(main): implement experimental Correlation Engine linking PFS wait events and CPU load with log events - feat(metadata): fix test badge and update version references in READMEs - feat(report): add pgBadger-inspired query, lock, and temp table visual analytics to HTML report From 11895cb10417067e5287b090cf7a3eee543ea99f Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:14:49 +0200 Subject: [PATCH 72/97] docs: regenerate release notes --- releases/v2.9.1.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 15a094038..eea988f4d 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -30,12 +30,12 @@ - chore(main): add doc links in localhost warnings and support custom local subdomains - chore(main): hide hostname, ssl, and replication warnings on localhost (#933) - chore(releases): start v2.9.1 branch with version bump and dependency updates -- feat(cli): implement agent-json flag returning structured actionable schema (#955) +- feat(cli): implement agent-json flag returning structured actionable schema (#955, jmrenouard#63) - feat(cli): resolve remote memory, socket override, and temptable sizing issues -- feat(container): implement dockerized auditing daemon and zero-dependency MCP server (#954) +- feat(container): implement dockerized auditing daemon and zero-dependency MCP server (#954, jmrenouard#62) - feat(main): audit InnoDB lock monitors and print all deadlocks configuration - feat(main): enhance error log parser with OOM, semaphore waits, file limits, and page corruption alerts -- feat(main): implement advanced log parser and lock monitoring (#953) +- feat(main): implement advanced log parser and lock monitoring (#953, jmrenouard#61) - feat(main): implement experimental Correlation Engine linking PFS wait events and CPU load with log events - feat(metadata): fix test badge and update version references in READMEs - feat(report): add pgBadger-inspired query, lock, and temp table visual analytics to HTML report @@ -96,6 +96,8 @@ ## 🛠️ Internal Commit History +- docs: link recent features to jmrenouard fork issues in Changelog (f2bacfd) +- docs: regenerate release notes (adee75b) - docs: regenerate release notes (f20c04e) - docs: regenerate release notes (cf6f35a) - docs: link recent features to GitHub issues in Changelog (bffa416) From 3f41b8ae28ddc3033ecf179bb00219052a41dad3 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:15:58 +0200 Subject: [PATCH 73/97] docs: regenerate release notes --- Changelog | 1 + releases/v2.9.1.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Changelog b/Changelog index 2e0337926..ddb24b5f6 100644 --- a/Changelog +++ b/Changelog @@ -73,6 +73,7 @@ - docs: generate FEATURES.md - docs: generate end-of-life status files - docs: link recent features to GitHub issues in Changelog +- docs: link recent features to jmrenouard fork issues in Changelog - docs: regenerate release notes - docs: update potential issues log with Phase 9 Galera audit findings - docs: update potential issues log with Renovate dependency dashboard analysis diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index eea988f4d..94eda76a9 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -78,6 +78,7 @@ - docs: generate FEATURES.md - docs: generate end-of-life status files - docs: link recent features to GitHub issues in Changelog +- docs: link recent features to jmrenouard fork issues in Changelog - docs: regenerate release notes - docs: update potential issues log with Phase 9 Galera audit findings - docs: update potential issues log with Renovate dependency dashboard analysis @@ -96,6 +97,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (11895cb) - docs: link recent features to jmrenouard fork issues in Changelog (f2bacfd) - docs: regenerate release notes (adee75b) - docs: regenerate release notes (f20c04e) From 682abca76289d5035463ec7e675d1c703deafe02 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:15:58 +0200 Subject: [PATCH 74/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 94eda76a9..b83bdb12e 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -97,6 +97,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (3f41b8a) - docs: regenerate release notes (11895cb) - docs: link recent features to jmrenouard fork issues in Changelog (f2bacfd) - docs: regenerate release notes (adee75b) From 372751e84819e6ef55d82c727039d4760eaea1aa Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:16:59 +0200 Subject: [PATCH 75/97] docs(roadmap): add Phase 22 for High Availability & Replication Auto-Discovery --- ROADMAP.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index 98175a7d4..266721ee5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -294,6 +294,17 @@ To ensure consistency and high-density development, the following roles are defi * [ ] **Automated Status Checklist Sync**: * [ ] Integrate a workflow script that automatically marks roadmap checklist items as completed (`[x]`) upon detection of related commit scopes (e.g. `feat(auth):` marking authentication items as done). +### [Phase 22: High Availability & Replication Auto-Discovery](file:///documentation/specifications/roadmap_phase_xxi_replication_autodiscovery.md) [NOT STARTED] + +* [ ] **Topology Auto-Discovery**: + * [ ] Query MySQL system tables and variables to automatically identify the topology (Galera Cluster, InnoDB Cluster, or Logical Replication source/replica). +* [ ] **Galera Member Exploration**: + * [ ] Discover all active cluster members from `wsrep_incoming_addresses` and support launching auditing runs on replica nodes. +* [ ] **Logical Replica Lag Auditing**: + * [ ] Track source-replica status, check lag metrics, and audit IO/SQL thread parameters on replicas. +* [ ] **InnoDB Cluster Auditing**: + * [ ] Query `mysql_innodb_cluster_metadata` to retrieve cluster members status and performance schema metrics. + ## 🤝 Contribution & Feedback We welcome community feedback on this roadmap. If you have specific feature requests or want to contribute to a specific phase, please open an issue on our [GitHub repository](https://github.com/jmrenouard/MySQLTuner-perl). From 938b2ed0b5dd94f013fbbc128713a63345511aa7 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:17:14 +0200 Subject: [PATCH 76/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index b83bdb12e..c3eafdb14 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -97,6 +97,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (682abca) - docs: regenerate release notes (3f41b8a) - docs: regenerate release notes (11895cb) - docs: link recent features to jmrenouard fork issues in Changelog (f2bacfd) From 08fa6a8e8d9857550dc842087865c5c39347dda2 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:18:22 +0200 Subject: [PATCH 77/97] docs: regenerate release notes --- Changelog | 1 + releases/v2.9.1.md | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Changelog b/Changelog index ddb24b5f6..320cc0af1 100644 --- a/Changelog +++ b/Changelog @@ -62,6 +62,7 @@ - docs(metadata): promote HTML reports and add E2E examples - docs(metadata): remove timestamp from doc-sync generated files - docs(releases): regenerate release notes and changelog +- docs(roadmap): add Phase 22 for High Availability & Replication Auto-Discovery - docs(roadmap): group strategic technical evolutions into phases 18 to 21 - docs(roadmap): link strategic technical evolutions specification and enforce changelog staging - docs(roadmap): mark Phase 12 as completed in ROADMAP.md diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index c3eafdb14..ac5af9a36 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -67,6 +67,7 @@ - docs(metadata): promote HTML reports and add E2E examples - docs(metadata): remove timestamp from doc-sync generated files - docs(releases): regenerate release notes and changelog +- docs(roadmap): add Phase 22 for High Availability & Replication Auto-Discovery - docs(roadmap): group strategic technical evolutions into phases 18 to 21 - docs(roadmap): link strategic technical evolutions specification and enforce changelog staging - docs(roadmap): mark Phase 12 as completed in ROADMAP.md @@ -97,6 +98,8 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (938b2ed) +- docs(roadmap): add Phase 22 for High Availability & Replication Auto-Discovery (372751e) - docs: regenerate release notes (682abca) - docs: regenerate release notes (3f41b8a) - docs: regenerate release notes (11895cb) From 0a06930e06d3b48a8db40464f53ad374166f8e84 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:18:22 +0200 Subject: [PATCH 78/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index ac5af9a36..d5b376d57 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -98,6 +98,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (08fa6a8) - docs: regenerate release notes (938b2ed) - docs(roadmap): add Phase 22 for High Availability & Replication Auto-Discovery (372751e) - docs: regenerate release notes (682abca) From 28b50b3227d2c8abb3b0eea7916b324dde376a75 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:31:52 +0200 Subject: [PATCH 79/97] fix(main): query events_errors_summary_global_by_error safely (#956, jmrenouard#64) --- Changelog | 1 + ROADMAP.md | 9 +++++++++ mysqltuner.pl | 17 +++++++++++------ tests/unit_deadlocks_pfs.t | 9 ++++++++- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/Changelog b/Changelog index 320cc0af1..8f014c60d 100644 --- a/Changelog +++ b/Changelog @@ -43,6 +43,7 @@ - fix(main): calculate query cache efficiency using Com_select on MariaDB (MDEV-4981) - fix(main): exclude MariaDB user roles and support zero-config TLS - fix(main): only recommend increasing innodb_log_buffer_size when log waits occur +- fix(main): query events_errors_summary_global_by_error safely (#956, jmrenouard#64) - fix: update documentation and code - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 diff --git a/ROADMAP.md b/ROADMAP.md index 266721ee5..5780438af 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -305,6 +305,15 @@ To ensure consistency and high-density development, the following roles are defi * [ ] **InnoDB Cluster Auditing**: * [ ] Query `mysql_innodb_cluster_metadata` to retrieve cluster members status and performance schema metrics. +### [Phase 23: E2E Quality and Query Safety Hardening](file:///documentation/specifications/roadmap_phase_xxii_query_safety.md) [NOT STARTED] + +* [ ] **Performance Schema Pre-Flight Checks**: + * [ ] Dynamically verify Performance Schema table availability in `information_schema.tables` before querying to prevent exit failures. +* [ ] **Horizontal Multi-Scenario Comparative HTML Report**: + * [ ] Extend the HTML dashboard with a side-by-side comparative table showing metric differences between Standard, Container, and Dumpdir modes. +* [ ] **Trace Logging for SQL Compilation Errors**: + * [ ] Capture and redirect SQL execution errors to a dedicated debug log rather than silent deletion to assist DBAs in diagnosing permission restrictions. + ## 🤝 Contribution & Feedback We welcome community feedback on this roadmap. If you have specific feature requests or want to contribute to a specific phase, please open an issue on our [GitHub repository](https://github.com/jmrenouard/MySQLTuner-perl). diff --git a/mysqltuner.pl b/mysqltuner.pl index 0cb65e7a7..e2d3277d0 100755 --- a/mysqltuner.pl +++ b/mysqltuner.pl @@ -12386,15 +12386,20 @@ sub mysql_innodb { # Task 8: Deadlock & Contention Analytics via Performance Schema if ( ( $myvar{'performance_schema'} // 'OFF' ) eq 'ON' ) { if ( $is_mysql && mysql_version_ge( 8, 0 ) ) { - my @err_res = select_array( -"SELECT SUM(COUNT_STAR) FROM performance_schema.events_errors_summary_global_by_error WHERE ERROR_NUMBER = 1213" + my $has_events_errors = select_one( +"SELECT 1 FROM information_schema.tables WHERE table_schema='performance_schema' AND table_name='events_errors_summary_global_by_error' LIMIT 1" ); - if ( @err_res && defined $err_res[0] && $err_res[0] > 0 ) { - badprint + if ($has_events_errors) { + my @err_res = select_array( +"SELECT SUM(COUNT_STAR) FROM performance_schema.events_errors_summary_global_by_error WHERE ERROR_NUMBER = 1213" + ); + if ( @err_res && defined $err_res[0] && $err_res[0] > 0 ) { + badprint "InnoDB experienced $err_res[0] lock deadlocks (ER_LOCK_DEADLOCK)"; - push( @generalrec, + push( @generalrec, "Optimize application queries, transaction lengths, and index coverage to reduce lock deadlocks." - ); + ); + } } } } diff --git a/tests/unit_deadlocks_pfs.t b/tests/unit_deadlocks_pfs.t index ded4c2021..f7f835d84 100644 --- a/tests/unit_deadlocks_pfs.t +++ b/tests/unit_deadlocks_pfs.t @@ -28,7 +28,14 @@ subtest 'InnoDB Lock Deadlocks PFS Audit' => sub { # Populate the version globals main::validate_mysql_version(); - # Mock select_array to return correct values + # Mock select_array and select_one to return correct values + local *main::select_one = sub { + my $sql = shift; + if ($sql =~ /events_errors_summary_global_by_error/i) { + return 1; + } + return 0; + }; local *main::select_array = sub { my $sql = shift; if ($sql =~ /SHOW ENGINE PERFORMANCE_SCHEMA STATUS/i) { From 07193c5a71485a532279269e3f0604beaf60191d Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:31:52 +0200 Subject: [PATCH 80/97] docs: regenerate release notes --- releases/v2.9.1.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index d5b376d57..bf2b4a562 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -50,6 +50,7 @@ - fix(main): calculate query cache efficiency using Com_select on MariaDB (MDEV-4981) - fix(main): exclude MariaDB user roles and support zero-config TLS - fix(main): only recommend increasing innodb_log_buffer_size when log waits occur +- fix(main): query events_errors_summary_global_by_error safely (#956, jmrenouard#64) - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 - test(test): add unit tests for potential issues and split unit_coverage_boost3 @@ -98,6 +99,8 @@ ## 🛠️ Internal Commit History +- fix(main): query events_errors_summary_global_by_error safely (#956, jmrenouard#64) (28b50b3) +- docs: regenerate release notes (0a06930) - docs: regenerate release notes (08fa6a8) - docs: regenerate release notes (938b2ed) - docs(roadmap): add Phase 22 for High Availability & Replication Auto-Discovery (372751e) From 18d1da0754803da42c0443901c2848b0941e9420 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Thu, 9 Jul 2026 02:34:53 +0200 Subject: [PATCH 81/97] test(test): verify replication terminology and checksums in unit tests (#888) --- tests/unit_replication_internals.t | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/unit_replication_internals.t b/tests/unit_replication_internals.t index 22d8d963d..b2cee64b4 100644 --- a/tests/unit_replication_internals.t +++ b/tests/unit_replication_internals.t @@ -118,4 +118,34 @@ subtest 'Semi-sync wait point check' => sub { ok(grep(/Set rpl_semi_sync_source_wait_point = AFTER_SYNC/, @main::generalrec), 'Recommends setting AFTER_SYNC wait point'); }; +# Task 8: Replication Checksums and Terminology verification +subtest 'Replication Checksums & Terminology' => sub { + @main::generalrec = (); + MySQLTuner::TestHelper::reset_state(); + $main::is_local_only = 0; + + # Set legacy variables to trigger warnings + $main::myvar{'master_verify_checksum'} = 'OFF'; + $main::myvar{'slave_sql_verify_checksum'} = 'OFF'; + $main::myvar{'slave_skip_verify_binlog_checksum'} = 'ON'; + + main::check_replication_advanced(); + + # Verify recommendations are made using clean terms + ok(grep(/Enable source_verify_checksum/, @main::generalrec), 'Recommends source_verify_checksum instead of master_verify_checksum'); + ok(grep(/Enable replica_sql_verify_checksum/, @main::generalrec), 'Recommends replica_sql_verify_checksum instead of slave_sql_verify_checksum'); + ok(grep(/Disable replica_skip_verify_binlog_checksum/, @main::generalrec), 'Recommends replica_skip_verify_binlog_checksum instead of slave'); + + # Double check no raw 'master' or 'slave' in the output recommendations + foreach my $rec (@main::generalrec) { + # Exclude legacy variable names in parentheses: e.g. "Enable source_verify_checksum = ON (or master_verify_checksum)." + my $clean_rec = $rec; + $clean_rec =~ s/\((?:or )?master_verify_checksum\)//g; + $clean_rec =~ s/\((?:or )?slave_sql_verify_checksum\)//g; + $clean_rec =~ s/slave_skip_verify_binlog_checksum//g; + + like($clean_rec, qr/^(?!.*\b(?:master|slave)\b).*$/i, "No standalone 'master' or 'slave' in recommendation: $rec"); + } +}; + done_testing(); From 2999bad0f159a5a16ba2abb547fd5e63a17da39d Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Fri, 10 Jul 2026 00:57:40 +0200 Subject: [PATCH 82/97] test(lab): add HA topology E2E tests, MCP server E2E test, and dedicated output analyzer - Add build/test_ha.sh E2E orchestrator for Galera, InnoDB Cluster, and Replication topologies - Add build/analyze_mt_output.pl dedicated output analyzer with 8 analysis categories - Add HA validation profiles (galera.json, innodb_cluster.json, replication.json) - Add tests/e2e_mcp_server.t for MCP server E2E validation with live MariaDB container - Add Makefile targets: test-ha, test-ha-galera, test-ha-innodb, test-ha-repli, test-mcp-e2e - Add ROADMAP phases 24-30 for project normalization and build stack rationalization - Update Changelog and release notes --- Changelog | 8 + Makefile | 30 +++ ROADMAP.md | 68 ++++++ build/analyze_mt_output.pl | 301 ++++++++++++++++++++++++++ build/ha_profiles/galera.json | 29 +++ build/ha_profiles/innodb_cluster.json | 28 +++ build/ha_profiles/replication.json | 29 +++ build/test_ha.sh | 273 +++++++++++++++++++++++ releases/v2.9.1.md | 10 + tests/e2e_mcp_server.t | 156 +++++++++++++ 10 files changed, 932 insertions(+) create mode 100755 build/analyze_mt_output.pl create mode 100644 build/ha_profiles/galera.json create mode 100644 build/ha_profiles/innodb_cluster.json create mode 100644 build/ha_profiles/replication.json create mode 100755 build/test_ha.sh create mode 100644 tests/e2e_mcp_server.t diff --git a/Changelog b/Changelog index 8f014c60d..9739c6820 100644 --- a/Changelog +++ b/Changelog @@ -44,9 +44,11 @@ - fix(main): exclude MariaDB user roles and support zero-config TLS - fix(main): only recommend increasing innodb_log_buffer_size when log waits occur - fix(main): query events_errors_summary_global_by_error safely (#956, jmrenouard#64) +- fix(main): support '0' and 'OFF' value representations when checking slow query log status (#517) - fix: update documentation and code - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 +- test(lab): expand unit tests for issue #517 to verify alternative representations of slow query log active state - test(test): add unit tests for potential issues and split unit_coverage_boost3 - test(test): add unit_agent_json.t to validate actionable JSON schema - test(test): add unit_deadlocks_pfs.t to validate InnoDB deadlock PFS checks @@ -59,6 +61,10 @@ - test(test): add unit_version_helpers.t to validate version caching and helpers - test(test): add unit_workload_traffic.t to validate Phase 11 workload profiling - test(test): split unit_coverage_boost3.t into three smaller test files +- test(lab): add e2e_mcp_server.t for MCP server E2E validation with live MariaDB container +- test(lab): add build/test_ha.sh E2E orchestrator for Galera, InnoDB Cluster, and Replication topologies +- test(lab): add build/analyze_mt_output.pl dedicated output analyzer with HA profile support +- test(lab): add HA validation profiles (galera.json, innodb_cluster.json, replication.json) - ci: optimize pre-commit hook to only run unit tests when code, tests, or dependencies are modified - docs(metadata): promote HTML reports and add E2E examples - docs(metadata): remove timestamp from doc-sync generated files @@ -81,6 +87,8 @@ - docs: update potential issues log with Renovate dependency dashboard analysis - docs: update potential issues log with release v2.9.0 and v2.9.1 audit results - docs: update repository links to major/MySQLTuner-perl and add GitHub stars badge +- docs(roadmap): add Phase 24 (Boolean Normalization), Phase 25 (Deprecated Variables), Phase 26 (Subtest Decomposition) to ROADMAP.md +- docs(roadmap): add Phase 27 (Multi-Language Normalization), Phase 28 (CI Version Matrix), Phase 29 (Publish Pipeline), Phase 30 (Build Stack Rationalization) from transversal audit 2.9.0 2026-07-03 diff --git a/Makefile b/Makefile index 547ad394d..199dfad04 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,12 @@ help: @echo " unit-tests-debug: Run unit tests with verbose debug information" @echo " clean_examples: Cleanup examples directory (KEEP=n, default 5)" @echo " setup_commits: Install Conventional Commits tools (Node.js)" + @echo " test-ha: Run E2E tests on all HA topologies (Galera, InnoDB Cluster, Replication)" + @echo " test-ha-galera: Run E2E tests on Galera Cluster only" + @echo " test-ha-innodb: Run E2E tests on InnoDB Cluster only" + @echo " test-ha-repli: Run E2E tests on Replication only" + @echo " test-mcp-e2e: Run MCP Server E2E tests with a real database" + @echo " analyze-output: Analyze MySQLTuner output (FILE=path/to/output.txt)" installdep_debian: setup_commits @@ -186,6 +192,30 @@ audit-logs: @echo "Running laboratory logs audit..." perl build/audit_logs.pl --dir=examples --verbose +test-ha: vendor_setup + @echo "Running MySQLTuner HA E2E Tests (all topologies)..." + bash build/test_ha.sh all + +test-ha-galera: vendor_setup + @echo "Running MySQLTuner HA E2E Tests (Galera)..." + bash build/test_ha.sh galera + +test-ha-innodb: vendor_setup + @echo "Running MySQLTuner HA E2E Tests (InnoDB Cluster)..." + bash build/test_ha.sh innodb + +test-ha-repli: vendor_setup + @echo "Running MySQLTuner HA E2E Tests (Replication)..." + bash build/test_ha.sh repli + +test-mcp-e2e: + @echo "Running MCP Server E2E Tests..." + prove -v tests/e2e_mcp_server.t + +analyze-output: + @echo "Analyzing MySQLTuner output..." + perl build/analyze_mt_output.pl $(FILE) + unit-tests: @echo "Running unit and regression tests..." perl ./build/audit_tests.pl diff --git a/ROADMAP.md b/ROADMAP.md index 5780438af..7738c9b9b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -314,6 +314,74 @@ To ensure consistency and high-density development, the following roles are defi * [ ] **Trace Logging for SQL Compilation Errors**: * [ ] Capture and redirect SQL execution errors to a dedicated debug log rather than silent deletion to assist DBAs in diagnosing permission restrictions. +### Phase 24: MySQL Boolean Normalization Engine [NOT STARTED] + +* [ ] **System-Wide Boolean Normalization**: + * [ ] Create an internal utility function to convert and normalize system variable boolean representations (`ON`/`OFF`, `1`/`0`, `YES`/`NO`) to simplify all current and future conditional logic in `mysqltuner.pl`. + +### Phase 25: Deprecated System Variables & Synonyms Audit [NOT STARTED] + +* [ ] **Obsolete Configuration Warnings**: + * [ ] Add specific diagnostic warnings when obsolete synonyms (e.g. `log_slow_queries`) are configured instead of the modern recommended variables (e.g. `slow_query_log`). + +### Phase 26: Subtest Decomposition & Test Suite Optimization [NOT STARTED] + +* [ ] **Granular Unit Test Decomposition**: + * [ ] Continue decomposing monolithic test scripts in the `tests/` directory into structured, human-assimilable subtests to simplify regression tracking and database laboratory debugging. + +### Phase 27: Multi-Language Normalization & Duplicate Elimination [NOT STARTED] + +> Addresses the 6 cross-language duplications identified during the transversal project audit (Perl/Python/Bash/YAML). + +* [ ] **CVE Update Consolidation (Perl-Only)**: + * [ ] Merge enriched fields from `updateCVElist.py` (CVSS scores, references, publication dates) into `updateCVElist.pl`. + * [ ] Deprecate and remove `updateCVElist.py` and its `__pycache__/` directory after migration validation. +* [ ] **Centralized Version Extraction Script**: + * [ ] Create a single `build/get_version.sh` script encapsulating the version extraction logic (`grep '- Version ' mysqltuner.pl | awk '{ print $NF}'`) currently duplicated in 5 locations (Makefile, 2 workflows, 1 test script). + * [ ] Refactor Makefile, `publish_release.yml`, `docker_publish.yml`, and `tests/check_release_files.sh` to source this single script. +* [ ] **Orphan File Cleanup**: + * [ ] Remove empty `JenkinsFile` (0 bytes, no pipeline defined). + * [ ] Remove `mysqltuner.pl.bak` and `tests/unit_versions.t.bak` (unversioned backup files). + +### Phase 28: CI/CD Version Matrix Harmonization [NOT STARTED] + +> Resolves critical discrepancies where CI workflows test exclusively EOL database versions while ignoring supported ones. + +* [ ] **Centralized CI Version Matrix**: + * [ ] Create a machine-readable matrix file (`build/ci_matrix.json`) defining supported DB versions for CI, consumed by all GitHub Actions workflows via a reusable workflow or composite action. +* [ ] **Obsolete Workflow Updates**: + * [ ] Update `generate_mariadb_examples.yml` to target supported versions (10.11, 11.4, 11.8, 12.3) instead of exclusively EOL versions (10.2→10.9). + * [ ] Update `generate_mysql_examples.yml` to target supported versions (8.4, 9.7) instead of exclusively EOL versions (5.6, 5.7, 8.0). + * [ ] Update `pull_request.yml` to test at least one supported MySQL (8.4) and one supported MariaDB (11.4) version alongside legacy versions. +* [ ] **Automated Matrix Synchronization**: + * [ ] Extend `lts_autobump.pl` to automatically update the CI version matrix in tandem with `mysqltuner.pl` and test suite updates. + +### Phase 29: Publish Pipeline Unification [NOT STARTED] + +> Eliminates duplication between local and CI publish flows, and harmonizes pre-publish validation. + +* [ ] **Unified Pre-Publish Validation Script**: + * [ ] Factor the pre-publish validation logic (critical file checks, release notes existence, tag/version consistency) into a single reusable script `build/validate_release.sh`. + * [ ] Refactor `docker_publish.yml` and `publish_release.yml` to call this shared script instead of embedding inline validation. + * [ ] Harmonize the critical file lists (currently divergent between the two workflows). +* [ ] **Local Docker Publish Deprecation**: + * [ ] Mark `publishtodockerhub.sh` as deprecated in favor of the `docker_publish.yml` workflow (which includes Buildx, multi-arch, and full validation). + * [ ] Update `Makefile` `docker_push` target to warn about deprecation and recommend using the CI workflow. + +### Phase 30: Build Stack Rationalization [NOT STARTED] + +> Simplifies the multi-language build toolchain toward Perl-first consistency with the project's zero-dependency philosophy. + +* [ ] **Release Notes Generator Migration (Python → Perl)**: + * [ ] Rewrite `release_gen.py` (347 lines) in Perl using Core modules only, eliminating the Python 3 runtime dependency from the build stack. + * [ ] Preserve all current features: changelog parsing, git commit grouping, diagnostic growth indicators, and CLI option delta analysis. +* [ ] **Features Generator Migration (Bash → Perl)**: + * [ ] Rewrite `genFeatures.sh` (currently a `grep | perl | sort | perl | grep` pipeline) as a pure Perl script to eliminate the shell dependency. +* [ ] **Build Script Header Standardization**: + * [ ] Standardize all `build/` script headers with a common format including: description, author, dependencies, usage, and exit codes. +* [ ] **EOL Script Consolidation**: + * [ ] Merge `endoflife.sh` (Bash + curl + jq) functionality into `sync_eol_dates.pl` (already uses HTTP::Tiny), eliminating the `jq` external dependency. + ## 🤝 Contribution & Feedback We welcome community feedback on this roadmap. If you have specific feature requests or want to contribute to a specific phase, please open an issue on our [GitHub repository](https://github.com/jmrenouard/MySQLTuner-perl). diff --git a/build/analyze_mt_output.pl b/build/analyze_mt_output.pl new file mode 100755 index 000000000..5d5ef587a --- /dev/null +++ b/build/analyze_mt_output.pl @@ -0,0 +1,301 @@ +#!/usr/bin/env perl +# =========================================================================== +# Script: analyze_mt_output.pl +# Description: Dedicated MySQLTuner output analyzer for E2E test validation. +# Detects errors, warnings, missing sections, and HA diagnostics. +# Author: Jean-Marie Renouard & Antigravity +# Usage: perl build/analyze_mt_output.pl [--profile ] +# Exit Codes: 0 = OK, 1 = WARNINGS detected, 2 = ERRORS detected +# =========================================================================== +use strict; +use warnings; +use Getopt::Long; +use JSON; +use File::Basename; + +my $profile_file = ''; +my $json_output = 0; +my $quiet = 0; + +GetOptions( + 'profile=s' => \$profile_file, + 'json' => \$json_output, + 'quiet' => \$quiet, +) or die "Usage: $0 [--profile ] [--json] [--quiet] \n"; + +my $output_file = shift @ARGV + or die "Usage: $0 [--profile ] [--json] [--quiet] \n"; + +die "File not found: $output_file\n" unless -f $output_file; + +# Load output content +open my $fh, '<', $output_file or die "Cannot open $output_file: $!\n"; +my $content = do { local $/; <$fh> }; +close $fh; + +# Load profile if provided +my $profile = {}; +if ($profile_file && -f $profile_file) { + open my $pf, '<', $profile_file or die "Cannot open profile $profile_file: $!\n"; + my $json_text = do { local $/; <$pf> }; + close $pf; + $profile = decode_json($json_text); +} + +# Result accumulators +my @errors; +my @warnings; +my @sections_found; +my @sections_missing; +my @diagnostics_found; +my @diagnostics_missing; + +# =================================================================== +# Category 1: Perl Warnings +# =================================================================== +my @perl_warnings; +while ($content =~ /^(.*(?:Use of uninitialized value|uninitialized value|deprecated).*)$/gmi) { + my $line = $1; + # Skip false positives from MySQLTuner output about DB deprecation + next if $line =~ /✔|✘|\[OK\]|\[!!?\]|uses DEPRECATED|uses DISABLED|DEPRECATED auth/i; + push @perl_warnings, $line; +} +if (@perl_warnings) { + push @errors, { + category => 'Perl Warnings', + severity => 'ERROR', + count => scalar @perl_warnings, + details => [ map { substr($_, 0, 200) } @perl_warnings[0 .. ($#perl_warnings > 4 ? 4 : $#perl_warnings)] ], + }; +} + +# =================================================================== +# Category 2: SQL Execution Failures +# =================================================================== +my @sql_failures; +while ($content =~ /^(.*FAIL Execute SQL.*)$/gmi) { + push @sql_failures, $1; +} +if (@sql_failures) { + push @errors, { + category => 'SQL Execution Failures', + severity => 'ERROR', + count => scalar @sql_failures, + details => [ map { substr($_, 0, 200) } @sql_failures[0 .. ($#sql_failures > 4 ? 4 : $#sql_failures)] ], + }; +} + +# =================================================================== +# Category 3: Transport / Connection Errors +# =================================================================== +my @conn_errors; +while ($content =~ /^(.*(?:Can't connect to|Access denied|Connection refused|timeout|Lost connection).*)$/gmi) { + push @conn_errors, $1; +} +if (@conn_errors) { + push @errors, { + category => 'Transport/Connection Errors', + severity => 'ERROR', + count => scalar @conn_errors, + details => [ map { substr($_, 0, 200) } @conn_errors[0 .. ($#conn_errors > 4 ? 4 : $#conn_errors)] ], + }; +} + +# =================================================================== +# Category 4: Incomplete Execution +# =================================================================== +unless ($content =~ /Terminated successfully/i) { + push @errors, { + category => 'Incomplete Execution', + severity => 'ERROR', + count => 1, + details => ['Missing "Terminated successfully" marker in output'], + }; +} + +# =================================================================== +# Category 5: Performance Schema Disabled +# =================================================================== +if ($content =~ /Performance_schema should be activated/i) { + push @warnings, { + category => 'Performance Schema Disabled', + severity => 'WARNING', + count => 1, + details => ['Performance Schema is disabled; some diagnostics may be incomplete'], + }; +} + +# =================================================================== +# Category 6: Standard Sections Detection +# =================================================================== +my @standard_sections = ( + 'General Statistics', + 'Storage Engine Statistics', + 'Performance Metrics', + 'Security Recommendations', +); + +for my $section (@standard_sections) { + if ($content =~ /\Q$section\E/i) { + push @sections_found, $section; + } else { + push @sections_missing, $section; + } +} + +if (@sections_missing) { + push @warnings, { + category => 'Missing Standard Sections', + severity => 'WARNING', + count => scalar @sections_missing, + details => \@sections_missing, + }; +} + +# =================================================================== +# Category 7: Profile-Based Validation (HA Diagnostics) +# =================================================================== +if ($profile && $profile->{required_sections}) { + for my $section (@{ $profile->{required_sections} }) { + if ($content =~ /\Q$section\E/i) { + push @sections_found, "HA:$section"; + } else { + push @sections_missing, "HA:$section"; + push @warnings, { + category => "Missing HA Section: $section", + severity => 'WARNING', + count => 1, + details => ["Expected HA section '$section' not found in output for topology '$profile->{topology}'"], + }; + } + } +} + +if ($profile && $profile->{required_patterns}) { + for my $pattern (@{ $profile->{required_patterns} }) { + if ($content =~ /\Q$pattern\E/i) { + push @diagnostics_found, $pattern; + } else { + push @diagnostics_missing, $pattern; + } + } + if (@diagnostics_missing) { + push @warnings, { + category => 'Missing HA Diagnostic Patterns', + severity => 'WARNING', + count => scalar @diagnostics_missing, + details => \@diagnostics_missing, + }; + } +} + +if ($profile && $profile->{expected_diagnostics}) { + for my $diag (@{ $profile->{expected_diagnostics} }) { + if ($content =~ /\Q$diag\E/i) { + push @diagnostics_found, "expected:$diag"; + } + } +} + +if ($profile && $profile->{forbidden_patterns}) { + for my $forbidden (@{ $profile->{forbidden_patterns} }) { + my @matches; + while ($content =~ /^(.*\Q$forbidden\E.*)$/gmi) { + push @matches, $1; + } + if (@matches) { + push @errors, { + category => "Forbidden Pattern: $forbidden", + severity => 'ERROR', + count => scalar @matches, + details => [ map { substr($_, 0, 200) } @matches[0 .. ($#matches > 2 ? 2 : $#matches)] ], + }; + } + } +} + +# =================================================================== +# Category 8: Empty / Silent Output +# =================================================================== +my $line_count = () = $content =~ /\n/g; +if ($line_count < 10) { + push @warnings, { + category => 'Suspiciously Short Output', + severity => 'WARNING', + count => 1, + details => ["Output contains only $line_count lines, expected >50"], + }; +} + +# =================================================================== +# Result Compilation +# =================================================================== +my $exit_code = 0; +$exit_code = 1 if @warnings; +$exit_code = 2 if @errors; + +my $result = { + file => basename($output_file), + profile => $profile->{topology} || 'standalone', + exit_code => $exit_code, + verdict => $exit_code == 0 ? 'PASS' : ($exit_code == 1 ? 'WARNING' : 'ERROR'), + error_count => scalar @errors, + warning_count => scalar @warnings, + errors => \@errors, + warnings => \@warnings, + sections_found => \@sections_found, + sections_missing => \@sections_missing, + diagnostics_found => \@diagnostics_found, + diagnostics_missing => \@diagnostics_missing, + output_lines => $line_count, +}; + +# =================================================================== +# Output +# =================================================================== +if ($json_output) { + print encode_json($result) . "\n"; +} elsif (!$quiet) { + my $icon = $exit_code == 0 ? '✅' : ($exit_code == 1 ? '⚠️' : '❌'); + print "=" x 60 . "\n"; + print "$icon MySQLTuner Output Analysis: $result->{verdict}\n"; + print "=" x 60 . "\n"; + printf " File: %s\n", $result->{file}; + printf " Profile: %s\n", $result->{profile}; + printf " Lines: %d\n", $result->{output_lines}; + printf " Errors: %d\n", $result->{error_count}; + printf " Warnings: %d\n", $result->{warning_count}; + printf " Sections: %d found, %d missing\n", scalar @sections_found, scalar @sections_missing; + printf " HA Diags: %d found, %d missing\n", scalar @diagnostics_found, scalar @diagnostics_missing; + print "-" x 60 . "\n"; + + if (@errors) { + print "\n🔴 ERRORS:\n"; + for my $err (@errors) { + printf " [%s] %s (count: %d)\n", $err->{severity}, $err->{category}, $err->{count}; + for my $d (@{ $err->{details} }) { + printf " → %s\n", $d; + } + } + } + + if (@warnings) { + print "\n🟡 WARNINGS:\n"; + for my $w (@warnings) { + printf " [%s] %s (count: %d)\n", $w->{severity}, $w->{category}, $w->{count}; + for my $d (@{ $w->{details} }) { + printf " → %s\n", $d; + } + } + } + + if (@diagnostics_found) { + print "\n🟢 HA DIAGNOSTICS FOUND:\n"; + for my $d (@diagnostics_found) { + printf " ✔ %s\n", $d; + } + } + print "\n"; +} + +exit $exit_code; diff --git a/build/ha_profiles/galera.json b/build/ha_profiles/galera.json new file mode 100644 index 000000000..4c285a6cc --- /dev/null +++ b/build/ha_profiles/galera.json @@ -0,0 +1,29 @@ +{ + "topology": "galera", + "display_name": "MariaDB Galera Cluster", + "required_sections": [ + "Galera Cluster", + "wsrep" + ], + "required_patterns": [ + "wsrep_cluster_size", + "wsrep_ready", + "wsrep_local_state_comment", + "wsrep_connected" + ], + "forbidden_patterns": [ + "Use of uninitialized value", + "FAIL Execute SQL", + "Can't connect to" + ], + "expected_diagnostics": [ + "wsrep_flow_control", + "wsrep_local_bf_aborts", + "gcache", + "Galera" + ], + "ports": [3511, 3512, 3513], + "startup_command": "up-galera", + "shutdown_command": "down-galera", + "inject_command": "inject-employee-galera" +} diff --git a/build/ha_profiles/innodb_cluster.json b/build/ha_profiles/innodb_cluster.json new file mode 100644 index 000000000..8391a402c --- /dev/null +++ b/build/ha_profiles/innodb_cluster.json @@ -0,0 +1,28 @@ +{ + "topology": "innodb_cluster", + "display_name": "MySQL InnoDB Cluster (Group Replication)", + "required_sections": [ + "Group Replication", + "Replication" + ], + "required_patterns": [ + "group_replication", + "MEMBER_STATE", + "MEMBER_ROLE" + ], + "forbidden_patterns": [ + "Use of uninitialized value", + "FAIL Execute SQL", + "Can't connect to" + ], + "expected_diagnostics": [ + "group_replication_flow_control", + "COUNT_TRANSACTIONS_IN_QUEUE", + "replication_group_members", + "certification" + ], + "ports": [4411, 4412, 4413], + "startup_command": "innodb-up", + "shutdown_command": "innodb-down", + "inject_command": "inject-data service=mysql_node1 db=employees" +} diff --git a/build/ha_profiles/replication.json b/build/ha_profiles/replication.json new file mode 100644 index 000000000..45d0c5fb8 --- /dev/null +++ b/build/ha_profiles/replication.json @@ -0,0 +1,29 @@ +{ + "topology": "replication", + "display_name": "MariaDB Source-Replica Replication", + "required_sections": [ + "Replication", + "Slave" + ], + "required_patterns": [ + "Slave_IO_Running", + "Slave_SQL_Running", + "Seconds_Behind_Master" + ], + "forbidden_patterns": [ + "Use of uninitialized value", + "FAIL Execute SQL", + "Can't connect to" + ], + "expected_diagnostics": [ + "replication lag", + "binlog", + "relay_log", + "gtid", + "slave_parallel" + ], + "ports": [3411, 3412, 3413], + "startup_command": "up-repli", + "shutdown_command": "down-repli", + "inject_command": "inject-employee-repli" +} diff --git a/build/test_ha.sh b/build/test_ha.sh new file mode 100755 index 000000000..557c6d865 --- /dev/null +++ b/build/test_ha.sh @@ -0,0 +1,273 @@ +#!/bin/bash +# =========================================================================== +# Script: test_ha.sh +# Description: E2E test suite for MySQLTuner against High Availability +# topologies provided by multi-db-docker-env. +# Author: Jean-Marie Renouard & Antigravity +# Usage: bash build/test_ha.sh [galera|innodb|repli|all] +# Dependencies: Docker, multi-db-docker-env (cloned via vendor/) +# =========================================================================== +set -euo pipefail + +# Configuration +PROJECT_ROOT=$(pwd) +VENDOR_DIR="$PROJECT_ROOT/vendor" +MULTI_DB_DIR="$VENDOR_DIR/multi-db-docker-env" +MULTI_DB_REPO="https://github.com/jmrenouard/multi-db-docker-env" +EXAMPLES_DIR="$PROJECT_ROOT/examples" +PROFILES_DIR="$PROJECT_ROOT/build/ha_profiles" +ANALYZER="$PROJECT_ROOT/build/analyze_mt_output.pl" +CVE_FILE="$PROJECT_ROOT/vulnerabilities.csv" +DATE_TAG=$(date +%Y%m%d_%H%M%S) +DB_PASS="${DB_ROOT_PASSWORD:-mysqltuner_test}" + +# Topologies to test (default: all) +TOPOS="${1:-all}" + +PASS_TOTAL=0 +FAIL_TOTAL=0 +WARN_TOTAL=0 + +log_step() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +} + +log_header() { + echo "======================================================================" + echo " MySQLTuner HA E2E Test - $1 - $(date)" + echo "======================================================================" +} + +# Setup vendor repositories +setup_vendor() { + log_step "Setting up vendor repositories..." + mkdir -p "$VENDOR_DIR" + if [ ! -d "$MULTI_DB_DIR" ]; then + git clone "$MULTI_DB_REPO" "$MULTI_DB_DIR" + else + (cd "$MULTI_DB_DIR" && git pull --ff-only 2>/dev/null || true) + fi + + # Ensure .env exists + if [ ! -f "$MULTI_DB_DIR/.env" ]; then + echo "DB_ROOT_PASSWORD=$DB_PASS" > "$MULTI_DB_DIR/.env" + fi +} + +# Wait for a MySQL port to be ready +wait_for_port() { + local port=$1 + local max_wait=${2:-120} + local count=0 + log_step "Waiting for port $port to be ready (max ${max_wait}s)..." + until mysqladmin -h 127.0.0.1 -P "$port" -u root -p"$DB_PASS" ping >/dev/null 2>&1; do + sleep 2 + count=$((count + 2)) + if [ $count -ge $max_wait ]; then + log_step "ERROR: Timeout waiting for port $port" + return 1 + fi + done + log_step "Port $port is ready (${count}s)." + return 0 +} + +# Run MySQLTuner against a specific port and capture output +run_mysqltuner_on_port() { + local port=$1 + local target_dir=$2 + local node_label=$3 + local profile_file=$4 + + local output_file="$target_dir/${node_label}_output.txt" + local report_file="$target_dir/${node_label}_report.html" + + log_step "Running MySQLTuner on $node_label (port $port)..." + + local mt_args="--host 127.0.0.1 --port $port --user root --pass $DB_PASS" + mt_args="$mt_args --verbose --forcemem 256" + [ -f "$CVE_FILE" ] && mt_args="$mt_args --cvefile $CVE_FILE" + mt_args="$mt_args --reportfile $report_file" + + local start_time=$(date +%s) + perl "$PROJECT_ROOT/mysqltuner.pl" $mt_args > "$output_file" 2>&1 || true + local end_time=$(date +%s) + local duration=$((end_time - start_time)) + + log_step "MySQLTuner finished on $node_label in ${duration}s" + + # Run analyzer + log_step "Analyzing output for $node_label..." + local analyzer_args="$output_file" + [ -n "$profile_file" ] && [ -f "$profile_file" ] && analyzer_args="--profile $profile_file $analyzer_args" + + local analyze_exit=0 + perl "$ANALYZER" $analyzer_args 2>&1 | tee "$target_dir/${node_label}_analysis.txt" || analyze_exit=$? + + # Also produce JSON + perl "$ANALYZER" --json $analyzer_args > "$target_dir/${node_label}_analysis.json" 2>/dev/null || true + + return $analyze_exit +} + +# Run a complete HA topology test +run_ha_test() { + local topo=$1 + local profile_file="$PROFILES_DIR/${topo}.json" + + if [ ! -f "$profile_file" ]; then + log_step "ERROR: Profile not found: $profile_file" + FAIL_TOTAL=$((FAIL_TOTAL + 1)) + return 1 + fi + + # Parse profile with perl (Core module JSON) + local display_name + display_name=$(perl -MJSON -e 'local $/; open my $f, "<", shift; print decode_json(<$f>)->{display_name}' "$profile_file") + local startup_cmd + startup_cmd=$(perl -MJSON -e 'local $/; open my $f, "<", shift; print decode_json(<$f>)->{startup_command}' "$profile_file") + local shutdown_cmd + shutdown_cmd=$(perl -MJSON -e 'local $/; open my $f, "<", shift; print decode_json(<$f>)->{shutdown_command}' "$profile_file") + local ports_json + ports_json=$(perl -MJSON -e 'local $/; open my $f, "<", shift; print encode_json(decode_json(<$f>)->{ports})' "$profile_file") + local inject_cmd + inject_cmd=$(perl -MJSON -e 'local $/; open my $f, "<", shift; print decode_json(<$f>)->{inject_command}' "$profile_file") + + log_header "$display_name" + + local target_dir="$EXAMPLES_DIR/${DATE_TAG}_ha_${topo}" + mkdir -p "$target_dir" + + # Navigate to multi-db-docker-env + cd "$MULTI_DB_DIR" || return 1 + + # Start topology + log_step "Starting $display_name via 'make $startup_cmd'..." + make "$startup_cmd" > "$target_dir/docker_start.log" 2>&1 || { + log_step "CRITICAL: Failed to start $display_name" + cat "$target_dir/docker_start.log" + FAIL_TOTAL=$((FAIL_TOTAL + 1)) + cd "$PROJECT_ROOT" + return 1 + } + + # Wait for all ports + local ports + ports=$(echo "$ports_json" | perl -MJSON -e 'local $/; my $a = decode_json(); print join(" ", @$a)') + local all_ready=true + for port in $ports; do + if ! wait_for_port "$port" 120; then + all_ready=false + break + fi + done + + if [ "$all_ready" = false ]; then + log_step "ERROR: Not all ports ready for $display_name" + docker compose -f "docker-compose-${topo}.yml" logs > "$target_dir/container_logs.log" 2>&1 || true + make "$shutdown_cmd" > /dev/null 2>&1 || true + FAIL_TOTAL=$((FAIL_TOTAL + 1)) + cd "$PROJECT_ROOT" + return 1 + fi + + # Inject data + if [ -n "$inject_cmd" ]; then + log_step "Injecting test data via 'make $inject_cmd'..." + make $inject_cmd > "$target_dir/db_injection.log" 2>&1 || { + log_step "WARNING: Data injection failed (non-fatal)" + } + fi + + # Capture container logs + docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" > "$target_dir/container_status.txt" 2>/dev/null || true + + # Return to project root + cd "$PROJECT_ROOT" + + # Run MySQLTuner on each node + local node_idx=0 + local topo_errors=0 + for port in $ports; do + node_idx=$((node_idx + 1)) + local node_label="node${node_idx}_port${port}" + local exit_code=0 + run_mysqltuner_on_port "$port" "$target_dir" "$node_label" "$profile_file" || exit_code=$? + + case $exit_code in + 0) PASS_TOTAL=$((PASS_TOTAL + 1)) ;; + 1) WARN_TOTAL=$((WARN_TOTAL + 1)) ;; + *) + FAIL_TOTAL=$((FAIL_TOTAL + 1)) + topo_errors=$((topo_errors + 1)) + ;; + esac + done + + # Shutdown topology + cd "$MULTI_DB_DIR" + log_step "Shutting down $display_name via 'make $shutdown_cmd'..." + make "$shutdown_cmd" > "$target_dir/docker_shutdown.log" 2>&1 || true + cd "$PROJECT_ROOT" + + # Generate consolidated summary + log_step "Generating summary for $display_name..." + { + echo "# HA E2E Test Summary: $display_name" + echo "**Date:** $(date)" + echo "**Topology:** $topo" + echo "**Nodes tested:** $node_idx" + echo "**Errors:** $topo_errors" + echo "" + echo "## Node Results" + for f in "$target_dir"/*_analysis.txt; do + [ -f "$f" ] && echo "### $(basename "$f" _analysis.txt)" && cat "$f" && echo "" + done + } > "$target_dir/summary.md" + + if [ $topo_errors -eq 0 ]; then + log_step "✅ $display_name: ALL NODES PASSED" + else + log_step "❌ $display_name: $topo_errors NODE(S) FAILED" + fi + + return $topo_errors +} + +# ===================================================================== +# Main Execution +# ===================================================================== +setup_vendor + +case "$TOPOS" in + galera) + run_ha_test "galera" + ;; + innodb|innodb_cluster) + run_ha_test "innodb_cluster" + ;; + repli|replication) + run_ha_test "replication" + ;; + all) + run_ha_test "galera" || true + run_ha_test "innodb_cluster" || true + run_ha_test "replication" || true + ;; + *) + echo "Usage: $0 [galera|innodb|repli|all]" + exit 1 + ;; +esac + +echo "" +echo "======================================================================" +echo " HA E2E Test Complete" +echo " PASS: $PASS_TOTAL | WARN: $WARN_TOTAL | FAIL: $FAIL_TOTAL" +echo " Reports: $EXAMPLES_DIR" +echo "======================================================================" + +if [ $FAIL_TOTAL -gt 0 ]; then + exit 1 +fi +exit 0 diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index bf2b4a562..9e8ffffc4 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -51,8 +51,10 @@ - fix(main): exclude MariaDB user roles and support zero-config TLS - fix(main): only recommend increasing innodb_log_buffer_size when log waits occur - fix(main): query events_errors_summary_global_by_error safely (#956, jmrenouard#64) +- fix(main): support '0' and 'OFF' value representations when checking slow query log status (#517) - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 +- test(lab): expand unit tests for issue #517 to verify alternative representations of slow query log active state - test(test): add unit tests for potential issues and split unit_coverage_boost3 - test(test): add unit_agent_json.t to validate actionable JSON schema - test(test): add unit_deadlocks_pfs.t to validate InnoDB deadlock PFS checks @@ -65,6 +67,10 @@ - test(test): add unit_version_helpers.t to validate version caching and helpers - test(test): add unit_workload_traffic.t to validate Phase 11 workload profiling - test(test): split unit_coverage_boost3.t into three smaller test files +- test(lab): add e2e_mcp_server.t for MCP server E2E validation with live MariaDB container +- test(lab): add build/test_ha.sh E2E orchestrator for Galera, InnoDB Cluster, and Replication topologies +- test(lab): add build/analyze_mt_output.pl dedicated output analyzer with HA profile support +- test(lab): add HA validation profiles (galera.json, innodb_cluster.json, replication.json) - docs(metadata): promote HTML reports and add E2E examples - docs(metadata): remove timestamp from doc-sync generated files - docs(releases): regenerate release notes and changelog @@ -86,6 +92,8 @@ - docs: update potential issues log with Renovate dependency dashboard analysis - docs: update potential issues log with release v2.9.0 and v2.9.1 audit results - docs: update repository links to major/MySQLTuner-perl and add GitHub stars badge +- docs(roadmap): add Phase 24 (Boolean Normalization), Phase 25 (Deprecated Variables), Phase 26 (Subtest Decomposition) to ROADMAP.md +- docs(roadmap): add Phase 27 (Multi-Language Normalization), Phase 28 (CI Version Matrix), Phase 29 (Publish Pipeline), Phase 30 (Build Stack Rationalization) from transversal audit ``` ## 📈 Diagnostic Growth Indicators @@ -99,6 +107,8 @@ ## 🛠️ Internal Commit History +- test(test): verify replication terminology and checksums in unit tests (#888) (18d1da0) +- docs: regenerate release notes (07193c5) - fix(main): query events_errors_summary_global_by_error safely (#956, jmrenouard#64) (28b50b3) - docs: regenerate release notes (0a06930) - docs: regenerate release notes (08fa6a8) diff --git a/tests/e2e_mcp_server.t b/tests/e2e_mcp_server.t new file mode 100644 index 000000000..42cb7f6af --- /dev/null +++ b/tests/e2e_mcp_server.t @@ -0,0 +1,156 @@ +#!/usr/bin/env perl +# =========================================================================== +# Test: e2e_mcp_server.t +# Description: E2E test for the MCP server against a real MariaDB container. +# Validates JSON-RPC protocol with live database interaction. +# Dependencies: Docker, Python3 +# =========================================================================== +use strict; +use warnings; +use Test::More; +use FindBin; +use IPC::Open2; +use File::Path qw(make_path remove_tree); + +# --- Pre-flight checks --- +my $has_docker = system("docker info >/dev/null 2>&1") == 0; +my $has_python = system("which python3 >/dev/null 2>&1") == 0; + +unless ($has_docker && $has_python) { + plan skip_all => "Docker and Python3 are required for MCP E2E tests" + . ($has_docker ? "" : " (Docker unavailable)") + . ($has_python ? "" : " (Python3 unavailable)"); +} + +plan tests => 2; + +my $DB_PASS = "mcp_test_pass"; +my $DB_PORT = 13306; # Non-standard port to avoid conflicts +my $CONTAINER = "mysqltuner_mcp_e2e_$$"; +my $MCP_SCRIPT = "$FindBin::Bin/../build/mcp_server.py"; +my $MT_SCRIPT = "$FindBin::Bin/../mysqltuner.pl"; +my $CACHE_DIR = "$FindBin::Bin/mcp_e2e_cache_$$"; + +# --- Helper: cleanup --- +sub cleanup { + system("docker rm -f $CONTAINER >/dev/null 2>&1"); + remove_tree($CACHE_DIR) if -d $CACHE_DIR; +} + +# Ensure cleanup on exit +END { cleanup(); } + +# --- Start MariaDB container --- +subtest 'MCP E2E: Database Container Lifecycle' => sub { + plan tests => 3; + + # Start container + my $docker_cmd = "docker run -d --name $CONTAINER " + . "-e MARIADB_ROOT_PASSWORD=$DB_PASS " + . "-p $DB_PORT:3306 " + . "mariadb:11.4"; + my $cid = `$docker_cmd 2>&1`; + chomp $cid; + ok(length($cid) > 10, "MariaDB container started: " . substr($cid, 0, 12)); + + # Wait for readiness (max 60s) + my $ready = 0; + for my $i (1..30) { + my $ping = system("docker exec $CONTAINER mariadb -uroot -p$DB_PASS -e 'SELECT 1' >/dev/null 2>&1"); + if ($ping == 0) { + $ready = 1; + last; + } + sleep 2; + } + ok($ready, "MariaDB is accepting connections"); + + # Verify version + my $version = `docker exec $CONTAINER mariadb -uroot -p$DB_PASS -sNe "SELECT VERSION();" 2>/dev/null`; + chomp $version; + like($version, qr/11\.4/, "MariaDB version is 11.4.x: $version"); +}; + +# --- MCP Server JSON-RPC E2E --- +subtest 'MCP E2E: JSON-RPC Tools with Live Database' => sub { + plan tests => 8; + + make_path($CACHE_DIR); + + # Set environment for MCP server + local $ENV{'DB_HOST'} = '127.0.0.1'; + local $ENV{'DB_PORT'} = $DB_PORT; + local $ENV{'DB_USER'} = 'root'; + local $ENV{'DB_PASSWORD'} = $DB_PASS; + local $ENV{'CACHE_DIR'} = $CACHE_DIR; + local $ENV{'MYSQLTUNER_PL'} = $MT_SCRIPT; + local $ENV{'READ_ONLY'} = 'true'; + + my ($chld_out, $chld_in); + my $pid = eval { + open2($chld_out, $chld_in, "python3", $MCP_SCRIPT); + }; + unless ($pid) { + fail("Failed to spawn mcp_server.py: $@"); + return; + } + + # Enable autoflush + my $old_fh = select($chld_in); + $| = 1; + select($old_fh); + + # Helper to send JSON-RPC and get response + my $send_rpc = sub { + my ($method, $id, $params) = @_; + my $req = "{\"jsonrpc\": \"2.0\", \"method\": \"$method\", \"id\": \"$id\""; + $req .= ", \"params\": $params" if $params; + $req .= "}\n"; + print $chld_in $req; + my $resp = <$chld_out>; + return $resp || ''; + }; + + # 1. Initialize + my $init_resp = $send_rpc->('initialize', 'e2e-1'); + like($init_resp, qr/"jsonrpc"\s*:\s*"2.0"/, 'Initialize: valid JSON-RPC 2.0'); + like($init_resp, qr/"name"\s*:\s*"mysqltuner-mcp"/, 'Initialize: server name correct'); + + # 2. List tools + my $tools_resp = $send_rpc->('tools/list', 'e2e-2'); + like($tools_resp, qr/run_audit/, 'Tools list: contains run_audit'); + like($tools_resp, qr/get_latest_audit/, 'Tools list: contains get_latest_audit'); + + # 3. Get latest audit (should be empty initially) + my $get_resp = $send_rpc->('tools/call', 'e2e-3', + '{"name": "get_latest_audit", "arguments": {}}'); + like($get_resp, qr/No cached audit|content/, 'Get latest audit: returns expected response'); + + # 4. Run audit (live database) + # This may take 10-30s for MySQLTuner to run + diag("Running live audit (this may take 30s)..."); + local $SIG{ALRM} = sub { fail("Audit timed out after 120s"); }; + alarm(120); + my $audit_resp = $send_rpc->('tools/call', 'e2e-4', + '{"name": "run_audit", "arguments": {}}'); + alarm(0); + + # Audit should return content (not an error) + ok(length($audit_resp) > 100, 'Run audit: received substantial response (' . length($audit_resp) . ' bytes)'); + + # 5. Apply recommendation (should be rejected in read-only mode) + my $apply_resp = $send_rpc->('tools/call', 'e2e-5', + '{"name": "apply_recommendation", "arguments": {"statement": "SET GLOBAL slow_query_log = ON"}}'); + like($apply_resp, qr/read-only|isError/i, 'Apply recommendation: rejected in read-only mode'); + + # 6. List resources + my $res_resp = $send_rpc->('resources/list', 'e2e-6'); + like($res_resp, qr/mysqltuner:\/\/reports\/latest\.json/, 'Resources list: contains latest report'); + + # Cleanup + close $chld_in; + close $chld_out; + waitpid($pid, 0); +}; + +done_testing(); From bb4ae9164688ef2fd5d5e59f872b8576c937aca2 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Fri, 10 Jul 2026 00:57:40 +0200 Subject: [PATCH 83/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 9e8ffffc4..d51b01895 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -107,6 +107,7 @@ ## 🛠️ Internal Commit History +- test(lab): add HA topology E2E tests, MCP server E2E test, and dedicated output analyzer (2999bad) - test(test): verify replication terminology and checksums in unit tests (#888) (18d1da0) - docs: regenerate release notes (07193c5) - fix(main): query events_errors_summary_global_by_error safely (#956, jmrenouard#64) (28b50b3) From b9f4083f97c11e31fccc7d98f8b3c036be94c607 Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Fri, 10 Jul 2026 01:09:08 +0200 Subject: [PATCH 84/97] fix(main): support '0' and 'OFF' value representations when checking slow query log status (#517) - Allow slow_query_log and log_slow_queries to be checked against '0' to detect disabled state - Add subtests in tests/test_issue_517.t to cover '0' and '1' representations for both variables --- Changelog | 2 +- mysqltuner.pl | 2 +- releases/v2.9.1.md | 3 +- tests/test_issue_517.t | 88 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 3 deletions(-) diff --git a/Changelog b/Changelog index 9739c6820..30246ae44 100644 --- a/Changelog +++ b/Changelog @@ -44,7 +44,7 @@ - fix(main): exclude MariaDB user roles and support zero-config TLS - fix(main): only recommend increasing innodb_log_buffer_size when log waits occur - fix(main): query events_errors_summary_global_by_error safely (#956, jmrenouard#64) -- fix(main): support '0' and 'OFF' value representations when checking slow query log status (#517) +- fix(main): support '0' and 'OFF' value representations when checking slow query log status (#517) (finalized implementation) - fix: update documentation and code - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 diff --git a/mysqltuner.pl b/mysqltuner.pl index e2d3277d0..6cfd72c9a 100755 --- a/mysqltuner.pl +++ b/mysqltuner.pl @@ -7758,7 +7758,7 @@ sub mysql_stats { my $slow_query_log_active = $myvar{'slow_query_log'} // $myvar{'log_slow_queries'}; if ( defined($slow_query_log_active) ) { - if ( $slow_query_log_active eq "OFF" ) { + if ( $slow_query_log_active eq "OFF" || $slow_query_log_active eq "0" ) { push( @generalrec, "Enable the slow query log to troubleshoot bad queries" ); } diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index d51b01895..7bb80630a 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -51,7 +51,7 @@ - fix(main): exclude MariaDB user roles and support zero-config TLS - fix(main): only recommend increasing innodb_log_buffer_size when log waits occur - fix(main): query events_errors_summary_global_by_error safely (#956, jmrenouard#64) -- fix(main): support '0' and 'OFF' value representations when checking slow query log status (#517) +- fix(main): support '0' and 'OFF' value representations when checking slow query log status (#517) (finalized implementation) - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 - test(lab): expand unit tests for issue #517 to verify alternative representations of slow query log active state @@ -107,6 +107,7 @@ ## 🛠️ Internal Commit History +- docs: regenerate release notes (bb4ae91) - test(lab): add HA topology E2E tests, MCP server E2E test, and dedicated output analyzer (2999bad) - test(test): verify replication terminology and checksums in unit tests (#888) (18d1da0) - docs: regenerate release notes (07193c5) diff --git a/tests/test_issue_517.t b/tests/test_issue_517.t index 014c935e3..b9983adae 100644 --- a/tests/test_issue_517.t +++ b/tests/test_issue_517.t @@ -165,4 +165,92 @@ subtest 'log_slow_queries fallback is OFF' => sub { ok($found, 'Should recommend enabling slow query log when log_slow_queries fallback is OFF'); }; +subtest 'slow_query_log is 0' => sub { + @main::generalrec = (); + @main::adjvars = (); + $main::physical_memory = 32 * 1024 * 1024 * 1024; + $main::swap_memory = 4 * 1024 * 1024 * 1024; + + %main::myvar = ( + get_base_mock_vars(), + slow_query_log => '0', + ); + %main::mystat = ( + get_base_mock_stats(), + ); + %main::mycalc = (); + + eval { main::calculations(); main::mysql_stats(); }; + ok(!$@, 'calculations() did not crash') or diag("Crashed: $@"); + + my $found = grep { /Enable the slow query log/ } @main::generalrec; + ok($found, 'Should recommend enabling slow query log when slow_query_log is 0'); +}; + +subtest 'slow_query_log is 1' => sub { + @main::generalrec = (); + @main::adjvars = (); + $main::physical_memory = 32 * 1024 * 1024 * 1024; + $main::swap_memory = 4 * 1024 * 1024 * 1024; + + %main::myvar = ( + get_base_mock_vars(), + slow_query_log => '1', + ); + %main::mystat = ( + get_base_mock_stats(), + ); + %main::mycalc = (); + + eval { main::calculations(); main::mysql_stats(); }; + ok(!$@, 'calculations() did not crash') or diag("Crashed: $@"); + + my $found = grep { /Enable the slow query log/ } @main::generalrec; + ok(!$found, 'Should not recommend enabling slow query log when slow_query_log is 1'); +}; + +subtest 'log_slow_queries fallback is 0' => sub { + @main::generalrec = (); + @main::adjvars = (); + $main::physical_memory = 32 * 1024 * 1024 * 1024; + $main::swap_memory = 4 * 1024 * 1024 * 1024; + + %main::myvar = ( + get_base_mock_vars(), + log_slow_queries => '0', + ); + %main::mystat = ( + get_base_mock_stats(), + ); + %main::mycalc = (); + + eval { main::calculations(); main::mysql_stats(); }; + ok(!$@, 'calculations() did not crash') or diag("Crashed: $@"); + + my $found = grep { /Enable the slow query log/ } @main::generalrec; + ok($found, 'Should recommend enabling slow query log when log_slow_queries fallback is 0'); +}; + +subtest 'log_slow_queries fallback is 1' => sub { + @main::generalrec = (); + @main::adjvars = (); + $main::physical_memory = 32 * 1024 * 1024 * 1024; + $main::swap_memory = 4 * 1024 * 1024 * 1024; + + %main::myvar = ( + get_base_mock_vars(), + log_slow_queries => '1', + ); + %main::mystat = ( + get_base_mock_stats(), + ); + %main::mycalc = (); + + eval { main::calculations(); main::mysql_stats(); }; + ok(!$@, 'calculations() did not crash') or diag("Crashed: $@"); + + my $found = grep { /Enable the slow query log/ } @main::generalrec; + ok(!$found, 'Should not recommend enabling slow query log when log_slow_queries fallback is 1'); +}; + done_testing(); From 5340c555fbfa0fef98eb6a209346eb1f77b6c05c Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Fri, 10 Jul 2026 01:09:08 +0200 Subject: [PATCH 85/97] docs: regenerate release notes --- releases/v2.9.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/releases/v2.9.1.md b/releases/v2.9.1.md index 7bb80630a..e22c6b737 100644 --- a/releases/v2.9.1.md +++ b/releases/v2.9.1.md @@ -107,6 +107,7 @@ ## 🛠️ Internal Commit History +- fix(main): support '0' and 'OFF' value representations when checking slow query log status (#517) (b9f4083) - docs: regenerate release notes (bb4ae91) - test(lab): add HA topology E2E tests, MCP server E2E test, and dedicated output analyzer (2999bad) - test(test): verify replication terminology and checksums in unit tests (#888) (18d1da0) From 049b59af67c31d97dcd75b2117d5aeffc854d18e Mon Sep 17 00:00:00 2001 From: Jean-Marie Renouard Date: Fri, 10 Jul 2026 10:47:22 +0200 Subject: [PATCH 86/97] fix(main): resolve uninitialized warnings in auto-increment audit and fix split delimiters fix #888 --- Changelog | 2 ++ mysqltuner.pl | 12 ++++++------ releases/v2.9.1.md | 3 +++ tests/unit_ha_cluster.t | 18 +++++++++--------- tests/unit_workload_traffic.t | 15 +++++++-------- 5 files changed, 27 insertions(+), 23 deletions(-) diff --git a/Changelog b/Changelog index 30246ae44..307b203dc 100644 --- a/Changelog +++ b/Changelog @@ -46,6 +46,7 @@ - fix(main): query events_errors_summary_global_by_error safely (#956, jmrenouard#64) - fix(main): support '0' and 'OFF' value representations when checking slow query log status (#517) (finalized implementation) - fix: update documentation and code +- fix(main): resolve uninitialized warnings in auto-increment audit and fix split delimiters - test(hook): verify pre-commit hook runs tests when test files change - test(lab): add unit test test_issue_810.t for issue 810 - test(lab): expand unit tests for issue #517 to verify alternative representations of slow query log active state @@ -65,6 +66,7 @@ - test(lab): add build/test_ha.sh E2E orchestrator for Galera, InnoDB Cluster, and Replication topologies - test(lab): add build/analyze_mt_output.pl dedicated output analyzer with HA profile support - test(lab): add HA validation profiles (galera.json, innodb_cluster.json, replication.json) +- test(test): update workload traffic and HA cluster unit tests to use tab-delimited mock data - ci: optimize pre-commit hook to only run unit tests when code, tests, or dependencies are modified - docs(metadata): promote HTML reports and add E2E examples - docs(metadata): remove timestamp from doc-sync generated files diff --git a/mysqltuner.pl b/mysqltuner.pl index 6cfd72c9a..f0a3db208 100755 --- a/mysqltuner.pl +++ b/mysqltuner.pl @@ -1608,7 +1608,7 @@ sub check_replication_advanced { my $primary_count = 0; my %versions; foreach my $m (@group_members) { - my @mparts = split( /\|/, $m ); + my @mparts = split( /\t/, $m ); my $host = $mparts[0] // ''; my $port = $mparts[1] // ''; my $state = $mparts[2] // ''; @@ -1828,7 +1828,7 @@ sub check_workload_traffic { my $lock_waits = 0; my $net_waits = 0; foreach my $we (@wait_events) { - my ( $name, $wait ) = split( /\|/, $we ); + my ( $name, $wait ) = split( /\t/, $we ); $name //= ''; $wait //= 0; infoprint " - $name: " @@ -1873,7 +1873,7 @@ sub check_workload_traffic { if ( scalar(@churn_tables) > 0 ) { my %churn_map; foreach my $ct (@churn_tables) { - my ( $schema, $table, $writes ) = split( /\|/, $ct ); + my ( $schema, $table, $writes ) = split( /\t/, $ct ); $churn_map{"$schema.$table"} = $writes if $schema && $table; } @@ -1909,7 +1909,7 @@ sub check_workload_traffic { if ( scalar(@auto_inc_cols) > 0 ) { foreach my $col_info (@auto_inc_cols) { my ( $schema, $table, $col, $type, $curr_val ) = - split( /\|/, $col_info ); + split( /\t/, $col_info ); $type = lc( $type // '' ); $curr_val //= 0; @@ -5800,7 +5800,7 @@ sub check_auth_plugins { # Extract user and host for CSV my ( $user, $host ) = ( '', '' ); - if ( $user_host =~ /'([^']*)'@'([^']*)'/ ) { + if ( $user_host =~ /'([^']*)'\@'([^']*)'/ ) { $user = $1; $host = $2; } @@ -14735,7 +14735,7 @@ sub dump_result { MySQLTuner Advanced Report - +