Skip to content

Dev/v0.5.0 resilience - #1

Merged
denerFernandes merged 19 commits into
masterfrom
dev/v0.5.0-resilience
May 14, 2026
Merged

denerFernandes merged 19 commits into
masterfrom
dev/v0.5.0-resilience

Conversation

@denerFernandes

Copy link
Copy Markdown
Member

Summary

This pull request introduces TurbineProxy v0.5.0, a major release focused on security hardening, high availability improvements, connection pooling enhancements, observability, and CI quality. Key highlights include migration to non-poisoning locks, SCRAM-SHA-256 fuzzing, new dashboard authentication endpoints, per-backend circuit breakers, connection multiplexing, and expanded Prometheus metrics. It also adds chaos testing infrastructure and stricter CI linting for critical code paths.

Security & Hardening

  • All internal mutexes and RW-locks now use parking_lot::{Mutex, RwLock} to eliminate lock poisoning, with panic-recovery tests added. [1] [2]
  • PostgreSQL SCRAM-SHA-256 authentication is fuzz-tested for robustness, ensuring correct handling of malformed and replayed messages.
  • Dashboard authentication failures are now tracked by a Prometheus counter (turbineproxy_dashboard_auth_failures_total). [1] [2] [3]

High Availability & Pooling

  • Per-backend circuit breakers prevent repeated failures from overloading backends; backends are only retried after a cooldown and health check passes. [1] [2] [3]
  • Failover events are protected by a configurable cooldown and minimum recovery checks to avoid flap/oscillation. [1] [2] [3]
  • Connection acquisition uses a bounded wait queue with a timeout, improving reliability under load. [1] [2]

Dashboard Authentication & API

  • New endpoints: POST /api/auth/refresh for token renewal, and POST /api/auth/logout for explicit session revocation. Read-only users can now refresh/logout. [1] [2]
  • Session tokens have configurable TTL; token_ttl_secs = 0 now means tokens never expire (breaking change).

Observability & CI

  • Prometheus metrics expanded: new gauges/counters for multiplexing efficiency, PostgreSQL replica lag, pinned sessions, and whitelist blocks. [1] [2]
  • CI now enforces clippy::unwrap_used lint on critical code paths (src/dashboard/, src/analytics/). [1] [2]
  • Added chaos testing suite and Docker Compose stack for resilience testing under network faults. [1] [2]

Other Notable Changes

  • PostgreSQL listener now supports all HA features previously available for MySQL, including TLS integration testing. [1] [2] [3]
  • Documentation and README updated to reflect new features, metrics, and configuration options. [1] [2] [3] [4] [5] [6] [7]

Please review for breaking changes in dashboard token configuration and expanded CI requirements.

Type of Change

  • feat — new feature
  • fix — bug fix
  • perf — performance improvement
  • refactor — code change without feat/fix
  • docs — documentation only
  • test — adding or updating tests
  • ci — CI/CD changes
  • chore — maintenance

Related Issue

Closes #

Checklist

  • Commit messages follow Conventional Commits
  • cargo fmt --all passes
  • cargo clippy --all-targets -- -D warnings passes
  • Tests added or updated for logic changes
  • cargo test --lib passes locally
  • Documentation updated (if applicable)

Testing

…eshold

- Add failover_cooldown_secs (default: 30) — hold failover active for N
  seconds after trigger before allowing recovery
- Add failover_min_recovery_checks (default: 3) — require N consecutive
  successful health checks before clearing failover
- Detect flap events when failover re-triggers within cooldown*2 window
- New Prometheus metric: turbineproxy_ha_failover_flap_total (counter)
- Log [HA] cooldown active when recovery checks pass but timer hasn't
- Log [HA] Failover FLAP detected on re-trigger within window
- Record failover_triggered_at timestamp and recovery_checks in BackendPool
- Apply same logic to both MySQL HealthChecker and PG PgHealthChecker
- PG trigger_failover now also increments failover_events_total

Docs:
- Update ha-failover.md with Flapping Protection section + alert rule
- Update configuration/reference.md with new fields in [ha] and [pgsql]
- Update failure-modes.md recovery description with cooldown semantics
- Update README feature table (14 metric families, flap protection)
- Update turbineproxy.example.toml with commented new options
- New lock-free circuit breaker module (src/proxy/circuit_breaker.rs)
  using atomics for hot-path performance
