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:
JaySon
2026-05-10 19:53:45 +08:00
committed by GitHub
parent 53ec1b95d8
commit bafff6d207
12 changed files with 131 additions and 58 deletions
+58
View File
@@ -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.
///
+2 -1
View File
@@ -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};
+1 -1
View File
@@ -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),
+15 -1
View File
@@ -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::*;
+1 -9
View File
@@ -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
+5 -15
View File
@@ -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}")),
}
}
+2 -10
View File
@@ -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();
+1 -9
View File
@@ -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)?,
+24
View File
@@ -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;
}