mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
feat(targets): add check_mysql_server_available probe function (#2884)
Signed-off-by: JaySon-Huang <tshent@qq.com> Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
@@ -123,6 +123,64 @@ pub async fn check_pulsar_broker_available(args: &crate::target::pulsar::PulsarA
|
||||
.unwrap_or_else(|_| Err(crate::TargetError::Timeout("Pulsar connection timed out".to_string())))
|
||||
}
|
||||
|
||||
/// Probes a MySQL server for connectivity.
|
||||
///
|
||||
/// 1. Validates `args`.
|
||||
/// 2. Parses the DSN and builds a connection pool.
|
||||
/// 3. Runs `SELECT 1` to confirm credentials work.
|
||||
pub async fn check_mysql_server_available(args: &crate::target::mysql::MySqlArgs) -> Result<(), crate::TargetError> {
|
||||
use crate::target::ensure_rustls_provider_installed;
|
||||
use crate::target::mysql::{MySqlDsn, map_mysql_error};
|
||||
use mysql_async::{Opts, OptsBuilder, Pool, SslOpts, prelude::Queryable};
|
||||
use std::path::PathBuf;
|
||||
|
||||
args.validate()?;
|
||||
|
||||
let dsn = MySqlDsn::parse(&args.dsn_string)?;
|
||||
|
||||
let mut builder = OptsBuilder::default()
|
||||
.user(Some(dsn.user.clone()))
|
||||
.pass(Some(dsn.password.clone()))
|
||||
.ip_or_hostname(dsn.host.clone())
|
||||
.tcp_port(dsn.port)
|
||||
.db_name(Some(dsn.database.clone()));
|
||||
|
||||
if dsn.tls {
|
||||
ensure_rustls_provider_installed();
|
||||
let mut ssl_opts = SslOpts::default();
|
||||
if !args.tls_ca.is_empty() {
|
||||
ssl_opts = ssl_opts.with_root_certs(vec![PathBuf::from(args.tls_ca.clone()).into()]);
|
||||
}
|
||||
if !args.tls_client_cert.is_empty() && !args.tls_client_key.is_empty() {
|
||||
let identity = mysql_async::ClientIdentity::new(
|
||||
PathBuf::from(args.tls_client_cert.clone()).into(),
|
||||
PathBuf::from(args.tls_client_key.clone()).into(),
|
||||
);
|
||||
ssl_opts = ssl_opts.with_client_identity(Some(identity));
|
||||
}
|
||||
builder = builder.ssl_opts(Some(ssl_opts));
|
||||
}
|
||||
|
||||
let pool = Pool::new(Opts::from(builder));
|
||||
// Pool is dropped at scope exit; pool.disconnect() is deliberately
|
||||
// avoided — integration tests show it hangs indefinitely, exceeding
|
||||
// the 8s timeout. Drops handle cleanup without blocking.
|
||||
|
||||
let timeout = std::time::Duration::from_secs(8);
|
||||
tokio::time::timeout(timeout, async {
|
||||
let mut conn = pool
|
||||
.get_conn()
|
||||
.await
|
||||
.map_err(|err| map_mysql_error(err, "MySQL connectivity probe failed to acquire connection"))?;
|
||||
conn.query_drop("SELECT 1")
|
||||
.await
|
||||
.map_err(|err| map_mysql_error(err, "MySQL connectivity probe failed"))?;
|
||||
Ok::<(), crate::TargetError>(())
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|_| Err(crate::TargetError::Timeout("MySQL connectivity probe timed out".to_string())))
|
||||
}
|
||||
|
||||
/// Probes a PostgreSQL server for connectivity and verifies the configured
|
||||
/// table is readable.
|
||||
///
|
||||
|
||||
@@ -23,7 +23,8 @@ pub mod target;
|
||||
|
||||
pub use check::{
|
||||
check_amqp_broker_available, check_kafka_broker_available, check_mqtt_broker_available, check_mqtt_broker_available_with_tls,
|
||||
check_nats_server_available, check_postgres_server_available, check_pulsar_broker_available, check_redis_server_available,
|
||||
check_mysql_server_available, check_nats_server_available, check_postgres_server_available, check_pulsar_broker_available,
|
||||
check_redis_server_available,
|
||||
};
|
||||
pub use error::{StoreError, TargetError};
|
||||
pub use plugin::{BuiltinTargetDescriptor, TargetPluginDescriptor, TargetPluginRegistry, TargetRequestValidator, boxed_target};
|
||||
|
||||
@@ -31,7 +31,7 @@ pub enum TargetRequestValidator {
|
||||
Mqtt,
|
||||
Amqp(crate::target::TargetType),
|
||||
Kafka(crate::target::TargetType),
|
||||
MySql,
|
||||
MySql(crate::target::TargetType),
|
||||
Nats(crate::target::TargetType),
|
||||
Postgres(crate::target::TargetType),
|
||||
Pulsar(crate::target::TargetType),
|
||||
|
||||
@@ -23,7 +23,7 @@ use std::fmt::Formatter;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tracing::warn;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
pub mod amqp;
|
||||
pub mod kafka;
|
||||
@@ -440,6 +440,20 @@ pub(crate) fn delete_stored_payload(
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures a rustls crypto provider is installed before any TLS operation.
|
||||
///
|
||||
/// Multiple target modules (MySQL, Redis, Postgres, MQTT) need this because
|
||||
/// each may be the first to perform a TLS handshake. Idempotent: if a
|
||||
/// provider is already registered, returns immediately.
|
||||
pub(crate) fn ensure_rustls_provider_installed() {
|
||||
if rustls::crypto::CryptoProvider::get_default().is_some() {
|
||||
return;
|
||||
}
|
||||
if let Err(err) = rustls::crypto::aws_lc_rs::default_provider().install_default() {
|
||||
debug!("rustls provider already installed or unavailable: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -176,14 +176,6 @@ fn websocket_broker_url(broker: &Url, secure: bool) -> Result<String, TargetErro
|
||||
Ok(url.to_string())
|
||||
}
|
||||
|
||||
fn ensure_rustls_provider_installed() {
|
||||
if rustls::crypto::CryptoProvider::get_default().is_none()
|
||||
&& rustls::crypto::aws_lc_rs::default_provider().install_default().is_err()
|
||||
{
|
||||
debug!("rustls crypto provider was installed concurrently, skipping aws-lc-rs install");
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_path_is_absolute(path: &str, field: &str) -> Result<(), TargetError> {
|
||||
if !Path::new(path).is_absolute() {
|
||||
return Err(TargetError::Configuration(format!("{field} must be an absolute path")));
|
||||
@@ -215,7 +207,7 @@ fn build_root_store(ca_path: &str, trust_leaf_as_ca: bool) -> Result<rustls::Roo
|
||||
}
|
||||
|
||||
fn build_mqtt_tls_transport(broker: &Url, tls: &MQTTTlsConfig) -> Result<Transport, TargetError> {
|
||||
ensure_rustls_provider_installed();
|
||||
super::ensure_rustls_provider_installed();
|
||||
|
||||
let client_config = match tls
|
||||
.policy
|
||||
|
||||
@@ -304,16 +304,6 @@ fn is_valid_identifier_segment(segment: &str) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn ensure_rustls_provider_installed() {
|
||||
if rustls::crypto::CryptoProvider::get_default().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = rustls::crypto::aws_lc_rs::default_provider().install_default() {
|
||||
debug!("rustls provider already installed or unavailable for mysql target: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validate_table_name(table: &str) -> Result<(), TargetError> {
|
||||
let table = table.trim();
|
||||
|
||||
@@ -571,7 +561,7 @@ where
|
||||
.db_name(Some(dsn.database.clone()));
|
||||
|
||||
if dsn.tls {
|
||||
ensure_rustls_provider_installed();
|
||||
super::ensure_rustls_provider_installed();
|
||||
let mut ssl_opts = SslOpts::default();
|
||||
if !self.args.tls_ca.is_empty() {
|
||||
ssl_opts = ssl_opts.with_root_certs(vec![PathBuf::from(self.args.tls_ca.clone()).into()]);
|
||||
@@ -666,7 +656,7 @@ where
|
||||
|
||||
conn.exec_drop(sql, (event_time.as_str(), event_data))
|
||||
.await
|
||||
.map_err(map_mysql_error)?;
|
||||
.map_err(|err| map_mysql_error(err, "Failed to insert event"))?;
|
||||
|
||||
self.delivery_counters.record_success();
|
||||
debug!(target_id = %self.id, "MySQL event inserted");
|
||||
@@ -690,16 +680,16 @@ where
|
||||
/// - `Server(1213|1205|1040)` → `Timeout` (deadlock/lock timeout/too
|
||||
/// many connections, exponential-backoff retry)
|
||||
/// - everything else → `Request` (permanent failure)
|
||||
fn map_mysql_error(err: mysql_async::Error) -> TargetError {
|
||||
pub(crate) fn map_mysql_error(err: mysql_async::Error, operation: &str) -> TargetError {
|
||||
match &err {
|
||||
mysql_async::Error::Io(_) | mysql_async::Error::Driver(_) => TargetError::NotConnected,
|
||||
mysql_async::Error::Server(server_err) => match server_err.code {
|
||||
1213 | 1205 | 1040 => {
|
||||
TargetError::Timeout(format!("MySQL transient server error {}: {}", server_err.code, server_err.message))
|
||||
}
|
||||
_ => TargetError::Request(format!("Failed to insert event: {err}")),
|
||||
_ => TargetError::Request(format!("{operation}: {err}")),
|
||||
},
|
||||
_ => TargetError::Request(format!("Failed to insert event: {err}")),
|
||||
_ => TargetError::Request(format!("{operation}: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use tokio_postgres::Config;
|
||||
use tokio_postgres_rustls::MakeRustlsConnect;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
use tracing::{error, info, instrument, warn};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -398,14 +398,6 @@ pub fn table_probe_sql(schema: &str, table: &str) -> String {
|
||||
format!("SELECT 1 FROM {} LIMIT 0", qualified_table(schema, table))
|
||||
}
|
||||
|
||||
fn ensure_rustls_provider_installed() {
|
||||
if rustls::crypto::CryptoProvider::get_default().is_none()
|
||||
&& rustls::crypto::aws_lc_rs::default_provider().install_default().is_err()
|
||||
{
|
||||
debug!("rustls crypto provider was installed concurrently, skipping aws-lc-rs install");
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a rustls `ClientConfig` for the PostgreSQL connection.
|
||||
///
|
||||
/// When `tls_ca` is empty the OS native trust store is used via
|
||||
@@ -413,7 +405,7 @@ fn ensure_rustls_provider_installed() {
|
||||
/// When `tls_client_cert` and `tls_client_key` are both set the connection
|
||||
/// uses mTLS authentication; otherwise no client cert is sent.
|
||||
pub fn build_tls_config(args: &PostgresArgs) -> Result<rustls::ClientConfig, TargetError> {
|
||||
ensure_rustls_provider_installed();
|
||||
super::ensure_rustls_provider_installed();
|
||||
|
||||
let mut root_store = rustls::RootCertStore::empty();
|
||||
|
||||
|
||||
@@ -290,14 +290,6 @@ fn validate_redis_tls_config(url: &Url, tls: &RedisTlsConfig) -> Result<(), Targ
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_rustls_provider_installed() {
|
||||
if rustls::crypto::CryptoProvider::get_default().is_none()
|
||||
&& rustls::crypto::aws_lc_rs::default_provider().install_default().is_err()
|
||||
{
|
||||
debug!("rustls crypto provider was installed concurrently, skipping aws-lc-rs install");
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RedisTarget<E>
|
||||
where
|
||||
E: Send + Sync + 'static + Clone + Serialize + DeserializeOwned,
|
||||
@@ -656,7 +648,7 @@ pub(crate) fn build_redis_client(args: &RedisArgs) -> Result<Client, TargetError
|
||||
|
||||
let secure_scheme = matches!(args.url.scheme(), "rediss" | "valkeys");
|
||||
if secure_scheme {
|
||||
ensure_rustls_provider_installed();
|
||||
super::ensure_rustls_provider_installed();
|
||||
let tls_certs = TlsCertificates {
|
||||
client_tls: read_client_tls(&args.tls)?,
|
||||
root_cert: read_root_cert(&args.tls)?,
|
||||
|
||||
@@ -266,3 +266,27 @@ async fn incompatible_schema_init_fails() {
|
||||
|
||||
drop_table(&dsn, &table).await;
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn check_mysql_server_available_succeeds_against_existing_table() {
|
||||
let dsn = test_dsn();
|
||||
let table = table_name("test_check");
|
||||
|
||||
{
|
||||
let pool = build_test_pool(&dsn);
|
||||
let mut conn = pool.get_conn().await.expect("get conn");
|
||||
conn.query_drop(format!(
|
||||
"CREATE TABLE `{table}` (event_time DATETIME(6) NOT NULL, event_data JSON NOT NULL)"
|
||||
))
|
||||
.await
|
||||
.expect("create table");
|
||||
}
|
||||
|
||||
let args = make_args(&dsn, &table, "");
|
||||
rustfs_targets::check_mysql_server_available(&args)
|
||||
.await
|
||||
.expect("connectivity probe should succeed against existing table");
|
||||
|
||||
drop_table(&dsn, &table).await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user