- States: Closed → Open (after N errors) → HalfOpen (probe after recovery_ms)
- Config: circuit_breaker_threshold (default 5), circuit_breaker_recovery_ms (default 10000)
- Integrated into BackendPool (replica_breakers + primary_breaker)
- Replica routing filters out Open backends in try_weighted_replica()
- Router records success/failure on both read and write paths
- Prometheus gauge: turbineproxy_circuit_breaker_state per-backend
- Log prefix [CB] for all state transitions
- Updated docs: ha-failover.md, failure-modes.md, example TOML
- Updated roadmap_v0.5.0.md — all CB items done
- New pool_wait_queue_size config (default 0 = reject-fast, legacy)
- New pool_wait_timeout_ms config (default 5000ms)
- When max_connections is hit and queue is enabled, requests wait on
  a tokio::sync::Semaphore instead of being rejected immediately
- Slot released back on connection return (put_for_database)
- Prometheus: turbineproxy_pool_wait_queue_length (gauge),
  turbineproxy_pool_wait_timeouts_total (counter)
- Log: [pool] wait queue timeout for backend ... (WARN)
- Config available for both MySQL and PostgreSQL listeners
- Updated failure-modes.md, example TOML, roadmap_v0.5.0.md
- Replace std Mutex/RwLock with parking_lot (no lock poisoning)
- Add per-backend circuit breaker (Closed/Open/Half-Open)
- Add bounded connection wait queue with timeout
- Add failover flap protection (cooldown + min recovery checks)
- Add connection multiplexing 1:N (Phase A)
- Add PostgreSQL HA parity (health, lag, failover, circuit breaker)
- Add SCRAM-SHA-256 hardening + fuzz corpus (15 invariants)
- Add POST /api/auth/refresh endpoint (atomic token rotation)
- Add POST /api/auth/logout with readonly support
- Add turbineproxy_dashboard_auth_failures_total Prometheus counter
- Add auth failure tracking in login, middleware, and refresh
- Add panic recovery unit tests for parking_lot Mutex and RwLock
- Add PG TLS integration test (auto-skip without psql/infra)
- Add clippy::unwrap_used CI lint for dashboard and analytics paths
- Add chaos testing suite and chaos CI workflow
- Bump version to 0.5.0
- Update README, CHANGELOG, and docs
set TURBINEPROXY_SKIP_DASHBOARD_BUILD=1 globally so build.rs does not
attempt to run 'npm run build' (vite) in jobs that have no node.js.
the dashboard is built separately in the frontend-tests job.
turbineproxy is a binary-only crate (no src/lib.rs),
so cargo test --lib fails with 'no library targets found'.
- routes.rs: sort_by -> sort_by_key for p95/count sorts
- postgres/mod.rs: collapse if into match guard for b'K' arm
- app_analytics.rs: sort_by -> sort_by_key for queries_total
- heatmap.rs: sort_by -> sort_by_key for queries
- stmt_shadow.rs: collapse if into match guard for b'C' arm
- tracer.rs: sort_by -> sort_by_key for pair counts
adds npm overrides to force a patched version of serialize-javascript,
fixing GHSA-5c6j-r48x-rmvq (RCE via RegExp.flags) and
GHSA-qj8w-gfj5-8c6v (cpu exhaustion dos) without downgrading docusaurus.
mysqladmin ping fails on mariadb 10.11/11.4 in github actions.
use the official 'healthcheck.sh --connect --innodb_initialized'
script (bundled in all official mariadb images) for mariadb variants.
mysql variants keep mysqladmin ping as before.

also add --health-start-period=30s and bump retries to 20 to allow
more time for initialization in slow ci runners.
ci runners (especially with mysql 8.4) are slower to initialize.
also inherit stderr so proxy startup errors surface in test output
instead of being silently discarded.
two root causes:

1. mysql 8.4 disabled mysql_native_password by default.
   the proxy pool auth fails with 'early eof' on every backend
   connection attempt. fix: add a ci step that uses docker exec
   to reconfigure root user with mysql_native_password before
   running tests. container is named 'mysql-service' via --name
   in the service options to make it addressable by docker exec.

