test(rpc): observe bootstrap CAS during fresh startup

This commit is contained in:
overtrue
2026-09-06 13:01:22 +08:00
parent 6277287399
commit 9e24d23c30
7 changed files with 776 additions and 9 deletions
+1 -1
View File
@@ -68,7 +68,7 @@ license = []
io-scheduler-debug = [] # Enable debug information in I/O scheduler
tracing-chunk-debug = [] # Enable per-chunk tracing in data plane (high noise, for debugging only)
full = ["metrics-gpu", "ftps", "swift", "webdav", "sftp", "pyroscope", "gcs"]
e2e-test-hooks = []
e2e-test-hooks = ["rustfs-ecstore/e2e-test-hooks"]
# Shortens Connect credentials only in debug E2E builds.
connect-e2e-short-credentials = []
# Builds the dedicated rustfs-cli-e2e target with a build-time public enrollment root.
+50
View File
@@ -62,6 +62,29 @@ fn emit_fatal_stderr(context: &str, error: impl std::fmt::Display) {
}
async fn async_main() -> Result<()> {
#[cfg(feature = "e2e-test-hooks")]
if let Ok(nonce) = std::env::var("RUSTFS_E2E_STARTUP_CAS_PROBE") {
let nonce = uuid::Uuid::parse_str(&nonce).map_err(Error::other)?;
// This precedes CLI parsing and observability, including `--help`.
println!(
"RUSTFS_E2E_STARTUP_CAS {}",
serde_json::json!({
"kind": "capability", "schema": "fresh-startup-cas/v1", "nonce": nonce,
})
);
return Ok(());
}
#[cfg(feature = "e2e-test-hooks")]
if let Ok(nonce) = std::env::var("RUSTFS_E2E_STARTUP_CAS_NONCE") {
let nonce = uuid::Uuid::parse_str(&nonce).map_err(Error::other)?;
let line = format!(
"RUSTFS_E2E_STARTUP_CAS {}\n",
serde_json::json!({
"kind": "observer-ready", "nonce": nonce, "pid": std::process::id(),
})
);
let _ = std::io::Write::write_all(&mut std::io::stderr().lock(), line.as_bytes());
}
hotpath::tokio_runtime!();
// Log container resource detection early in startup
@@ -160,6 +183,33 @@ async fn run(config: Config) -> Result<()> {
shutdown_token: ctx,
} = init_startup_storage_runtime(server_addr, &endpoint_pools, readiness.clone(), instance_ctx).await?;
#[cfg(feature = "e2e-test-hooks")]
if let Ok(nonce) = std::env::var("RUSTFS_E2E_STARTUP_CAS_NONCE") {
let nonce = uuid::Uuid::parse_str(&nonce).map_err(Error::other)?;
let release = std::path::PathBuf::from(
std::env::var_os("RUSTFS_E2E_STARTUP_CAS_RELEASE")
.ok_or_else(|| Error::other("startup CAS fixture requires a release path"))?,
);
if server_ctx.installed_object_store().is_some() {
return Err(Error::other("startup CAS gate reached an installed slot"));
}
let line = format!(
"RUSTFS_E2E_STARTUP_CAS {}\n",
serde_json::json!({
"kind": "gate", "nonce": nonce, "pid": std::process::id(), "slot_installed": false,
})
);
let _ = std::io::Write::write_all(&mut std::io::stderr().lock(), line.as_bytes());
tokio::time::timeout(std::time::Duration::from_secs(180), async {
while !tokio::fs::try_exists(&release).await? {
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
Ok::<_, Error>(())
})
.await
.map_err(|_| Error::other("startup CAS gate release timed out"))??;
}
let capacity_tasks = crate::capacity::capacity_integration::init_capacity_management_managed().await;
let service_runtime = init_startup_runtime_services(
+35 -3
View File
@@ -39,6 +39,29 @@ use tonic::{Request, Response, Status};
use tracing::debug;
use uuid::Uuid;
#[cfg(feature = "e2e-test-hooks")]
fn startup_cas_rename_observation(
target: &LocalMutationTarget,
request: &RenameDataRequest,
file_info: &FileInfo,
) -> Option<serde_json::Value> {
use sha2::{Digest, Sha256};
if request.dst_volume != ".rustfs.sys" || !matches!(request.dst_path.as_str(), "pool.bin" | "pool.bin.identity") {
return None;
}
let nonce = uuid::Uuid::parse_str(&std::env::var("RUSTFS_E2E_STARTUP_CAS_NONCE").ok()?).ok()?;
let body = rustfs_protos::canonical_rename_data_request_body(request).ok()?;
Some(serde_json::json!({
"kind": "receiver", "nonce": nonce, "pid": std::process::id(),
"target": match target { LocalMutationTarget::Ready(_) => "ready", LocalMutationTarget::Bootstrap(_) => "bootstrap", LocalMutationTarget::Unbound => "unbound" },
"disk": request.disk, "src_volume": request.src_volume, "src_path": request.src_path,
"dst_volume": request.dst_volume, "dst_path": request.dst_path,
"body_sha256": format!("{:x}", Sha256::digest(body)),
"etag": file_info.metadata.get("etag"),
"mod_time": file_info.mod_time.map(|time| time.unix_timestamp_nanos().to_string()),
}))
}
impl LocalMutationTarget {
async fn rename_local_data(
&self,
@@ -1275,7 +1298,9 @@ impl NodeService {
Some(token)
};
let request_decoded_from_msgpack = decoded_file_info.from_msgpack;
match target
#[cfg(feature = "e2e-test-hooks")]
let observation = startup_cas_rename_observation(&target, &request, &decoded_file_info.value);
let result = target
.rename_local_data(
&request.disk,
(&request.src_volume, &request.src_path),
@@ -1283,8 +1308,15 @@ impl NodeService {
(&request.dst_volume, &request.dst_path),
scanner_publication_lease_token,
)
.await
{
.await;
#[cfg(feature = "e2e-test-hooks")]
if let Some(mut observation) = observation {
observation["ok"] = serde_json::json!(result.is_ok());
observation["error"] = serde_json::json!(result.as_ref().err().map(ToString::to_string));
let line = format!("RUSTFS_E2E_STARTUP_CAS {observation}\n");
let _ = std::io::Write::write_all(&mut std::io::stderr().lock(), line.as_bytes());
}
match result {
Ok(rename_data_resp) => match encode_rename_data_response_payloads(&rename_data_resp, request_decoded_from_msgpack) {
Ok((rename_data_resp, rename_data_resp_bin)) => Ok(Response::new(RenameDataResponse {
success: true,