diff --git a/seaweed-volume/src/server/grpc_server.rs b/seaweed-volume/src/server/grpc_server.rs index f7387faf7..563a09cf4 100644 --- a/seaweed-volume/src/server/grpc_server.rs +++ b/seaweed-volume/src/server/grpc_server.rs @@ -3575,3 +3575,43 @@ fn get_process_rss_linux() -> Option { } Some(resident * page_size as u64) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_grpc_address_with_explicit_grpc_port() { + // Format: "ip:port.grpcPort" — used by SeaweedFS for source_data_node + let result = parse_grpc_address("192.168.1.66:8080.18080").unwrap(); + assert_eq!(result, "192.168.1.66:18080"); + } + + #[test] + fn test_parse_grpc_address_with_implicit_grpc_port() { + // Format: "ip:port" — grpc port = port + 10000 + let result = parse_grpc_address("192.168.1.66:8080").unwrap(); + assert_eq!(result, "192.168.1.66:18080"); + } + + #[test] + fn test_parse_grpc_address_localhost() { + let result = parse_grpc_address("localhost:9333").unwrap(); + assert_eq!(result, "localhost:19333"); + } + + #[test] + fn test_parse_grpc_address_with_ipv4_dots() { + // Regression: naive split on '.' breaks on IP addresses + let result = parse_grpc_address("10.0.0.1:8080.18080").unwrap(); + assert_eq!(result, "10.0.0.1:18080"); + + let result = parse_grpc_address("10.0.0.1:8080").unwrap(); + assert_eq!(result, "10.0.0.1:18080"); + } + + #[test] + fn test_parse_grpc_address_invalid() { + assert!(parse_grpc_address("no-colon").is_err()); + } +} diff --git a/seaweed-volume/src/storage/erasure_coding/ec_encoder.rs b/seaweed-volume/src/storage/erasure_coding/ec_encoder.rs index cc5e4b639..1afb830fb 100644 --- a/seaweed-volume/src/storage/erasure_coding/ec_encoder.rs +++ b/seaweed-volume/src/storage/erasure_coding/ec_encoder.rs @@ -541,4 +541,118 @@ mod tests { assert_eq!(shard_opts[0].as_ref().unwrap(), &original_0); assert_eq!(shard_opts[1].as_ref().unwrap(), &original_1); } + + /// EC encode must read .idx from a separate index directory when configured. + #[test] + fn test_ec_encode_with_separate_idx_dir() { + let dat_tmp = TempDir::new().unwrap(); + let idx_tmp = TempDir::new().unwrap(); + let dat_dir = dat_tmp.path().to_str().unwrap(); + let idx_dir = idx_tmp.path().to_str().unwrap(); + + // Create a volume with separate data and index directories + let mut v = Volume::new( + dat_dir, + idx_dir, + "", + VolumeId(1), + NeedleMapKind::InMemory, + None, + None, + 0, + Version::current(), + ) + .unwrap(); + + for i in 1..=5 { + let data = format!("needle {} payload", i); + let mut n = Needle { + id: NeedleId(i), + cookie: Cookie(i as u32), + data: data.as_bytes().to_vec(), + data_size: data.len() as u32, + ..Needle::default() + }; + v.write_needle(&mut n, true).unwrap(); + } + v.sync_to_disk().unwrap(); + v.close(); + + // Verify .dat is in data dir, .idx is in idx dir + assert!(std::path::Path::new(&format!("{}/1.dat", dat_dir)).exists()); + assert!(!std::path::Path::new(&format!("{}/1.idx", dat_dir)).exists()); + assert!(std::path::Path::new(&format!("{}/1.idx", idx_dir)).exists()); + assert!(!std::path::Path::new(&format!("{}/1.dat", idx_dir)).exists()); + + // EC encode with separate idx dir + let data_shards = 10; + let parity_shards = 4; + let total_shards = data_shards + parity_shards; + write_ec_files(dat_dir, idx_dir, "", VolumeId(1), data_shards, parity_shards).unwrap(); + + // Verify all 14 shard files in data dir + for i in 0..total_shards { + let path = format!("{}/1.ec{:02}", dat_dir, i); + assert!( + std::path::Path::new(&path).exists(), + "shard {} should exist in data dir", + path + ); + } + + // Verify .ecx in data dir (not idx dir) + assert!(std::path::Path::new(&format!("{}/1.ecx", dat_dir)).exists()); + assert!(!std::path::Path::new(&format!("{}/1.ecx", idx_dir)).exists()); + + // Verify no shard files leaked into idx dir + for i in 0..total_shards { + let path = format!("{}/1.ec{:02}", idx_dir, i); + assert!( + !std::path::Path::new(&path).exists(), + "shard {} should NOT exist in idx dir", + path + ); + } + } + + /// EC encode should fail gracefully when .idx is only in the data dir + /// but we pass a wrong idx_dir. This guards against regressions where + /// write_ec_files ignores the idx_dir parameter. + #[test] + fn test_ec_encode_fails_with_wrong_idx_dir() { + let dat_tmp = TempDir::new().unwrap(); + let idx_tmp = TempDir::new().unwrap(); + let wrong_tmp = TempDir::new().unwrap(); + let dat_dir = dat_tmp.path().to_str().unwrap(); + let idx_dir = idx_tmp.path().to_str().unwrap(); + let wrong_dir = wrong_tmp.path().to_str().unwrap(); + + let mut v = Volume::new( + dat_dir, + idx_dir, + "", + VolumeId(1), + NeedleMapKind::InMemory, + None, + None, + 0, + Version::current(), + ) + .unwrap(); + + let mut n = Needle { + id: NeedleId(1), + cookie: Cookie(1), + data: b"hello".to_vec(), + data_size: 5, + ..Needle::default() + }; + v.write_needle(&mut n, true).unwrap(); + v.sync_to_disk().unwrap(); + v.close(); + + // Should fail: .idx is in idx_dir, not wrong_dir + let result = write_ec_files(dat_dir, wrong_dir, "", VolumeId(1), 10, 4); + assert!(result.is_err(), "should fail when idx_dir doesn't contain .idx"); + } } diff --git a/seaweed-volume/src/storage/volume.rs b/seaweed-volume/src/storage/volume.rs index 579094584..18a15974f 100644 --- a/seaweed-volume/src/storage/volume.rs +++ b/seaweed-volume/src/storage/volume.rs @@ -2759,4 +2759,88 @@ mod tests { assert_eq!(info.data_size, data.len() as u32); assert!(info.data_file_offset > 0); } + + /// Volume destroy must preserve .vif files (needed by EC volumes). + #[test] + fn test_destroy_preserves_vif() { + let tmp = TempDir::new().unwrap(); + let dir = tmp.path().to_str().unwrap(); + + let mut v = make_test_volume(dir); + let mut n = Needle { + id: NeedleId(1), + cookie: Cookie(1), + data: b"test".to_vec(), + data_size: 4, + ..Needle::default() + }; + v.write_needle(&mut n, true).unwrap(); + + // Write a .vif file (as EC encode would) + let vif_path = format!("{}/1.vif", dir); + std::fs::write(&vif_path, r#"{"version":3}"#).unwrap(); + assert!(std::path::Path::new(&vif_path).exists()); + + // .dat and .idx should exist + let dat_path = format!("{}/1.dat", dir); + let idx_path = format!("{}/1.idx", dir); + assert!(std::path::Path::new(&dat_path).exists()); + assert!(std::path::Path::new(&idx_path).exists()); + + // Destroy the volume + v.destroy().unwrap(); + + // .dat and .idx should be gone + assert!(!std::path::Path::new(&dat_path).exists(), ".dat should be removed"); + assert!(!std::path::Path::new(&idx_path).exists(), ".idx should be removed"); + + // .vif MUST be preserved for EC volumes + assert!(std::path::Path::new(&vif_path).exists(), ".vif must survive destroy"); + } + + /// Volume destroy with separate idx directory must clean up both dirs. + #[test] + fn test_destroy_with_separate_idx_dir() { + let dat_tmp = TempDir::new().unwrap(); + let idx_tmp = TempDir::new().unwrap(); + let dat_dir = dat_tmp.path().to_str().unwrap(); + let idx_dir = idx_tmp.path().to_str().unwrap(); + + let mut v = Volume::new( + dat_dir, + idx_dir, + "", + VolumeId(1), + NeedleMapKind::InMemory, + None, + None, + 0, + Version::current(), + ) + .unwrap(); + + let mut n = Needle { + id: NeedleId(1), + cookie: Cookie(1), + data: b"hello".to_vec(), + data_size: 5, + ..Needle::default() + }; + v.write_needle(&mut n, true).unwrap(); + + // Write .vif in data dir (as EC encode would) + let vif_path = format!("{}/1.vif", dat_dir); + std::fs::write(&vif_path, r#"{"version":3}"#).unwrap(); + + let dat_path = format!("{}/1.dat", dat_dir); + let idx_path = format!("{}/1.idx", idx_dir); + assert!(std::path::Path::new(&dat_path).exists()); + assert!(std::path::Path::new(&idx_path).exists()); + + v.destroy().unwrap(); + + assert!(!std::path::Path::new(&dat_path).exists(), ".dat removed from data dir"); + assert!(!std::path::Path::new(&idx_path).exists(), ".idx removed from idx dir"); + assert!(std::path::Path::new(&vif_path).exists(), ".vif preserved in data dir"); + } }