2. when start_proxy() panicked after the 30s timeout, the OnceLock
   was left uninitialized. subsequent test threads would re-run the
   closure, spawn a second proxy process and fail with 'address
   already in use'. fix: start_proxy() now returns bool instead of
   panicking — on timeout it kills the child and returns false, which
   sets OnceLock to false so no further proxy spawns occur and all
   tests are cleanly skipped via require_proxy!().
…ysql 8.4

mysql 8.4 completely removed the mysql_native_password plugin, so
ALTER USER cannot re-enable it at runtime (error: plugin not loaded).

the correct fix is to start mysqld with --mysql-native-passwords=ON.
add a db-cmd field to every matrix entry and pass it as the service
container command. for mariadb, db-cmd is empty so docker falls back
to the image default CMD.

removes the failing docker exec step added in the previous commit.
…b containers

empty command: "" in github actions service containers overrides the
image default CMD with an empty string, causing the container to fail.

set db-cmd to 'mariadbd' (the actual default CMD for mariadb 10.11
and 11.4 official images) so the container starts with the same
behaviour as without a command override.
… mysql 8.4

the mysqld option name has no trailing 's'.
'mysql-native-passwords=ON' is unknown; the correct flag is
'--mysql-native-password=ON'.
--mysql-native-password=ON loads the plugin at startup, but the root
user is still initialized with caching_sha2_password by the docker
entrypoint init script. the proxy pool auth fails because it only
implements mysql_native_password handshake.

add a step that runs after the healthy service to ALTER USER and switch
root@% and root@localhost to mysql_native_password. this now succeeds
because the plugin is loaded (unlike the earlier attempt where the plugin
was completely absent).
…fault

--mysql-native-password=ON loads the plugin but does not change the
default auth method. the docker init script still creates root with
caching_sha2_password, and the proxy's pool auth fails because it
only implements the mysql_native_password handshake (no AuthSwitch).

mysql 8.4 removed --default-authentication-plugin. the replacement is
--authentication-policy=mysql_native_password,, which sets native
password as the default first-factor auth plugin. combined with
--mysql-native-password=ON, this makes the docker init script create
root with mysql_native_password from the start.

removes the docker exec ALTER USER step (no longer needed since root
is now created with the correct plugin).
COM_STMT_CLOSE and COM_STMT_SEND_LONG_DATA are fire-and-forget: MySQL
sends NO response for these commands. the proxy's send_raw() always
calls collect_response_tracked() which blocks on read_exact() forever
waiting for bytes that never arrive — deadlocking the connection.

add send_raw_no_response() to BackendConnection trait (write only, no
read). use it in route_stmt_mysql_shadow for CLOSE and SEND_LONG_DATA.
return an empty BackendResponse so the caller skips writing to the
client (these commands also expect no client-side response).

this fixes integration test hangs on MySQL 8.0 where prepared
statement tests (test_prepared_insert_select and later) would block
indefinitely when the mysql crate closes a prepared statement.
the response to com_stmt_prepare is a unique multi-packet sequence:
  1. prepare_ok packet (status=0x00, stmt_id, num_columns, num_params, ...)
  2. if num_params > 0: num_params column-def packets + eof
  3. if num_columns > 0: num_columns column-def packets + eof

collect_response_tracked() saw the leading 0x00 byte and treated it as
a regular ok packet, parsed the stmt_id bytes as garbage lenenc fields,
and returned after the first packet. the remaining param/column-def
packets stayed in the tcp buffer, corrupting the protocol stream for
the next command and causing the proxy to hang waiting for response
bytes that were actually unrelated leftover payload.

add collect_prepare_response() which reads the prepare_ok packet,
extracts num_columns and num_params, then drains the exact number of
column-def packets + eofs. send_raw() now detects the com_stmt_prepare
command byte and uses this collector instead.

this is the actual root cause of the prepared statement hang on mysql
8.0 (the previous fix for com_stmt_close was correct but not sufficient
on its own — close was never reached because prepare's reply stream
was already corrupted).
The transaction state was being set to false BEFORE routing, causing
COMMIT/ROLLBACK to go to a fresh pool connection instead of the sticky
tx_conn where the transaction was actually open. This resulted in
committed data appearing lost (the original tx_conn was silently rolled
back on drop).

Now we defer set_in_transaction(false) until AFTER the COMMIT/ROLLBACK
is sent through the tx_conn, then release the connection to the pool.
@denerFernandes
denerFernandes merged commit 9eeb503 into master May 14, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant