From 045ace29d55eb5283875ac807fcfb8d4e31cdbf5 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Mon, 27 Apr 2026 01:44:11 -0700 Subject: [PATCH] fix(seaweed-volume): parse host:port.grpcPort in master address (#9235) The Go ServerAddress format encodes an optional explicit gRPC port as host:port.grpcPort. The Rust heartbeat client only handled host:port (falling back to port+10000), so feeding it host:port.grpcPort yielded a malformed gRPC target like "host:port.grpcPort", which manifests as checkWithMaster transport errors. Mirror pb.ServerToGrpcAddress(): if the part after the last ':' contains a '.' followed by a valid u16, treat that suffix as the explicit gRPC port; otherwise keep the +10000 default. Refs #9234. --- seaweed-volume/src/server/heartbeat.rs | 34 ++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/seaweed-volume/src/server/heartbeat.rs b/seaweed-volume/src/server/heartbeat.rs index 434e841cf..b7ff32a85 100644 --- a/seaweed-volume/src/server/heartbeat.rs +++ b/seaweed-volume/src/server/heartbeat.rs @@ -169,10 +169,20 @@ pub async fn run_heartbeat_with_state( } } -/// Convert a master address "host:port" to a gRPC host:port target. -/// The Go master uses port + 10000 for gRPC by default. +/// Convert a master address to a gRPC `host:port` target. +/// +/// Mirrors Go's `pb.ServerToGrpcAddress()`: +/// - `host:port.grpcPort` returns `host:grpcPort` (explicit gRPC port). +/// - `host:port` returns `host:(port+10000)` (Go's default offset). +/// - Anything that fails to parse is returned unchanged. pub fn to_grpc_address(master_addr: &str) -> String { if let Some((host, port_str)) = master_addr.rsplit_once(':') { + // "host:port.grpcPort" — the part after the last '.' is the gRPC port. + if let Some((_, grpc_port)) = port_str.rsplit_once('.') { + if grpc_port.parse::().is_ok() { + return format!("{}:{}", host, grpc_port); + } + } if let Ok(port) = port_str.parse::() { let grpc_port = port + 10000; return format!("{}:{}", host, grpc_port); @@ -1038,6 +1048,26 @@ mod tests { }) } + #[test] + fn test_to_grpc_address_default_offset() { + assert_eq!(to_grpc_address("10.0.0.1:9333"), "10.0.0.1:19333"); + assert_eq!(to_grpc_address("localhost:9333"), "localhost:19333"); + } + + #[test] + fn test_to_grpc_address_explicit_grpc_port() { + // host:port.grpcPort form — gRPC port is what's after the dot. + assert_eq!(to_grpc_address("10.85.183.6:5300.6300"), "10.85.183.6:6300"); + assert_eq!(to_grpc_address("master.local:9333.19333"), "master.local:19333"); + } + + #[test] + fn test_to_grpc_address_returns_input_when_unparseable() { + assert_eq!(to_grpc_address(""), ""); + assert_eq!(to_grpc_address("no-port"), "no-port"); + assert_eq!(to_grpc_address("host:not-a-port"), "host:not-a-port"); + } + #[test] fn test_build_heartbeat_includes_store_identity_and_disk_metadata() { let temp_dir = tempfile::tempdir().unwrap();