diff --git a/plugins/mysqlx/src/mysqlx_plugin.cpp b/plugins/mysqlx/src/mysqlx_plugin.cpp index 1c098ef6e..f979e96cf 100644 --- a/plugins/mysqlx/src/mysqlx_plugin.cpp +++ b/plugins/mysqlx/src/mysqlx_plugin.cpp @@ -299,19 +299,24 @@ uint64_t monotonic_time_ms_local() { void mysqlx_populate_stats_processlist(SQLite3DB& statsdb) { MysqlxPluginContext& ctx = mysqlx_context(); - // Always wipe the projection table — empty thread pool or empty - // session list both mean "no active sessions" and the operator - // must see that, not stale rows from the last refresh. - statsdb.execute("DELETE FROM stats_mysqlx_processlist"); - - if (ctx.threads.empty()) return; - uint64_t now_ms = monotonic_time_ms_local(); std::vector rows; for (const auto& thr : ctx.threads) { if (thr) thr->snapshot_sessions_for_stats(rows, now_ms); } + // Atomic rebuild: DELETE + INSERTs run in a single transaction so a + // failure in any INSERT rolls everything back, leaving the previous + // projection in place rather than a half-populated (or empty) table. + // Reviewer feedback (CodeRabbit / Gemini on PR #5704) flagged that the + // previous bare DELETE-then-loop-INSERTs would leave operators staring + // at "no sessions" right after a transient SQLite error, which is far + // more misleading than "stale-but-recent" data. + if (!statsdb.execute("BEGIN")) return; + if (!statsdb.execute("DELETE FROM stats_mysqlx_processlist")) { + statsdb.execute("ROLLBACK"); + return; + } for (const auto& r : rows) { std::string sql = "INSERT INTO stats_mysqlx_processlist " "(username, route, worker_id, backend_host, backend_port, " @@ -328,8 +333,12 @@ void mysqlx_populate_stats_processlist(SQLite3DB& statsdb) { sql += ", "; sql += std::to_string(r.bytes_out); sql += ", "; sql += std::to_string(r.session_age_ms); sql += ")"; - statsdb.execute(sql.c_str()); + if (!statsdb.execute(sql.c_str())) { + statsdb.execute("ROLLBACK"); + return; + } } + statsdb.execute("COMMIT"); } // Default visibility is required because the plugin .so is built with diff --git a/plugins/mysqlx/src/mysqlx_session.cpp b/plugins/mysqlx/src/mysqlx_session.cpp index 4ff6ffdad..49770d420 100644 --- a/plugins/mysqlx/src/mysqlx_session.cpp +++ b/plugins/mysqlx/src/mysqlx_session.cpp @@ -1799,12 +1799,26 @@ void MysqlxSession::handler_connecting_server() { // above) using mysqlx_tls_backend_mode + per-endpoint // use_ssl override + frontend TLS state. Replicate the decision // here onto the freshly-allocated MysqlxConnection. + // + // Fail-closed contract: if backend TLS is desired but no SSL_CTX + // is available on this worker, refuse the connection rather than + // silently downgrading to plaintext. The earlier code had `if + // (desired_backend_tls) { if (ctx) { ... } }` — the inner ctx + // check was a guard, but the missing else-branch meant a + // missing/misconfigured SSL_CTX silently produced a plaintext + // connection in TLS-required and AsClient-on-TLS-frontend + // scenarios. CodeRabbit flagged this on PR #5707; fixed here. if (desired_backend_tls) { - if (thread_ptr_ && thread_ptr_->get_ssl_ctx()) { - backend_conn_->set_backend_tls_required(true); - backend_conn_->set_ssl_ctx(thread_ptr_->get_ssl_ctx()); - backend_conn_->set_backend_tls_fallback_allowed(tls_fallback_allowed); + SSL_CTX* ssl_ctx = thread_ptr_ ? thread_ptr_->get_ssl_ctx() : nullptr; + if (ssl_ctx == nullptr) { + send_error(2026, "Backend TLS required but no SSL context configured on this worker"); + delete backend_conn_; backend_conn_ = nullptr; + status_ = X_SESSION_CLOSING; healthy = false; + return; } + backend_conn_->set_backend_tls_required(true); + backend_conn_->set_ssl_ctx(ssl_ctx); + backend_conn_->set_backend_tls_fallback_allowed(tls_fallback_allowed); } if (identity_) { diff --git a/plugins/mysqlx/src/mysqlx_stats.cpp b/plugins/mysqlx/src/mysqlx_stats.cpp index 8d69e0594..c02645e99 100644 --- a/plugins/mysqlx/src/mysqlx_stats.cpp +++ b/plugins/mysqlx/src/mysqlx_stats.cpp @@ -38,6 +38,12 @@ MysqlxRouteStats& MysqlxStatsStore::get_or_create(const std::string& route_name, inserted->second.destination_hostgroup = destination_hostgroup; return inserted->second; } + // Refresh the metadata so a route whose destination_hostgroup was + // rebound (e.g. via LOAD MYSQLX ROUTES TO RUNTIME pointing the same + // route at a different hostgroup) reports the current target, not + // the hostgroup we first saw at the route's first traffic event. + // Counters are NOT reset — only metadata is refreshed. + it->second.destination_hostgroup = destination_hostgroup; return it->second; } @@ -97,7 +103,16 @@ std::optional> MysqlxStatsStore::get_last_conn_err_f void MysqlxStatsStore::flush_to_sqlite(SQLite3DB& statsdb) { std::lock_guard lock(mutex_); - statsdb.execute("DELETE FROM stats_mysqlx_routes"); + // Atomic rebuild: DELETE + INSERTs run in a single transaction so a + // transient SQLite error during the loop rolls back to the previous + // projection rather than leaving an empty stats_mysqlx_routes — which + // would mislead operators into thinking no traffic flowed. Same shape + // as mysqlx_populate_stats_processlist in mysqlx_plugin.cpp. + if (!statsdb.execute("BEGIN")) return; + if (!statsdb.execute("DELETE FROM stats_mysqlx_routes")) { + statsdb.execute("ROLLBACK"); + return; + } for (const auto& [name, stats] : route_stats_) { // Build with std::string so a long, escaped route name can never silently @@ -120,6 +135,10 @@ void MysqlxStatsStore::flush_to_sqlite(SQLite3DB& statsdb) { sql += ", "; sql += std::to_string(stats.bytes_recv.load(std::memory_order_relaxed)); sql += ")"; - statsdb.execute(sql.c_str()); + if (!statsdb.execute(sql.c_str())) { + statsdb.execute("ROLLBACK"); + return; + } } + statsdb.execute("COMMIT"); } diff --git a/plugins/mysqlx/src/mysqlx_thread.cpp b/plugins/mysqlx/src/mysqlx_thread.cpp index bbeeb0f2c..39e2099d9 100644 --- a/plugins/mysqlx/src/mysqlx_thread.cpp +++ b/plugins/mysqlx/src/mysqlx_thread.cpp @@ -444,6 +444,7 @@ const char* session_status_to_string(MysqlxSession::Status s) { case MysqlxSession::X_SESSION_CLOSING: return "X_SESSION_CLOSING"; case MysqlxSession::X_SESSION_CLOSED: return "X_SESSION_CLOSED"; case MysqlxSession::X_SESSION_RESET_WAITING: return "X_SESSION_RESET_WAITING"; + case MysqlxSession::X_PASSTHROUGH_FORWARD: return "X_PASSTHROUGH_FORWARD"; } return "UNKNOWN"; }