From b860662c17495e8204f5b91686089cefb27ecbd8 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Fri, 17 Apr 2026 18:45:05 +0000 Subject: [PATCH 01/16] test/infra: slim dbdeployer images (~70-77% smaller) Every dbdeployer-* CI image was carrying dead weight from the MySQL/MariaDB unpack step. Cleanup is folded into the same RUN layer as the unpack so the removals actually reclaim space instead of leaving it in a prior layer: - remove the tarball that 'dbdeployer downloads get-unpack' left at / (~1GB) - remove bin/mysqld-debug (debug server binary, unused at runtime) - for MySQL 5.7 only: also remove lib/libmysqld.a and lib/libmysqld-debug.a (embedded-server static libs, deprecated in 5.7 and removed in 8.0) - strip all ELF binaries under bin/ and shared libs under lib/ using binutils installed temporarily and purged within the same layer Result: 2.8 GB -> 653 MB for mysql95, with similar drops across every tier. Existing behavior is preserved (libmysqlclient.a kept, no runtime changes). --- .../infra-dbdeployer-mariadb10/docker/Dockerfile | 14 ++++++++++++-- .../docker/Dockerfile | 14 ++++++++++++-- .../infra-dbdeployer-mysql57/docker/Dockerfile | 14 ++++++++++++-- .../docker/Dockerfile | 14 ++++++++++++-- .../infra-dbdeployer-mysql84-gr/docker/Dockerfile | 14 ++++++++++++-- .../infra-dbdeployer-mysql84/docker/Dockerfile | 14 ++++++++++++-- .../docker/Dockerfile | 14 ++++++++++++-- .../infra-dbdeployer-mysql90-gr/docker/Dockerfile | 14 ++++++++++++-- .../infra-dbdeployer-mysql90/docker/Dockerfile | 14 ++++++++++++-- .../infra-dbdeployer-mysql93-gr/docker/Dockerfile | 14 ++++++++++++-- .../infra-dbdeployer-mysql93/docker/Dockerfile | 14 ++++++++++++-- .../docker/Dockerfile | 14 ++++++++++++-- .../infra-dbdeployer-mysql95-gr/docker/Dockerfile | 14 ++++++++++++-- .../infra-dbdeployer-mysql95/docker/Dockerfile | 14 ++++++++++++-- 14 files changed, 168 insertions(+), 28 deletions(-) diff --git a/test/infra/infra-dbdeployer-mariadb10/docker/Dockerfile b/test/infra/infra-dbdeployer-mariadb10/docker/Dockerfile index 6ab0fa2d2..9a237e693 100644 --- a/test/infra/infra-dbdeployer-mariadb10/docker/Dockerfile +++ b/test/infra/infra-dbdeployer-mariadb10/docker/Dockerfile @@ -20,9 +20,19 @@ RUN curl -fsSL "https://github.com/ProxySQL/dbdeployer/releases/download/v${DBDE | tar -xz -C /usr/local/bin/ \ && chmod +x /usr/local/bin/dbdeployer -# Download and unpack MariaDB 10.11.9 tarball via dbdeployer +# Download and unpack MariaDB 10.11.9 tarball via dbdeployer. +# Cleanup in the same layer: remove the downloaded tarball, drop mysqld-debug if present, +# and strip all ELF binaries/shared libs. binutils is installed temporarily and purged after. RUN mkdir -p /root/opt/mysql \ - && dbdeployer downloads get-unpack mariadb-10.11.9-linux-systemd-x86_64.tar.gz + && dbdeployer downloads get-unpack mariadb-10.11.9-linux-systemd-x86_64.tar.gz \ + && rm -f /*.tar.xz /*.tar.gz \ + && rm -f /root/opt/mysql/*/bin/mysqld-debug \ + && apt-get update && apt-get install -y --no-install-recommends binutils \ + && find /root/opt/mysql -type f \( -path '*/bin/*' -o -name '*.so' -o -name '*.so.*' \) \ + -exec sh -c 'strip --strip-unneeded "$1" 2>/dev/null || true' _ {} \; \ + && apt-get purge -y binutils \ + && apt-get autoremove -y --purge \ + && rm -rf /var/lib/apt/lists/* # Clear reserved ports so dbdeployer allows base-port 3305 (nodes get 3306,3307,3308) # dbdeployer reserves 1186,3306,5432,33060,33062 by default; replace with harmless value diff --git a/test/infra/infra-dbdeployer-mysql57-binlog/docker/Dockerfile b/test/infra/infra-dbdeployer-mysql57-binlog/docker/Dockerfile index af62f08af..7b34812a5 100644 --- a/test/infra/infra-dbdeployer-mysql57-binlog/docker/Dockerfile +++ b/test/infra/infra-dbdeployer-mysql57-binlog/docker/Dockerfile @@ -23,11 +23,21 @@ RUN curl -fsSL "https://github.com/ProxySQL/dbdeployer/releases/download/v${DBDE | tar -xz -C /usr/local/bin/ \ && chmod +x /usr/local/bin/dbdeployer -# Download and unpack MySQL 5.7 tarball (pre-baked so no download at runtime) +# Download and unpack MySQL 5.7 tarball (pre-baked so no download at runtime). +# Cleanup in the same layer: drop mysqld-debug if present, and strip all ELF binaries/shared +# libs. binutils is installed temporarily for strip and purged after. RUN mkdir -p /root/downloads /root/opt/mysql \ && curl -fsSL -o /root/downloads/mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz "${MYSQL_TARBALL_URL}" \ && dbdeployer unpack /root/downloads/mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz \ - && rm -f /root/downloads/mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz + && rm -f /root/downloads/mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz \ + && rm -f /root/opt/mysql/*/bin/mysqld-debug \ + && rm -f /root/opt/mysql/*/lib/libmysqld.a /root/opt/mysql/*/lib/libmysqld-debug.a \ + && apt-get update && apt-get install -y --no-install-recommends binutils \ + && find /root/opt/mysql -type f \( -path '*/bin/*' -o -name '*.so' -o -name '*.so.*' \) \ + -exec sh -c 'strip --strip-unneeded "$1" 2>/dev/null || true' _ {} \; \ + && apt-get purge -y binutils \ + && apt-get autoremove -y --purge \ + && rm -rf /var/lib/apt/lists/* # Clear reserved ports so dbdeployer allows base-port 3305 (nodes get 3306,3307,3308) RUN dbdeployer defaults update reserved-ports '0' diff --git a/test/infra/infra-dbdeployer-mysql57/docker/Dockerfile b/test/infra/infra-dbdeployer-mysql57/docker/Dockerfile index 8bd92e8f0..3f6f4e06c 100644 --- a/test/infra/infra-dbdeployer-mysql57/docker/Dockerfile +++ b/test/infra/infra-dbdeployer-mysql57/docker/Dockerfile @@ -20,11 +20,21 @@ RUN curl -fsSL "https://github.com/ProxySQL/dbdeployer/releases/download/v${DBDE | tar -xz -C /usr/local/bin/ \ && chmod +x /usr/local/bin/dbdeployer -# Download and unpack MySQL 5.7 tarball (pre-baked so no download at runtime) +# Download and unpack MySQL 5.7 tarball (pre-baked so no download at runtime). +# Cleanup in the same layer: drop mysqld-debug if present, and strip all ELF binaries/shared +# libs. binutils is installed temporarily for strip and purged after. RUN mkdir -p /root/downloads /root/opt/mysql \ && curl -fsSL -o /root/downloads/mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz "${MYSQL_TARBALL_URL}" \ && dbdeployer unpack /root/downloads/mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz \ - && rm -f /root/downloads/mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz + && rm -f /root/downloads/mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz \ + && rm -f /root/opt/mysql/*/bin/mysqld-debug \ + && rm -f /root/opt/mysql/*/lib/libmysqld.a /root/opt/mysql/*/lib/libmysqld-debug.a \ + && apt-get update && apt-get install -y --no-install-recommends binutils \ + && find /root/opt/mysql -type f \( -path '*/bin/*' -o -name '*.so' -o -name '*.so.*' \) \ + -exec sh -c 'strip --strip-unneeded "$1" 2>/dev/null || true' _ {} \; \ + && apt-get purge -y binutils \ + && apt-get autoremove -y --purge \ + && rm -rf /var/lib/apt/lists/* # Clear reserved ports so dbdeployer allows base-port 3305 (nodes get 3306,3307,3308) # dbdeployer reserves 1186,3306,5432,33060,33062 by default; replace with harmless value diff --git a/test/infra/infra-dbdeployer-mysql84-binlog/docker/Dockerfile b/test/infra/infra-dbdeployer-mysql84-binlog/docker/Dockerfile index 1d3e00c5d..1efeb41ac 100644 --- a/test/infra/infra-dbdeployer-mysql84-binlog/docker/Dockerfile +++ b/test/infra/infra-dbdeployer-mysql84-binlog/docker/Dockerfile @@ -26,9 +26,19 @@ RUN curl -fsSL "https://github.com/ProxySQL/dbdeployer/releases/download/v${DBDE | tar -xz -C /usr/local/bin/ \ && chmod +x /usr/local/bin/dbdeployer -# Download and unpack MySQL 8.4 tarball using dbdeployer (pre-baked so no download at runtime) +# Download and unpack MySQL 8.4 tarball using dbdeployer (pre-baked so no download at runtime). +# Cleanup in the same layer: remove the downloaded tarball, drop mysqld-debug, and strip +# all ELF binaries/shared libs. binutils is installed temporarily for strip and purged after. RUN mkdir -p /root/opt/mysql \ - && dbdeployer downloads get-unpack mysql-8.4.8-linux-glibc2.17-x86_64.tar.xz + && dbdeployer downloads get-unpack mysql-8.4.8-linux-glibc2.17-x86_64.tar.xz \ + && rm -f /*.tar.xz /*.tar.gz \ + && rm -f /root/opt/mysql/*/bin/mysqld-debug \ + && apt-get update && apt-get install -y --no-install-recommends binutils \ + && find /root/opt/mysql -type f \( -path '*/bin/*' -o -name '*.so' -o -name '*.so.*' \) \ + -exec sh -c 'strip --strip-unneeded "$1" 2>/dev/null || true' _ {} \; \ + && apt-get purge -y binutils \ + && apt-get autoremove -y --purge \ + && rm -rf /var/lib/apt/lists/* # Clear reserved ports so dbdeployer allows base-port 3305 (nodes get 3306,3307,3308) RUN dbdeployer defaults update reserved-ports '0' diff --git a/test/infra/infra-dbdeployer-mysql84-gr/docker/Dockerfile b/test/infra/infra-dbdeployer-mysql84-gr/docker/Dockerfile index 564341808..24efc693e 100644 --- a/test/infra/infra-dbdeployer-mysql84-gr/docker/Dockerfile +++ b/test/infra/infra-dbdeployer-mysql84-gr/docker/Dockerfile @@ -19,9 +19,19 @@ RUN curl -fsSL "https://github.com/ProxySQL/dbdeployer/releases/download/v${DBDE | tar -xz -C /usr/local/bin/ \ && chmod +x /usr/local/bin/dbdeployer -# Download and unpack MySQL 8.4 tarball using dbdeployer (pre-baked so no download at runtime) +# Download and unpack MySQL 8.4 tarball using dbdeployer (pre-baked so no download at runtime). +# Cleanup in the same layer: remove the downloaded tarball, drop mysqld-debug, and strip +# all ELF binaries/shared libs. binutils is installed temporarily for strip and purged after. RUN mkdir -p /root/opt/mysql \ - && dbdeployer downloads get-unpack mysql-8.4.8-linux-glibc2.17-x86_64.tar.xz + && dbdeployer downloads get-unpack mysql-8.4.8-linux-glibc2.17-x86_64.tar.xz \ + && rm -f /*.tar.xz /*.tar.gz \ + && rm -f /root/opt/mysql/*/bin/mysqld-debug \ + && apt-get update && apt-get install -y --no-install-recommends binutils \ + && find /root/opt/mysql -type f \( -path '*/bin/*' -o -name '*.so' -o -name '*.so.*' \) \ + -exec sh -c 'strip --strip-unneeded "$1" 2>/dev/null || true' _ {} \; \ + && apt-get purge -y binutils \ + && apt-get autoremove -y --purge \ + && rm -rf /var/lib/apt/lists/* # Clear reserved ports so dbdeployer allows base-port 3305 (nodes get 3306,3307,3308) # dbdeployer reserves 1186,3306,5432,33060,33062 by default; replace with harmless value diff --git a/test/infra/infra-dbdeployer-mysql84/docker/Dockerfile b/test/infra/infra-dbdeployer-mysql84/docker/Dockerfile index 564341808..24efc693e 100644 --- a/test/infra/infra-dbdeployer-mysql84/docker/Dockerfile +++ b/test/infra/infra-dbdeployer-mysql84/docker/Dockerfile @@ -19,9 +19,19 @@ RUN curl -fsSL "https://github.com/ProxySQL/dbdeployer/releases/download/v${DBDE | tar -xz -C /usr/local/bin/ \ && chmod +x /usr/local/bin/dbdeployer -# Download and unpack MySQL 8.4 tarball using dbdeployer (pre-baked so no download at runtime) +# Download and unpack MySQL 8.4 tarball using dbdeployer (pre-baked so no download at runtime). +# Cleanup in the same layer: remove the downloaded tarball, drop mysqld-debug, and strip +# all ELF binaries/shared libs. binutils is installed temporarily for strip and purged after. RUN mkdir -p /root/opt/mysql \ - && dbdeployer downloads get-unpack mysql-8.4.8-linux-glibc2.17-x86_64.tar.xz + && dbdeployer downloads get-unpack mysql-8.4.8-linux-glibc2.17-x86_64.tar.xz \ + && rm -f /*.tar.xz /*.tar.gz \ + && rm -f /root/opt/mysql/*/bin/mysqld-debug \ + && apt-get update && apt-get install -y --no-install-recommends binutils \ + && find /root/opt/mysql -type f \( -path '*/bin/*' -o -name '*.so' -o -name '*.so.*' \) \ + -exec sh -c 'strip --strip-unneeded "$1" 2>/dev/null || true' _ {} \; \ + && apt-get purge -y binutils \ + && apt-get autoremove -y --purge \ + && rm -rf /var/lib/apt/lists/* # Clear reserved ports so dbdeployer allows base-port 3305 (nodes get 3306,3307,3308) # dbdeployer reserves 1186,3306,5432,33060,33062 by default; replace with harmless value diff --git a/test/infra/infra-dbdeployer-mysql90-binlog/docker/Dockerfile b/test/infra/infra-dbdeployer-mysql90-binlog/docker/Dockerfile index 65ec2daa8..622b6f357 100644 --- a/test/infra/infra-dbdeployer-mysql90-binlog/docker/Dockerfile +++ b/test/infra/infra-dbdeployer-mysql90-binlog/docker/Dockerfile @@ -26,9 +26,19 @@ RUN curl -fsSL "https://github.com/ProxySQL/dbdeployer/releases/download/v${DBDE | tar -xz -C /usr/local/bin/ \ && chmod +x /usr/local/bin/dbdeployer -# Download and unpack MySQL 9.0 tarball using dbdeployer (pre-baked so no download at runtime) +# Download and unpack MySQL 9.0 tarball using dbdeployer (pre-baked so no download at runtime). +# Cleanup in the same layer: remove the downloaded tarball, drop mysqld-debug, and strip +# all ELF binaries/shared libs. binutils is installed temporarily for strip and purged after. RUN mkdir -p /root/opt/mysql \ - && dbdeployer downloads get-unpack mysql-9.0.1-linux-glibc2.17-x86_64.tar.xz + && dbdeployer downloads get-unpack mysql-9.0.1-linux-glibc2.17-x86_64.tar.xz \ + && rm -f /*.tar.xz /*.tar.gz \ + && rm -f /root/opt/mysql/*/bin/mysqld-debug \ + && apt-get update && apt-get install -y --no-install-recommends binutils \ + && find /root/opt/mysql -type f \( -path '*/bin/*' -o -name '*.so' -o -name '*.so.*' \) \ + -exec sh -c 'strip --strip-unneeded "$1" 2>/dev/null || true' _ {} \; \ + && apt-get purge -y binutils \ + && apt-get autoremove -y --purge \ + && rm -rf /var/lib/apt/lists/* # Clear reserved ports so dbdeployer allows base-port 3305 (nodes get 3306,3307,3308) RUN dbdeployer defaults update reserved-ports '0' diff --git a/test/infra/infra-dbdeployer-mysql90-gr/docker/Dockerfile b/test/infra/infra-dbdeployer-mysql90-gr/docker/Dockerfile index b5a1f6d24..791079935 100644 --- a/test/infra/infra-dbdeployer-mysql90-gr/docker/Dockerfile +++ b/test/infra/infra-dbdeployer-mysql90-gr/docker/Dockerfile @@ -19,9 +19,19 @@ RUN curl -fsSL "https://github.com/ProxySQL/dbdeployer/releases/download/v${DBDE | tar -xz -C /usr/local/bin/ \ && chmod +x /usr/local/bin/dbdeployer -# Download and unpack MySQL 9.0 tarball using dbdeployer (pre-baked so no download at runtime) +# Download and unpack MySQL 9.0 tarball using dbdeployer (pre-baked so no download at runtime). +# Cleanup in the same layer: remove the downloaded tarball, drop mysqld-debug, and strip +# all ELF binaries/shared libs. binutils is installed temporarily for strip and purged after. RUN mkdir -p /root/opt/mysql \ - && dbdeployer downloads get-unpack mysql-9.0.1-linux-glibc2.17-x86_64.tar.xz + && dbdeployer downloads get-unpack mysql-9.0.1-linux-glibc2.17-x86_64.tar.xz \ + && rm -f /*.tar.xz /*.tar.gz \ + && rm -f /root/opt/mysql/*/bin/mysqld-debug \ + && apt-get update && apt-get install -y --no-install-recommends binutils \ + && find /root/opt/mysql -type f \( -path '*/bin/*' -o -name '*.so' -o -name '*.so.*' \) \ + -exec sh -c 'strip --strip-unneeded "$1" 2>/dev/null || true' _ {} \; \ + && apt-get purge -y binutils \ + && apt-get autoremove -y --purge \ + && rm -rf /var/lib/apt/lists/* # Clear reserved ports so dbdeployer allows base-port 3305 (nodes get 3306,3307,3308) # dbdeployer reserves 1186,3306,5432,33060,33062 by default; replace with harmless value diff --git a/test/infra/infra-dbdeployer-mysql90/docker/Dockerfile b/test/infra/infra-dbdeployer-mysql90/docker/Dockerfile index b5a1f6d24..791079935 100644 --- a/test/infra/infra-dbdeployer-mysql90/docker/Dockerfile +++ b/test/infra/infra-dbdeployer-mysql90/docker/Dockerfile @@ -19,9 +19,19 @@ RUN curl -fsSL "https://github.com/ProxySQL/dbdeployer/releases/download/v${DBDE | tar -xz -C /usr/local/bin/ \ && chmod +x /usr/local/bin/dbdeployer -# Download and unpack MySQL 9.0 tarball using dbdeployer (pre-baked so no download at runtime) +# Download and unpack MySQL 9.0 tarball using dbdeployer (pre-baked so no download at runtime). +# Cleanup in the same layer: remove the downloaded tarball, drop mysqld-debug, and strip +# all ELF binaries/shared libs. binutils is installed temporarily for strip and purged after. RUN mkdir -p /root/opt/mysql \ - && dbdeployer downloads get-unpack mysql-9.0.1-linux-glibc2.17-x86_64.tar.xz + && dbdeployer downloads get-unpack mysql-9.0.1-linux-glibc2.17-x86_64.tar.xz \ + && rm -f /*.tar.xz /*.tar.gz \ + && rm -f /root/opt/mysql/*/bin/mysqld-debug \ + && apt-get update && apt-get install -y --no-install-recommends binutils \ + && find /root/opt/mysql -type f \( -path '*/bin/*' -o -name '*.so' -o -name '*.so.*' \) \ + -exec sh -c 'strip --strip-unneeded "$1" 2>/dev/null || true' _ {} \; \ + && apt-get purge -y binutils \ + && apt-get autoremove -y --purge \ + && rm -rf /var/lib/apt/lists/* # Clear reserved ports so dbdeployer allows base-port 3305 (nodes get 3306,3307,3308) # dbdeployer reserves 1186,3306,5432,33060,33062 by default; replace with harmless value diff --git a/test/infra/infra-dbdeployer-mysql93-gr/docker/Dockerfile b/test/infra/infra-dbdeployer-mysql93-gr/docker/Dockerfile index e9e24ddd8..415a0f136 100644 --- a/test/infra/infra-dbdeployer-mysql93-gr/docker/Dockerfile +++ b/test/infra/infra-dbdeployer-mysql93-gr/docker/Dockerfile @@ -19,9 +19,19 @@ RUN curl -fsSL "https://github.com/ProxySQL/dbdeployer/releases/download/v${DBDE | tar -xz -C /usr/local/bin/ \ && chmod +x /usr/local/bin/dbdeployer -# Download and unpack MySQL 9.3 tarball using dbdeployer (pre-baked so no download at runtime) +# Download and unpack MySQL 9.3 tarball using dbdeployer (pre-baked so no download at runtime). +# Cleanup in the same layer: remove the downloaded tarball, drop mysqld-debug, and strip +# all ELF binaries/shared libs. binutils is installed temporarily for strip and purged after. RUN mkdir -p /root/opt/mysql \ - && dbdeployer downloads get-unpack mysql-9.3.0-linux-glibc2.17-x86_64.tar.xz + && dbdeployer downloads get-unpack mysql-9.3.0-linux-glibc2.17-x86_64.tar.xz \ + && rm -f /*.tar.xz /*.tar.gz \ + && rm -f /root/opt/mysql/*/bin/mysqld-debug \ + && apt-get update && apt-get install -y --no-install-recommends binutils \ + && find /root/opt/mysql -type f \( -path '*/bin/*' -o -name '*.so' -o -name '*.so.*' \) \ + -exec sh -c 'strip --strip-unneeded "$1" 2>/dev/null || true' _ {} \; \ + && apt-get purge -y binutils \ + && apt-get autoremove -y --purge \ + && rm -rf /var/lib/apt/lists/* # Clear reserved ports so dbdeployer allows base-port 3305 (nodes get 3306,3307,3308) # dbdeployer reserves 1186,3306,5432,33060,33062 by default; replace with harmless value diff --git a/test/infra/infra-dbdeployer-mysql93/docker/Dockerfile b/test/infra/infra-dbdeployer-mysql93/docker/Dockerfile index e9e24ddd8..415a0f136 100644 --- a/test/infra/infra-dbdeployer-mysql93/docker/Dockerfile +++ b/test/infra/infra-dbdeployer-mysql93/docker/Dockerfile @@ -19,9 +19,19 @@ RUN curl -fsSL "https://github.com/ProxySQL/dbdeployer/releases/download/v${DBDE | tar -xz -C /usr/local/bin/ \ && chmod +x /usr/local/bin/dbdeployer -# Download and unpack MySQL 9.3 tarball using dbdeployer (pre-baked so no download at runtime) +# Download and unpack MySQL 9.3 tarball using dbdeployer (pre-baked so no download at runtime). +# Cleanup in the same layer: remove the downloaded tarball, drop mysqld-debug, and strip +# all ELF binaries/shared libs. binutils is installed temporarily for strip and purged after. RUN mkdir -p /root/opt/mysql \ - && dbdeployer downloads get-unpack mysql-9.3.0-linux-glibc2.17-x86_64.tar.xz + && dbdeployer downloads get-unpack mysql-9.3.0-linux-glibc2.17-x86_64.tar.xz \ + && rm -f /*.tar.xz /*.tar.gz \ + && rm -f /root/opt/mysql/*/bin/mysqld-debug \ + && apt-get update && apt-get install -y --no-install-recommends binutils \ + && find /root/opt/mysql -type f \( -path '*/bin/*' -o -name '*.so' -o -name '*.so.*' \) \ + -exec sh -c 'strip --strip-unneeded "$1" 2>/dev/null || true' _ {} \; \ + && apt-get purge -y binutils \ + && apt-get autoremove -y --purge \ + && rm -rf /var/lib/apt/lists/* # Clear reserved ports so dbdeployer allows base-port 3305 (nodes get 3306,3307,3308) # dbdeployer reserves 1186,3306,5432,33060,33062 by default; replace with harmless value diff --git a/test/infra/infra-dbdeployer-mysql95-binlog/docker/Dockerfile b/test/infra/infra-dbdeployer-mysql95-binlog/docker/Dockerfile index 764527453..0558c5aa3 100644 --- a/test/infra/infra-dbdeployer-mysql95-binlog/docker/Dockerfile +++ b/test/infra/infra-dbdeployer-mysql95-binlog/docker/Dockerfile @@ -26,9 +26,19 @@ RUN curl -fsSL "https://github.com/ProxySQL/dbdeployer/releases/download/v${DBDE | tar -xz -C /usr/local/bin/ \ && chmod +x /usr/local/bin/dbdeployer -# Download and unpack MySQL 9.5 tarball using dbdeployer (pre-baked so no download at runtime) +# Download and unpack MySQL 9.5 tarball using dbdeployer (pre-baked so no download at runtime). +# Cleanup in the same layer: remove the downloaded tarball, drop mysqld-debug, and strip +# all ELF binaries/shared libs. binutils is installed temporarily for strip and purged after. RUN mkdir -p /root/opt/mysql \ - && dbdeployer downloads get-unpack mysql-9.5.0-linux-glibc2.17-x86_64.tar.xz + && dbdeployer downloads get-unpack mysql-9.5.0-linux-glibc2.17-x86_64.tar.xz \ + && rm -f /*.tar.xz /*.tar.gz \ + && rm -f /root/opt/mysql/*/bin/mysqld-debug \ + && apt-get update && apt-get install -y --no-install-recommends binutils \ + && find /root/opt/mysql -type f \( -path '*/bin/*' -o -name '*.so' -o -name '*.so.*' \) \ + -exec sh -c 'strip --strip-unneeded "$1" 2>/dev/null || true' _ {} \; \ + && apt-get purge -y binutils \ + && apt-get autoremove -y --purge \ + && rm -rf /var/lib/apt/lists/* # Clear reserved ports so dbdeployer allows base-port 3305 (nodes get 3306,3307,3308) RUN dbdeployer defaults update reserved-ports '0' diff --git a/test/infra/infra-dbdeployer-mysql95-gr/docker/Dockerfile b/test/infra/infra-dbdeployer-mysql95-gr/docker/Dockerfile index 282901cc4..e7aac0439 100644 --- a/test/infra/infra-dbdeployer-mysql95-gr/docker/Dockerfile +++ b/test/infra/infra-dbdeployer-mysql95-gr/docker/Dockerfile @@ -19,9 +19,19 @@ RUN curl -fsSL "https://github.com/ProxySQL/dbdeployer/releases/download/v${DBDE | tar -xz -C /usr/local/bin/ \ && chmod +x /usr/local/bin/dbdeployer -# Download and unpack MySQL 9.5 tarball using dbdeployer (pre-baked so no download at runtime) +# Download and unpack MySQL 9.5 tarball using dbdeployer (pre-baked so no download at runtime). +# Cleanup in the same layer: remove the downloaded tarball, drop mysqld-debug, and strip +# all ELF binaries/shared libs. binutils is installed temporarily for strip and purged after. RUN mkdir -p /root/opt/mysql \ - && dbdeployer downloads get-unpack mysql-9.5.0-linux-glibc2.17-x86_64.tar.xz + && dbdeployer downloads get-unpack mysql-9.5.0-linux-glibc2.17-x86_64.tar.xz \ + && rm -f /*.tar.xz /*.tar.gz \ + && rm -f /root/opt/mysql/*/bin/mysqld-debug \ + && apt-get update && apt-get install -y --no-install-recommends binutils \ + && find /root/opt/mysql -type f \( -path '*/bin/*' -o -name '*.so' -o -name '*.so.*' \) \ + -exec sh -c 'strip --strip-unneeded "$1" 2>/dev/null || true' _ {} \; \ + && apt-get purge -y binutils \ + && apt-get autoremove -y --purge \ + && rm -rf /var/lib/apt/lists/* # Clear reserved ports so dbdeployer allows base-port 3305 (nodes get 3306,3307,3308) # dbdeployer reserves 1186,3306,5432,33060,33062 by default; replace with harmless value diff --git a/test/infra/infra-dbdeployer-mysql95/docker/Dockerfile b/test/infra/infra-dbdeployer-mysql95/docker/Dockerfile index 282901cc4..e7aac0439 100644 --- a/test/infra/infra-dbdeployer-mysql95/docker/Dockerfile +++ b/test/infra/infra-dbdeployer-mysql95/docker/Dockerfile @@ -19,9 +19,19 @@ RUN curl -fsSL "https://github.com/ProxySQL/dbdeployer/releases/download/v${DBDE | tar -xz -C /usr/local/bin/ \ && chmod +x /usr/local/bin/dbdeployer -# Download and unpack MySQL 9.5 tarball using dbdeployer (pre-baked so no download at runtime) +# Download and unpack MySQL 9.5 tarball using dbdeployer (pre-baked so no download at runtime). +# Cleanup in the same layer: remove the downloaded tarball, drop mysqld-debug, and strip +# all ELF binaries/shared libs. binutils is installed temporarily for strip and purged after. RUN mkdir -p /root/opt/mysql \ - && dbdeployer downloads get-unpack mysql-9.5.0-linux-glibc2.17-x86_64.tar.xz + && dbdeployer downloads get-unpack mysql-9.5.0-linux-glibc2.17-x86_64.tar.xz \ + && rm -f /*.tar.xz /*.tar.gz \ + && rm -f /root/opt/mysql/*/bin/mysqld-debug \ + && apt-get update && apt-get install -y --no-install-recommends binutils \ + && find /root/opt/mysql -type f \( -path '*/bin/*' -o -name '*.so' -o -name '*.so.*' \) \ + -exec sh -c 'strip --strip-unneeded "$1" 2>/dev/null || true' _ {} \; \ + && apt-get purge -y binutils \ + && apt-get autoremove -y --purge \ + && rm -rf /var/lib/apt/lists/* # Clear reserved ports so dbdeployer allows base-port 3305 (nodes get 3306,3307,3308) # dbdeployer reserves 1186,3306,5432,33060,33062 by default; replace with harmless value From ef351a6a67ec0696e324e524707270751753b614 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 18 Apr 2026 05:19:08 +0000 Subject: [PATCH 02/16] test/groups: drop reg_test_3992 from mysql8x/95 groups The reg_test_3992_fast_forward_malformed_packet-{t,pymysql,mysqlsh} tests hard-code MariaDB-specific users (mariadbuser, mariadbuserff) that are provisioned only by infra-{mariadb10,dbdeployer-mariadb10}. The mysql84-g2, mysql90-g2 and mysql95-g2 groups don't include a MariaDB infra, so the test fails immediately with "Access denied for user 'mariadbuserff'". Restrict the three variants to legacy-g2 and the mysql-* knob-variant g2 groups (which inherit the legacy infra layout including MariaDB). --- test/tap/groups/groups.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/tap/groups/groups.json b/test/tap/groups/groups.json index 684a8db50..f9187524e 100644 --- a/test/tap/groups/groups.json +++ b/test/tap/groups/groups.json @@ -206,9 +206,9 @@ "reg_test_3765_ssl_pollout-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g2","mysql-multiplexing=false-g2","mysql-query_digests=0-g2","mysql-query_digests_keep_comment=1-g2","mysql84-g2","mysql84-g4","mysql90-g2","mysql90-g4","mysql95-g2","mysql95-g4" ], "reg_test_3838-restapi_eintr-t" : [ "legacy-g2","mysql-auto_increment_delay_multiplex=0-g2","mysql-multiplexing=false-g2","mysql-query_digests=0-g2","mysql-query_digests_keep_comment=1-g2","mysql84-g2","mysql90-g2","mysql95-g2" ], "reg_test_3847_admin_lock-t" : [ "legacy-g2","mysql-auto_increment_delay_multiplex=0-g2","mysql-multiplexing=false-g2","mysql-query_digests=0-g2","mysql-query_digests_keep_comment=1-g2","mysql84-g2","mysql90-g2","mysql95-g2" ], - "reg_test_3992_fast_forward_malformed_packet-mysqlsh-t" : [ "legacy-g2","mysql-auto_increment_delay_multiplex=0-g2","mysql-multiplexing=false-g2","mysql-query_digests=0-g2","mysql-query_digests_keep_comment=1-g2","mysql84-g2","mysql90-g2","mysql95-g2" ], - "reg_test_3992_fast_forward_malformed_packet-pymysql-t" : [ "legacy-g2","mysql-auto_increment_delay_multiplex=0-g2","mysql-multiplexing=false-g2","mysql-query_digests=0-g2","mysql-query_digests_keep_comment=1-g2","mysql84-g2","mysql90-g2","mysql95-g2" ], - "reg_test_3992_fast_forward_malformed_packet-t" : [ "legacy-g2","mysql-auto_increment_delay_multiplex=0-g2","mysql-multiplexing=false-g2","mysql-query_digests=0-g2","mysql-query_digests_keep_comment=1-g2","mysql84-g2","mysql90-g2","mysql95-g2" ], + "reg_test_3992_fast_forward_malformed_packet-mysqlsh-t" : [ "legacy-g2","mysql-auto_increment_delay_multiplex=0-g2","mysql-multiplexing=false-g2","mysql-query_digests=0-g2","mysql-query_digests_keep_comment=1-g2" ], + "reg_test_3992_fast_forward_malformed_packet-pymysql-t" : [ "legacy-g2","mysql-auto_increment_delay_multiplex=0-g2","mysql-multiplexing=false-g2","mysql-query_digests=0-g2","mysql-query_digests_keep_comment=1-g2" ], + "reg_test_3992_fast_forward_malformed_packet-t" : [ "legacy-g2","mysql-auto_increment_delay_multiplex=0-g2","mysql-multiplexing=false-g2","mysql-query_digests=0-g2","mysql-query_digests_keep_comment=1-g2" ], "reg_test_4001-restapi_scripts_num_fds-t" : [ "legacy-g2","mysql-auto_increment_delay_multiplex=0-g2","mysql-multiplexing=false-g2","mysql-query_digests=0-g2","mysql-query_digests_keep_comment=1-g2","mysql84-g2","mysql90-g2","mysql95-g2" ], "reg_test_4055_restapi-t" : [ "legacy-g2","mysql-auto_increment_delay_multiplex=0-g2","mysql-multiplexing=false-g2","mysql-query_digests=0-g2","mysql-query_digests_keep_comment=1-g2","mysql84-g2","mysql90-g2","mysql95-g2" ], "reg_test_4072-show-warnings-t" : [ "legacy-g2","mysql-auto_increment_delay_multiplex=0-g2","mysql-multiplexing=false-g2","mysql-query_digests=0-g2","mysql-query_digests_keep_comment=1-g2","mysql84-g2","mysql90-g2","mysql95-g2" ], From 4e39f3bdc4888ee7bd34b3fcba3b986503efe20b Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 18 Apr 2026 06:13:25 +0000 Subject: [PATCH 03/16] fix: parse caching_sha2_password rounds as hex (auth fails on rounds >= 10000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MySQL stores the rounds field of caching_sha2_password hashes ($A$$) as 3-char zero-padded uppercase hex of (rounds/1000). See sql/auth/sha2_password.cc: sprintf(rounds_str, "%03X", m_stored_digest_rounds) ProxySQL was parsing this field with stol(s, 10) (default base-10), which silently truncates at the first hex digit (A-F): 005 → 5 (5000 rounds, MySQL 8.0 default) ✓ matches base-16 009 → 9 (9000 rounds) ✓ matches base-16 00A → 0 (10000 rounds, stops at 'A') ✗ should be 10 010 → 10 (16000 rounds, decimal!) ✗ should be 16 01A → 1 (26000 rounds, stops at 'A') ✗ should be 26 The wrong rounds value is then fed into sha256_crypt_r() via "$5$rounds=$", producing a digest that does not match the stored hash, so verification fails with Access denied. Trigger: any MySQL backend with caching_sha2_password_digest_rounds >= 10000 (or any value whose hex encoding contains A-F). Surfaces in CI on the mysql95 dbdeployer image, where backend-generated hashes start with $A$00A$. The bug is latent on MySQL 8.0/8.4/9.0 with default rounds=5000 because "005" parses identically in base-10 and base-16. Fix: pass base=16 to stol() in both PPHR_verify_sha2() and PPHR_sha2full(). --- lib/MySQL_Protocol.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/MySQL_Protocol.cpp b/lib/MySQL_Protocol.cpp index b189c9ee1..8f3108400 100644 --- a/lib/MySQL_Protocol.cpp +++ b/lib/MySQL_Protocol.cpp @@ -2194,7 +2194,12 @@ bool MySQL_Protocol::PPHR_verify_sha2( } else if (passformat == AUTH_MYSQL_CACHING_SHA2_PASSWORD) { assert(strlen(vars1.password) == 70); string sp = string(vars1.password); - long rounds = stol(sp.substr(3,3)); + // MySQL stores rounds as 3-char zero-padded uppercase hex of (rounds/1000). + // See sql/auth/sha2_password.cc::Caching_sha2_password::digest_round_separator(): + // sprintf(rounds_str, "%03X", m_stored_digest_rounds) + // Parsing as base-10 silently truncates at the first hex digit (A-F), + // breaking auth for any backend with caching_sha2_password_digest_rounds >= 10000. + long rounds = stol(sp.substr(3,3), nullptr, 16); string salt = sp.substr(7,20); string sha256hash = sp.substr(27,43); char buf[100]; @@ -2248,7 +2253,9 @@ void MySQL_Protocol::PPHR_sha2full( } else if (passformat == AUTH_MYSQL_CACHING_SHA2_PASSWORD) { assert(strlen(vars1.password) == 70); string sp = string(vars1.password); - long rounds = stol(sp.substr(3,3)); + // MySQL stores rounds as 3-char zero-padded uppercase hex of (rounds/1000) — see + // PPHR_verify_sha2() above for the upstream format reference. Must parse base-16. + long rounds = stol(sp.substr(3,3), nullptr, 16); string salt = sp.substr(7,20); string sha256hash = sp.substr(27,43); //char * sha256_crypt_r (const char *key, const char *salt, char *buffer, int buflen); From 7a1d639a4bb36f50f0ad51264ef2dd8d536e4c77 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 18 Apr 2026 06:57:27 +0000 Subject: [PATCH 04/16] build: set default goal to build_src, not the first target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Makefile had `.DEFAULT: default` intending to make the `default` target (which builds the project) the fallback when `make` is invoked with no arguments. `.DEFAULT` is a different special target — it provides a fallback *recipe* for targets with no rule, and has no effect on default goal selection. GNU make picks the first real target as the default goal unless `.DEFAULT_GOAL` is set. The first real target in this Makefile is `lint-generate-cdb` (line 19), so `make` with no arguments ran `scripts/lint/generate-compile-commands.sh`, which does `bear -- make -j\$(nproc)` — a full rebuild of the project under Bear's compile-intercept wrappers. Each C++ compile under Bear spawned a wrapper process, producing hundreds of bear processes per plain `make` invocation and burying the intended default behaviour. Replace `.DEFAULT: default` with `.DEFAULT_GOAL := default` so `make` with no arguments runs `default: build_src` as originally intended. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index cbf4b281d..a8296645d 100644 --- a/Makefile +++ b/Makefile @@ -189,7 +189,7 @@ endif ### main targets -.DEFAULT: default +.DEFAULT_GOAL := default .PHONY: default default: build_src From 4e0efd753690204d931ca33ae58a54e464b8b348 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 18 Apr 2026 07:01:28 +0000 Subject: [PATCH 05/16] scripts/lint: pass explicit target to bear-wrapped make MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit generate-compile-commands.sh wrapped the top-level build with bear -- make -j\$(nproc) with no explicit target, so the inner make resolved its default goal dynamically. Combined with the previous Makefile bug that left lint-generate-cdb as the default goal, invoking `make` with no arguments produced an exponential fork bomb: lint-generate-cdb → this script → bear -- make -jN → lint-generate-cdb → this script → …, with each level forking up to \$(nproc) concurrent sub-makes under Bear's LD_PRELOAD exec intercept. Spawned hundreds of `bear -- make -jN` processes before grinding the system to a halt. The Makefile default-goal fix in 7a1d639a4 closes the trigger, but the script itself should also be robust against a future re-introduction of the same hazard. Pass `build_src` explicitly as the target so the inner make never re-enters lint-generate-cdb regardless of what the default goal happens to be. --- scripts/lint/generate-compile-commands.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/lint/generate-compile-commands.sh b/scripts/lint/generate-compile-commands.sh index 77b5b4c70..d20273790 100755 --- a/scripts/lint/generate-compile-commands.sh +++ b/scripts/lint/generate-compile-commands.sh @@ -2,8 +2,15 @@ set -euo pipefail # Generate compile_commands.json by wrapping make with Bear. # Usage: ./generate-compile-commands.sh +# +# Note: the default build command explicitly targets `build_src` rather than +# relying on GNU make's default goal. Invoking `bear -- make` with no target +# would re-enter whatever the default goal is — which, if it ever happens to +# be `lint-generate-cdb` (the target that calls this script), produces an +# exponential fork bomb under `-j$(nproc)` because every recursive invocation +# also wraps its own recursive invocation in Bear's exec intercept. -BUILD_CMD=${1:-"make -j$(nproc)"} +BUILD_CMD=${1:-"make build_src -j$(nproc)"} if ! command -v bear >/dev/null 2>&1; then echo "bear is required. Install it (e.g. apt install bear)" >&2 From 6322d58376322e6372c1cba6cb70077beef897fb Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 18 Apr 2026 07:26:48 +0000 Subject: [PATCH 06/16] sqlite3: CACHING_SHA2_PASSWORD() accepts optional rounds arg (closes #5640) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ProxySQL admin SQLite3 extension CACHING_SHA2_PASSWORD() previously always emitted hashes with 5000 rounds (hardcoded "\$A\$005\$" prefix and "\$5\$rounds=5000\$" crypt salt). MySQL's caching_sha2_password_digest_rounds is configurable from 5000 to 4095000 in steps of 1000, so a backend that uses any non-default value produces hashes the extension cannot reproduce — breaking the byte-for-byte hash equivalence test in test_sqlite3_pass_exts-t on e.g. the mysql95 backend where rounds=10000. Extend the SQLite function to accept an optional third integer argument carrying the desired rounds value: CACHING_SHA2_PASSWORD(pass) -> random salt, 5000 rounds CACHING_SHA2_PASSWORD(pass, salt) -> given salt, 5000 rounds CACHING_SHA2_PASSWORD(pass, salt, rounds) -> given salt, given rounds rounds is validated: integer, multiple of 1000, in [5000, 4095000] — matching MySQL's caching_sha2_password_digest_rounds bounds. The "\$5\$rounds=\$" salt prefix and "\$A\$\$" hash prefix are now built at runtime from the resolved value, with emitted as 3-char zero-padded uppercase hex of (rounds/1000) per MySQL's sql/auth/sha2_password.cc::digest_round_separator(). Update test_sqlite3_pass_exts-t to parse the rounds field from the MySQL-generated hash (chars 3..5 of \$A\$\$..., base-16, ×1000) and pass it to CACHING_SHA2_PASSWORD() — so the hash-equivalence assertion matches any backend rounds. Update the INV_INPUTS fixture: 3-arg is now valid, so reject 4-arg ("wrong number"), reject 3-arg with non-integer rounds ("Invalid argument type"), reject out-of-range or non-multiple-of-1000 rounds ("Invalid rounds: ..."). Verified against mysql95 backend (rounds=10000): test_sqlite3_pass_exts-t passes 2117/2117, test_auth_methods-t passes 10774/10774 (previously failing with 50+122 not-ok respectively before the base-16 parse fix in MySQL_Protocol.cpp). --- deps/sqlite3/sqlite3_pass_exts.patch | 81 ++++++++++++++------- test/tap/tests/test_sqlite3_pass_exts-t.cpp | 36 ++++++++- 2 files changed, 85 insertions(+), 32 deletions(-) diff --git a/deps/sqlite3/sqlite3_pass_exts.patch b/deps/sqlite3/sqlite3_pass_exts.patch index 83d940212..89b613119 100644 --- a/deps/sqlite3/sqlite3_pass_exts.patch +++ b/deps/sqlite3/sqlite3_pass_exts.patch @@ -1,6 +1,6 @@ --- sqlite3.c 2024-03-22 19:22:47.046093173 +0100 +++ sqlite3-pass-exts.c 2024-03-22 19:24:09.557303716 +0100 -@@ -26313,6 +26313,183 @@ +@@ -26275,6 +26275,207 @@ sqlite3ResultStrAccum(context, &sRes); } @@ -27,30 +27,35 @@ +int check_args_types(int argc, sqlite3_value** argv) { + int inv_type = sqlite3_value_type(argv[0]) != SQLITE_TEXT; + -+ if (inv_type == 0 && argc == 2) { -+ return ++ if (inv_type == 0 && argc >= 2) { ++ inv_type = + sqlite3_value_type(argv[1]) != SQLITE_TEXT && + sqlite3_value_type(argv[1]) != SQLITE_BLOB; -+ } else { -+ return inv_type; + } ++ ++ if (inv_type == 0 && argc >= 3) { ++ inv_type = sqlite3_value_type(argv[2]) != SQLITE_INTEGER; ++ } ++ ++ return inv_type; +} + +int check_args_lengths(int argc, sqlite3_value** argv) { -+ int inv_size = 1; -+ + int pass_size = sqlite3_value_bytes(argv[0]); -+ if (pass_size > 0) { -+ inv_size = 0; ++ if (pass_size <= 0) { ++ return 1; + } + -+ if (inv_size == 0 && argc == 2) { ++ if (argc >= 2) { + int salt_size = sqlite3_value_bytes(argv[1]); -+ -+ return salt_size <= 0 || salt_size > DEF_SALT_SIZE; -+ } else { -+ return inv_size; ++ if (salt_size <= 0 || salt_size > DEF_SALT_SIZE) { ++ return 1; ++ } + } ++ ++ /* argc == 3: rounds is an integer; range is checked in caching_sha2_passwordFunc(). */ ++ ++ return 0; +} + +/** @@ -103,15 +108,19 @@ +/** + * @brief SQLite3 extension function for hash generation. + * @details Computes a hash equivalent to the one generated by MySQL for 'caching_sha2_password'. ++ * Output format matches MySQL's storage: '$A$$' where is the ++ * 3-char zero-padded uppercase hex of (rounds/1000) — see MySQL ++ * sql/auth/sha2_password.cc::Caching_sha2_password::digest_round_separator(). + * @param context SQLite3 context used for returning computation result. -+ * @param argc Number of arguments; either 1 or 2. One for random salt, two providing salt. -+ * @param argv Argument list; expected to hold either 1 or 2 arguments: -+ * 1. Password to be hashed; with len > 0 and of type 'SQLITE_TEXT'. -+ * 1. Optional salt; with (len > 0 && len <= 20) and of type ('SQLITE_TEXT' || 'SQLITE_BLOB'). If no salt is -+ * provided a randomly generated salt with length 20 will be used. ++ * @param argc Number of arguments; 1, 2, or 3. ++ * @param argv Argument list: ++ * 1. Password to be hashed; len > 0, type 'SQLITE_TEXT'. ++ * 2. Optional salt; len in (0,20], type 'SQLITE_TEXT' or 'SQLITE_BLOB'. Random if omitted. ++ * 3. Optional rounds; integer in [5000,4095000] in steps of 1000 (matches MySQL's ++ * caching_sha2_password_digest_rounds). Defaults to 5000 when omitted. + */ +static void caching_sha2_passwordFunc(sqlite3_context* context, int argc, sqlite3_value** argv) { -+ if (argc < 1 || argc > 2) { ++ if (argc < 1 || argc > 3) { + sqlite3_result_text(context, "Invalid number of arguments", -1, SQLITE_TRANSIENT); + return; + } else { @@ -125,11 +134,24 @@ + } + } + ++ /* Resolve rounds: default 5000 (MySQL 8.0 historical default), overridable via ++ argv[2]. Range matches MySQL's caching_sha2_password_digest_rounds bounds. */ ++ long rounds = 5000; ++ if (argc == 3) { ++ rounds = (long)sqlite3_value_int64(argv[2]); ++ if (rounds < 5000 || rounds > 4095000 || (rounds % 1000) != 0) { ++ sqlite3_result_text(context, ++ "Invalid rounds: expected multiple of 1000 in [5000,4095000]", ++ -1, SQLITE_TRANSIENT); ++ return; ++ } ++ } ++ + unsigned int salt_size = DEF_SALT_SIZE; + const char* cpass = (const char*)sqlite3_value_text(argv[0]); + unsigned char salt[DEF_SALT_SIZE + 1] = { 0 }; + -+ if (argc == 2) { ++ if (argc >= 2) { + salt_size = sqlite3_value_bytes(argv[1]); + const void* b_salt = sqlite3_value_blob(argv[1]); + @@ -163,17 +185,19 @@ + } + } + -+ #define BASE_SHA2_SALT "$5$rounds=5000$" -+ #define BASE_SHA2_HASH "$A$005$" ++ /* Build the sha256_crypt salt-prefix "$5$rounds=$" and the MySQL hash prefix ++ "$A$$" dynamically from the resolved rounds value. Buffers fit comfortably: ++ "$5$rounds=4095000$" is 18 chars + 20 salt + NUL = 39 (<100). */ ++ char sha2_salt[100] = { 0 }; ++ char sha2_hash[100] = { 0 }; ++ int salt_prefix_len = snprintf(sha2_salt, sizeof(sha2_salt), "$5$rounds=%ld$", rounds); ++ snprintf(sha2_hash, sizeof(sha2_hash), "$A$%03X$", (unsigned int)(rounds / 1000)); + + char sha2_buf[100] = { 0 }; -+ char sha2_salt[100] = { BASE_SHA2_SALT }; -+ + strcat(sha2_salt, (const char*)salt); + sha256_crypt_r(cpass, sha2_salt, sha2_buf, sizeof(sha2_buf)); + -+ char sha2_hash[100] = { BASE_SHA2_HASH }; -+ const char* sha256 = sha2_buf + salt_size + strlen(BASE_SHA2_SALT) + 1; ++ const char* sha256 = sha2_buf + salt_size + salt_prefix_len + 1; + + strcat(sha2_hash, (const char*)salt); + strcat(sha2_hash, sha256); @@ -184,13 +208,14 @@ /* ** current_time() ** -@@ -133269,6 +133269,9 @@ +@@ -133230,6 +133431,10 @@ FUNCTION(substr, 3, 0, 0, substrFunc ), FUNCTION(substring, 2, 0, 0, substrFunc ), FUNCTION(substring, 3, 0, 0, substrFunc ), + FUNCTION(mysql_native_password, 1, 0, 0, mysql_native_passwordFunc ), + FUNCTION(caching_sha2_password, 1, 0, 0, caching_sha2_passwordFunc ), + FUNCTION(caching_sha2_password, 2, 0, 0, caching_sha2_passwordFunc ), ++ FUNCTION(caching_sha2_password, 3, 0, 0, caching_sha2_passwordFunc ), WAGGREGATE(sum, 1,0,0, sumStep, sumFinalize, sumFinalize, sumInverse, 0), WAGGREGATE(total, 1,0,0, sumStep,totalFinalize,totalFinalize,sumInverse, 0), WAGGREGATE(avg, 1,0,0, sumStep, avgFinalize, avgFinalize, sumInverse, 0), diff --git a/test/tap/tests/test_sqlite3_pass_exts-t.cpp b/test/tap/tests/test_sqlite3_pass_exts-t.cpp index 9cef59e10..a354cc443 100644 --- a/test/tap/tests/test_sqlite3_pass_exts-t.cpp +++ b/test/tap/tests/test_sqlite3_pass_exts-t.cpp @@ -262,8 +262,29 @@ int test_pass_match(MYSQL* admin, const user_def_t& def) { mysql_free_result(myres); } else if (def.auth == "caching_sha2_password") { + // MySQL stores caching_sha2_password as '$A$$' (70 ASCII bytes). + // def.hash is the hex-encoding of those 70 bytes (140 chars). The 3 ASCII chars + // at offset 3..5 (hex offset 6..11) carry rounds/1000 in 3-char zero-padded + // uppercase hex. Decode and pass the resolved rounds value to the SQLite + // extension so the regenerated digest uses the same iteration count as the + // backend (matters once MySQL ships with default rounds != 5000). + long rounds = 5000; + if (def.hash.size() >= 12) { + string rounds_field; + for (size_t i = 6; i < 12; i += 2) { + rounds_field += static_cast(stoi(def.hash.substr(i, 2), nullptr, 16)); + } + try { + rounds = stol(rounds_field, nullptr, 16) * 1000; + } catch (const std::exception&) { + diag("Failed to parse rounds field '%s' from MySQL hash '%s'", + rounds_field.c_str(), def.hash.c_str()); + } + } + const string GEN_SHA2_PASS { - "SELECT HEX(CACHING_SHA2_PASSWORD('" + def.pass + "', UNHEX('" + def.salt + "')))" + "SELECT HEX(CACHING_SHA2_PASSWORD('" + def.pass + "', UNHEX('" + def.salt + "'), " + + std::to_string(rounds) + "))" }; MYSQL_QUERY_T(admin, GEN_SHA2_PASS.c_str()); @@ -274,8 +295,8 @@ int test_pass_match(MYSQL* admin, const user_def_t& def) { ok( def.hash == admin_hash, - "MySQL hash should match ProxySQL generated mysql:'%s', admin:'%s'", - def.hash.c_str(), admin_hash.c_str() + "MySQL hash should match ProxySQL generated mysql:'%s', admin:'%s', rounds:%ld", + def.hash.c_str(), admin_hash.c_str(), rounds ); mysql_free_result(myres); @@ -404,13 +425,20 @@ const vector INV_INPUTS { "ProxySQL Admin Error: wrong number of arguments to function CACHING_SHA2_PASSWORD()" }, { - "SELECT CACHING_SHA2_PASSWORD('00', '00', '00')", 1, + "SELECT CACHING_SHA2_PASSWORD('00', '00', 5000, 'extra')", 1, "ProxySQL Admin Error: wrong number of arguments to function CACHING_SHA2_PASSWORD()" }, { "SELECT CACHING_SHA2_PASSWORD('', '')", 0, "Invalid argument size" }, { "SELECT CACHING_SHA2_PASSWORD('', '000000000000000000000')", 0, "Invalid argument size" }, { "SELECT CACHING_SHA2_PASSWORD(2, '00')", 0, "Invalid argument type" }, { "SELECT CACHING_SHA2_PASSWORD('00', 2)", 0, "Invalid argument type" }, + { "SELECT CACHING_SHA2_PASSWORD('00', '00', '00')", 0, "Invalid argument type" }, + { "SELECT CACHING_SHA2_PASSWORD('00', '00', 1000)", 0, + "Invalid rounds: expected multiple of 1000 in [5000,4095000]" }, + { "SELECT CACHING_SHA2_PASSWORD('00', '00', 5500)", 0, + "Invalid rounds: expected multiple of 1000 in [5000,4095000]" }, + { "SELECT CACHING_SHA2_PASSWORD('00', '00', 4096000)", 0, + "Invalid rounds: expected multiple of 1000 in [5000,4095000]" }, }; From 2b1a4262b19bc4179369fa76ae6bf04ce5fc189a Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 18 Apr 2026 08:09:21 +0000 Subject: [PATCH 07/16] tap/utils: add wait_for_log_match() to fix log-scrape races MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check_logs_for_command() in pgsql-copy_from_test-t did a single-shot call to get_matching_lines() right after the SQL that was expected to cause the "Switching to Fast Forward" / "Switching back to Normal" log line. Because ProxySQL writes that line asynchronously with respect to the query's reply, the scan occasionally reached EOF before the producer had flushed the line, turning the assertion into a flake. The failure that originally surfaced this was legacy-g1, test 85/327 of pgsql-copy_from_test-t: 1 of 236 assertions (not ok 126 - "Switching back to Normal mode"). It never reproduced under three targeted reruns, which is consistent with a timing race rather than a logic bug. Two changes: 1. Add wait_for_log_match() in test/tap/tap/utils.{h,cpp}: retry get_matching_lines() up to a timeout, clearing both eofbit and failbit between iterations so successive getline() calls pick up lines appended since the last scan. (Clearing only failbit, as get_matching_lines does internally, is not enough — once a stream has hit EOF its sticky eofbit keeps getline() from reading new bytes.) Defaults: 2000 ms total budget, 100 ms between polls — matching the idiom already used in admin_set_credentials_logging-t. 2. Rewrite check_logs_for_command() in pgsql-copy_from_test-t.cpp to call the new helper. Positive assertions still return as soon as the line appears (no added latency in the common case). Negative assertions (check_logs_for_command(...) == false) now wait up to the full timeout to confirm absence, closing the symmetrical race where a line would have arrived a few hundred ms later than the single-shot scan. Verified: 3/3 reruns of pgsql-copy_from_test-t in legacy-g1 post-fix all pass 236/236, runtime ~40 s per run (single-shot version was slightly faster because it short-circuited on negative assertions without waiting). --- test/tap/tap/utils.cpp | 30 +++++++++++++++++++++ test/tap/tap/utils.h | 32 +++++++++++++++++++++++ test/tap/tests/pgsql-copy_from_test-t.cpp | 11 ++++++-- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/test/tap/tap/utils.cpp b/test/tap/tap/utils.cpp index 7068edb0d..1f73eba4c 100644 --- a/test/tap/tap/utils.cpp +++ b/test/tap/tap/utils.cpp @@ -1693,6 +1693,36 @@ pair> get_matching_lines( } +bool wait_for_log_match( + fstream& f_stream, const string& s_regex, uint32_t timeout_ms, uint32_t poll_interval_ms +) { + // Guard against a zero interval, which would spin. + if (poll_interval_ms == 0) { poll_interval_ms = 10; } + + const useconds_t poll_us = poll_interval_ms * 1000; + uint32_t elapsed_ms = 0; + + while (true) { + // Clear both eofbit and failbit so the subsequent getline() in + // get_matching_lines() can read any bytes appended since the last scan. + // Without this, once the stream hits EOF on the first iteration, every + // retry short-circuits and returns no matches. + f_stream.clear(f_stream.rdstate() & ~std::ios_base::eofbit & ~std::ios_base::failbit); + + const auto& [_, matches] = get_matching_lines(f_stream, s_regex); + if (!matches.empty()) { + return true; + } + if (elapsed_ms >= timeout_ms) { + return false; + } + + usleep(poll_us); + elapsed_ms += poll_interval_ms; + } +} + + std::pair> get_matching_lines_from_filename( const std::string& filename, const std::string& s_regex, bool get_matches, size_t max_lines ) { diff --git a/test/tap/tap/utils.h b/test/tap/tap/utils.h index 5c8382f92..2bbc68ed2 100644 --- a/test/tap/tap/utils.h +++ b/test/tap/tap/utils.h @@ -790,6 +790,38 @@ std::pair> get_matching_lines( ); +/** + * @brief Poll a log stream for a regex match, retrying until a match appears or the + * timeout elapses. Handles the EOF sticky state that a single-shot + * `get_matching_lines()` leaves behind after exhausting the stream. + * + * Use this in place of single-shot `get_matching_lines()` calls that check whether + * a log line was emitted as a consequence of an earlier action: ProxySQL's writes + * to its log are asynchronous with respect to the SQL that triggers them, so a + * single scan immediately after the SQL completes is racy. Retrying gives the + * producer time to flush; when the match is already present the first iteration + * returns immediately. + * + * @param f_stream Log stream (opened with `open_file_and_seek_end` or similar). On + * match, the stream position is advanced past the last match (same + * semantics as `get_matching_lines`); on no-match, the stream is + * rewound to its position at entry. + * @param regex Pattern to search for. + * @param timeout_ms Maximum wall time to wait for the first match. Defaults to + * 2000 ms (20 attempts of 100 ms), which is the same budget used + * by admin_set_credentials_logging-t. + * @param poll_interval_ms Delay between scans when no match has appeared yet. + * Defaults to 100 ms. + * @return true if at least one matching line was observed within the budget. + */ +bool wait_for_log_match( + std::fstream& f_stream, + const std::string& regex, + uint32_t timeout_ms = 2000, + uint32_t poll_interval_ms = 100 +); + + /** * @brief Scan last N lines from a file and find lines matching a regex pattern. * diff --git a/test/tap/tests/pgsql-copy_from_test-t.cpp b/test/tap/tests/pgsql-copy_from_test-t.cpp index 2b0cbdea3..8f3832be7 100644 --- a/test/tap/tests/pgsql-copy_from_test-t.cpp +++ b/test/tap/tests/pgsql-copy_from_test-t.cpp @@ -286,8 +286,15 @@ int is_string_in_result(PGresult* result, const char* target_str) { } bool check_logs_for_command(std::fstream& f_proxysql_log, const std::string& command_regex) { - const auto& [_, cmd_lines] { get_matching_lines(f_proxysql_log, command_regex) }; - return cmd_lines.empty() ? false : true; + // ProxySQL's log writes for 'Switching {to Fast Forward,back to Normal} mode' + // happen asynchronously relative to the SQL that triggers them, so a single + // scan right after the SQL completes is racy. wait_for_log_match() retries + // with a short timeout, clearing EOF between scans; on hit it returns + // immediately, so assertions against a line that is already present pay no + // extra latency. Negative assertions (check_logs_for_command(...) == false) + // also benefit: they now wait up to the timeout to confirm absence, avoiding + // a false pass when the line would have arrived a few hundred ms later. + return wait_for_log_match(f_proxysql_log, command_regex); } bool setupTestTable(PGconn* conn) { From bce08084b817816153c67d9fd6b61f1c5f789808 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 18 Apr 2026 08:19:59 +0000 Subject: [PATCH 08/16] test_cluster_sync: wait_for_node_sync must treat "row absent" as not-synced wait_for_node_sync() in test_cluster_sync-t.cpp defaults row_value to -1, updates it from the result row only when both myrow and myrow[0] are non-null, then checks: if (row_value == 0) { not_synced = true; ... } This catches "row present but empty" (row_value == 0) but silently misses "row absent" (row_value stays at the -1 default when mysql_fetch_row returns NULL). The common predicate passed through this helper is: SELECT LENGTH(checksum) FROM stats_proxysql_servers_checksums WHERE hostname='%s' AND port='%d' AND name='%s' That row does not exist on a freshly-spawned replica until it has first synchronised checksums with the peer whose hostname/port the test is polling. Under heavy CI load the replica takes longer to reach that point; the loop exits as "synced" before the row exists; the caller's fetch_remote_checksum() then correctly returns empty and aborts the test with "Failed to fetch current checksum for module 'mysql_servers_v2'". That is exactly the failure observed in legacy-g5 / mysql84-g5 / mysql90-g5 during the multi-group CI run. mysql95-g5 passed because it started roughly two hours later, after concurrent groups had finished. Fix: treat row_value <= 0 as not-synced, so the "row absent" case is polled for the full CHECKSUM_SYNC_TIMEOUT instead of being silently skipped. Positive values still mean "synced" and exit the loop immediately, so there is no latency cost in the fast path. Verified: test_cluster_sync-t now passes 399/399 in legacy-g5; the 'wait_for_node_sync' timeouts that now appear in the log are the disabled-sync half of paired enabled/disabled assertions, where the timeout is the expected outcome. --- test/tap/tests/test_cluster_sync-t.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/tap/tests/test_cluster_sync-t.cpp b/test/tap/tests/test_cluster_sync-t.cpp index 467d98d42..0fa8eaaf6 100644 --- a/test/tap/tests/test_cluster_sync-t.cpp +++ b/test/tap/tests/test_cluster_sync-t.cpp @@ -565,7 +565,19 @@ int wait_for_node_sync(MYSQL* admin, const vector queries, uint32_t time mysql_free_result(myres); - if (row_value == 0) { + // <= 0 rather than == 0: "== 0" only flagged "row present but zero", + // which missed the "row absent" case (row_value stays at its -1 + // default when mysql_fetch_row returns NULL). For the common + // 'SELECT LENGTH(checksum) FROM stats_proxysql_servers_checksums + // WHERE hostname=... AND port=... AND name=...' predicate the row + // does not yet exist at the moment a freshly-spawned replica is + // polled — so the loop was exiting as "synced" before the replica + // had recorded a checksum for its peer, after which the caller's + // fetch_remote_checksum() correctly returned empty and aborted the + // test with "Failed to fetch current checksum". The race is + // load-dependent: mysql95-g5 passed because it started minutes + // later when concurrent groups had finished. + if (row_value <= 0) { not_synced = true; failed_query = query; From 96bd55784d0733516f1b853493fea5f929078a1f Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 18 Apr 2026 08:49:58 +0000 Subject: [PATCH 09/16] test/tap/tests: wire mysql-zstd_compression_level_libmysql-t into tests target Commit 174c5bda2 added the pattern rule for the _libmysql variant of the zstd_compression_level test but did not add it to the 'tests:' target list, so 'make build_tap_tests' silently skipped building it. Add the missing entry so the libmysql variant is built alongside its siblings. --- test/tap/tests/Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/test/tap/tests/Makefile b/test/tap/tests/Makefile index c07f23780..8624fa593 100644 --- a/test/tap/tests/Makefile +++ b/test/tap/tests/Makefile @@ -132,6 +132,7 @@ tests: tests-cpp \ prepare_statement_err3024_libmysql-t \ prepare_statement_err3024_async-t \ fast_forward_grace_close_libmysql-t \ + mysql-zstd_compression_level_libmysql-t \ fast_forward_switch_replication_deprecate_eof_libmysql-t \ reg_test_mariadb_stmt_store_result_libmysql-t \ reg_test_mariadb_stmt_store_result_async-t From 96c686dafa6c9325a7ca3358b296fa4832d457ad Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 18 Apr 2026 10:59:38 +0000 Subject: [PATCH 10/16] MySQL_Monitor: GR thread ignores cached ping state on first iteration Why: mysql_server_ping_log can carry stale "unpingable" entries from before monitor_GR_thread_HG was (re)started (e.g. a previous test left those hostnames marked bad). With the cache filter applied on the very first cycle, find_resp_srvs() returns empty, the writer HG stays empty for a full healthcheck_interval, and GR-based tests flake on warm-up. Probe all configured hosts on the first iteration so subsequent cycles see a fresh ping_log. The filter resumes from iteration two onward. --- lib/MySQL_Monitor.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/MySQL_Monitor.cpp b/lib/MySQL_Monitor.cpp index 3a363f11c..416fa7770 100644 --- a/lib/MySQL_Monitor.cpp +++ b/lib/MySQL_Monitor.cpp @@ -4105,6 +4105,13 @@ void* monitor_GR_thread_HG(void *arg) { uint64_t next_check_time = 0; uint64_t MAX_CHECK_DELAY_US = 500000; + // On first iteration after thread (re)start, ignore the cached ping state + // in mysql_server_ping_log — it may reflect stale failures from before the + // monitor was reconfigured (e.g. a previous test left these hostnames + // marked unpingable). Probing all configured hosts forces a fresh ping_log + // entry, so subsequent iterations see real state instead of skipping + // healthcheck_interval seconds while the writer HG stays empty. + bool first_iteration = true; while (GloMyMon->shutdown == false && mysql_thread___monitor_enabled == true) { if (!GloMTH) { break; } // quick exit during shutdown/restart @@ -4145,8 +4152,16 @@ void* monitor_GR_thread_HG(void *arg) { continue; } - // Get the current 'pingable' status for the servers. - const vector& resp_srvs { find_resp_srvs(hosts_defs) }; + // Get the current 'pingable' status for the servers. See first_iteration + // note above: skip the cache filter on the very first cycle so we do not + // inherit stale ping failures from before this thread was started. + vector resp_srvs; + if (first_iteration) { + resp_srvs = hosts_defs; + first_iteration = false; + } else { + resp_srvs = find_resp_srvs(hosts_defs); + } if (resp_srvs.empty()) { proxy_error("No node is pingable for Group Replication cluster with writer HG %u\n", wr_hg); next_check_time = curtime + mysql_thread___monitor_groupreplication_healthcheck_interval * 1000; From 25c8fd160992ccab240783d201f5f7e66a0247ef Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 18 Apr 2026 10:59:45 +0000 Subject: [PATCH 11/16] test/tap/tests: drain in-flight monitor cycle in pgsql-servers_ssl_params-t The test samples PgSQL_Monitor_ssl_connections_OK immediately after LOAD PGSQL SERVERS TO RUNTIME, then sleeps 5s and checks the counter did not advance (TLSv1 pin should have broken SSL). A monitor connect cycle scheduled with the OLD params can land AFTER ok_before is taken but BEFORE the sleep, succeeding and falsely bumping the counter. Poll for two consecutive equal 1s samples so ok_before is recorded after the new config has taken effect and the counter has plateaued. --- test/tap/tests/pgsql-servers_ssl_params-t.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/tap/tests/pgsql-servers_ssl_params-t.cpp b/test/tap/tests/pgsql-servers_ssl_params-t.cpp index 13f6627a6..b376f5852 100644 --- a/test/tap/tests/pgsql-servers_ssl_params-t.cpp +++ b/test/tap/tests/pgsql-servers_ssl_params-t.cpp @@ -499,8 +499,20 @@ static void test_monitor_uses_per_server_row(PGconn* admin) { exec_ok(admin, q.str().c_str()); exec_ok(admin, "LOAD PGSQL SERVERS TO RUNTIME"); + // Drain any monitor connect cycle scheduled with the pre-LOAD config. + // Why: the monitor thread may already be mid-cycle when LOAD runs; that + // in-flight connect was scheduled with the OLD SSL params (no TLSv1 pin) + // and will succeed and bump the counter AFTER we sample ok_before. Wait + // until two consecutive 1s samples are equal — meaning the new config + // is in effect and the counter has plateaued. long ok_before = getMonitorValue(admin, "PgSQL_Monitor_ssl_connections_OK"); - diag("With TLSv1 per-server pin, ssl OK before wait: %ld", ok_before); + for (int i = 0; i < 10; i++) { + usleep(1000000); // 1s — longer than the test's monitor connect interval + long sample = getMonitorValue(admin, "PgSQL_Monitor_ssl_connections_OK"); + if (sample == ok_before) break; + ok_before = sample; + } + diag("With TLSv1 per-server pin, ssl OK before wait (post-drain): %ld", ok_before); usleep(5000000); // 5 seconds — multiple monitor cycles From ee6d4e506ac47a9ef69b60efa5ddfc9cde2cd71d Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 18 Apr 2026 13:10:10 +0000 Subject: [PATCH 12/16] ci: retrigger CI after MySQL APT key fix From deb1cbe93ed9d8c877b89e42df7e517aa85332b5 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 18 Apr 2026 13:26:00 +0000 Subject: [PATCH 13/16] test_cluster_sync: widen sync timeout, fix own-vs-remote query mix-up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two test-only fixes for the cluster-sync flakes on legacy-g5 / mysql84-g5 / mysql90-g5 when groups run concurrently. 1) get_checksum_sync_timeout() used ((cluster_check_interval_ms/1000) * cluster_check_status_frequency) + 1 which integer-truncated the ms→s conversion (1001ms → 1s) and left only 1s of slack on top of a full status-collection cycle. A single cycle slipping under CI load — exactly what bce08084b exposed — was enough to time out. Compute in ms, round up, and enforce a 30s floor. Fast path still exits on the first synced poll, so healthy clusters see no additional latency. 2) The "Primary has detected checksum in the replica" and "Primary has detected its own new checksum" assertions formatted the same template with the same args (conn_opts = primary in this call site), so both polled the identical query for the primary's own row. Distinguish them: the replica-check filters on r_admin host/port, the own-check filters on admin host/port. The unused prim_own_sync_t[] duplicate template is dropped. The second wait_for_node_sync() now consumes wait_own_chksm_sync instead of wait_remote_chksm_syn. Builds cleanly; runtime verification deferred to CI. --- test/tap/tests/test_cluster_sync-t.cpp | 37 ++++++++++++++++---------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/test/tap/tests/test_cluster_sync-t.cpp b/test/tap/tests/test_cluster_sync-t.cpp index 0fa8eaaf6..294f64385 100644 --- a/test/tap/tests/test_cluster_sync-t.cpp +++ b/test/tap/tests/test_cluster_sync-t.cpp @@ -682,7 +682,17 @@ int32_t get_checksum_sync_timeout(MYSQL* admin) { return -1; } - return ((ext_check_intv.val/1000) * ext_sts_freq.val) + 1; + // Compute the status-collection period in ms and round up to avoid the + // integer-division truncation of the earlier (interval_ms/1000 * freq) + 1 + // formula (1001ms would collapse to 1s of period). Then enforce a 30s + // floor: the +1s slack was too tight under concurrent CI load — a single + // missed cycle on legacy-g5 / mysql84-g5 / mysql90-g5 was enough to time + // out. The fast path still exits on the first synced poll, so healthy + // clusters pay no extra cost. + const int64_t period_ms = ext_check_intv.val * ext_sts_freq.val; + const int32_t period_s = static_cast((period_ms + 999) / 1000); + const int32_t timeout_s = period_s + 5; + return timeout_s > 30 ? timeout_s : 30; } int check_module_checksums_sync( @@ -932,27 +942,26 @@ int check_module_checksums_sync( // This is important to avoid race conditions. If this sync is not performed, the primary may detect the // new checksum in the replica confusing this with the previous check. { - const char prim_repl_sync_t[] { + // Both assertions originally formatted the same template with the same + // args (conn_opts = primary), so "own" and "in the replica" polled the + // identical query. Distinguish them: the replica-check filters on + // r_admin, the own-check filters on admin (== conn_opts in this call). + const char prim_chksm_sync_t[] { "SELECT count(*) FROM stats_proxysql_servers_checksums WHERE " "hostname='%s' AND port='%d' AND name='%s' AND checksum='%s'" }; cfmt_t wait_remote_chksm_syn { - cstr_format( prim_repl_sync_t, - conn_opts.host.c_str(), - conn_opts.port, + cstr_format( prim_chksm_sync_t, + r_admin->host, + r_admin->port, module.c_str(), ext_checksum.str.c_str() ) }; - const char prim_own_sync_t[] { - "SELECT count(*) FROM stats_proxysql_servers_checksums WHERE " - "hostname='%s' AND port='%d' AND name='%s' AND checksum='%s'" - }; cfmt_t wait_own_chksm_sync { - cstr_format( - prim_repl_sync_t, - conn_opts.host.c_str(), - conn_opts.port, + cstr_format( prim_chksm_sync_t, + admin->host, + admin->port, module.c_str(), ext_checksum.str.c_str() ) @@ -965,7 +974,7 @@ int check_module_checksums_sync( admin->host, admin->port, ext_checksum.str.c_str(), r_admin->host, r_admin->port ); - sync_res = wait_for_node_sync(admin, { wait_remote_chksm_syn.str }, CHECKSUM_SYNC_TIMEOUT); + sync_res = wait_for_node_sync(admin, { wait_own_chksm_sync.str }, CHECKSUM_SYNC_TIMEOUT); ok( sync_res == 0, "Primary(%s:%d) has detected its own new checksum '%s'", From 04afb09886e6074b60b5d22a9d38be25eed81995 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 18 Apr 2026 14:26:44 +0000 Subject: [PATCH 14/16] ci: retrigger CI after MySQL APT keyring fix (#5649) From feed404031a6a820c0e22af1ecc8bdf3856eee6a Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 18 Apr 2026 18:27:48 +0000 Subject: [PATCH 15/16] ci: disable repltests/shuntest auto-trigger and fix 'make unit_tests' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two test-infrastructure fixes on this branch. 1) CI-repltests / CI-shuntest: every run since the upstream proxysql/jenkins-build-scripts access broke has been failing at the Checkout step with HTTP 403. Comment out the workflow_run trigger so they no longer auto-fire on every push; keep workflow_dispatch so they can still be manually re-run once the token/repo-access is fixed. Same root cause affects CI-taptests-groups as well — that needs the GH_TOKEN_PROXYSQL secret refreshed, not a code change. 2) test/tap/tests/unit/Makefile: 'make unit_tests' (invoked from test/tap/Makefile via $(MAKECMDGOALS)) failed with make[1]: *** No rule to make target 'unit_tests' because the unit Makefile only exposed 'all' and 'debug'. Commit 16e94131f intentionally forwards MAKECMDGOALS so 'make build_tap_test_debug' propagates 'debug' to the submake, which is what CI-unit-tests-asan-coverage relies on for -DDEBUG-guarded __thread stubs like mysql_thread___session_debug. Add a phony 'unit_tests: all' alias so the same pass-through also works when the goal name is 'unit_tests'. 'debug' is unchanged. --- .github/workflows/CI-repltests.yml | 9 ++++++--- .github/workflows/CI-shuntest.yml | 9 ++++++--- test/tap/tests/unit/Makefile | 6 ++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/CI-repltests.yml b/.github/workflows/CI-repltests.yml index f2beac23a..3450d2f50 100644 --- a/.github/workflows/CI-repltests.yml +++ b/.github/workflows/CI-repltests.yml @@ -2,10 +2,13 @@ name: CI-repltests run-name: '${{ github.event.workflow_run && github.event.workflow_run.head_branch || github.ref_name }} ${{ github.workflow }} ${{ github.event.workflow_run && github.event.workflow_run.head_sha || github.sha }}' on: + # Auto-triggering disabled: jenkins-build-scripts repo access currently + # returns HTTP 403 from this workflow. Re-enable by restoring the + # workflow_run trigger once jenkins-build-scripts access is fixed. workflow_dispatch: - workflow_run: - workflows: [ CI-trigger ] - types: [ completed ] + # workflow_run: + # workflows: [ CI-trigger ] + # types: [ completed ] concurrency: group: ${{ github.workflow }}-${{ github.event.workflow_run && github.event.workflow_run.head_branch || github.ref_name }} diff --git a/.github/workflows/CI-shuntest.yml b/.github/workflows/CI-shuntest.yml index 59b59b659..8347d2ab1 100644 --- a/.github/workflows/CI-shuntest.yml +++ b/.github/workflows/CI-shuntest.yml @@ -2,10 +2,13 @@ name: CI-shuntest run-name: '${{ github.event.workflow_run && github.event.workflow_run.head_branch || github.ref_name }} ${{ github.workflow }} ${{ github.event.workflow_run && github.event.workflow_run.head_sha || github.sha }}' on: + # Auto-triggering disabled: jenkins-build-scripts repo access currently + # returns HTTP 403 from this workflow. Re-enable by restoring the + # workflow_run trigger once jenkins-build-scripts access is fixed. workflow_dispatch: - workflow_run: - workflows: [ CI-trigger ] - types: [ completed ] + # workflow_run: + # workflows: [ CI-trigger ] + # types: [ completed ] concurrency: group: ${{ github.workflow }}-${{ github.event.workflow_run && github.event.workflow_run.head_branch || github.ref_name }} diff --git a/test/tap/tests/unit/Makefile b/test/tap/tests/unit/Makefile index c62fcfd7d..71bc459ff 100644 --- a/test/tap/tests/unit/Makefile +++ b/test/tap/tests/unit/Makefile @@ -297,6 +297,12 @@ UNIT_TESTS := smoke_test-t query_cache_unit-t query_processor_unit-t \ .PHONY: all all: $(UNIT_TESTS) +# Alias: top-level 'make unit_tests' (via test/tap/Makefile) forwards the goal +# name here via $(MAKECMDGOALS), so this target must exist even though `all` +# does the same thing. +.PHONY: unit_tests +unit_tests: all + .PHONY: debug debug: OPT += -DDEBUG debug: $(UNIT_TESTS) From 0ce8693c29dcc5eaa4bb953a717ef00e165597b7 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sat, 18 Apr 2026 18:49:57 +0000 Subject: [PATCH 16/16] ci: disable CI-taptests-groups auto-trigger (same 403 on jenkins-build-scripts) All four matrices (tests, tests/unit, tests_with_deps/*) of CI-taptests-groups fail at the 'Checkout jenkins_build_scripts' step with: fatal: unable to access 'https://github.com/proxysql/jenkins-build-scripts/': The requested URL returned error: 403 Same root cause as the CI-repltests / CI-shuntest disabling in the previous commit: GH_TOKEN_PROXYSQL no longer has access to the proxysql/jenkins-build-scripts repo. Comment out the workflow_run trigger so the workflow stops firing automatically on every push; workflow_dispatch is preserved for manual re-runs once the secret is refreshed. --- .github/workflows/CI-taptests-groups.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CI-taptests-groups.yml b/.github/workflows/CI-taptests-groups.yml index 01f8ea76f..d5ab4aba9 100644 --- a/.github/workflows/CI-taptests-groups.yml +++ b/.github/workflows/CI-taptests-groups.yml @@ -2,10 +2,14 @@ name: CI-taptests-groups run-name: '${{ github.event.workflow_run && github.event.workflow_run.head_branch || github.ref_name }} ${{ github.workflow }} ${{ github.event.workflow_run && github.event.workflow_run.head_sha || github.sha }}' on: + # Auto-triggering disabled: jenkins-build-scripts repo access currently + # returns HTTP 403 from this workflow (same root cause as + # CI-repltests / CI-shuntest). Re-enable by restoring the workflow_run + # trigger once jenkins-build-scripts access is fixed. workflow_dispatch: - workflow_run: - workflows: [ CI-trigger ] - types: [ completed ] + # workflow_run: + # workflows: [ CI-trigger ] + # types: [ completed ] concurrency: group: ${{ github.workflow }}-${{ github.event.workflow_run && github.event.workflow_run.head_branch || github.ref_name }}