From 3967ca23be20962434d1e1f1f6c2f8cab5f9e744 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Fri, 28 Aug 2026 17:17:47 -0700 Subject: [PATCH] rust: cover the READS scrub reconstruction path (#11027) --- seaweed-volume/src/server/grpc_server.rs | 85 ++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/seaweed-volume/src/server/grpc_server.rs b/seaweed-volume/src/server/grpc_server.rs index c6d9bb7cd..e366413d5 100644 --- a/seaweed-volume/src/server/grpc_server.rs +++ b/seaweed-volume/src/server/grpc_server.rs @@ -6102,4 +6102,89 @@ mod tests { assert!(vif.expire_at_sec >= before + ttl.to_seconds()); assert!(vif.expire_at_sec <= before + ttl.to_seconds() + 5); } + + async fn scrub_ec_volume_1( + service: &VolumeGrpcService, + mode: i32, + ) -> volume_server_pb::ScrubEcVolumeResponse { + service + .scrub_ec_volume(Request::new(volume_server_pb::ScrubEcVolumeRequest { + mode, + volume_ids: vec![1], + force_deleted_needles_check: false, + })) + .await + .unwrap() + .into_inner() + } + + #[tokio::test] + async fn test_scrub_ec_volume_reads_mode_reconstructs_missing_shard() { + let (service, _tmp) = make_local_service_with_volume("", None); + service + .volume_ec_shards_generate(Request::new( + volume_server_pb::VolumeEcShardsGenerateRequest { + volume_id: 1, + collection: String::new(), + }, + )) + .await + .unwrap(); + service + .volume_ec_shards_mount(Request::new( + volume_server_pb::VolumeEcShardsMountRequest { + volume_id: 1, + collection: String::new(), + shard_ids: (0..14).collect(), + source_disk_type: String::new(), + recover_missing_index: false, + }, + )) + .await + .unwrap(); + + // Seed the shard-location cache so the scrub skips the master lookup. + // The explicit-gRPC-port form targets port 1, so remote reads fail fast + // with connection-refused instead of resolving to a live local port. + { + let store = service.state.store.read().unwrap(); + let ecv = store.find_ec_volume(VolumeId(1)).unwrap(); + let mut locs = ecv.shard_locations.write().unwrap(); + for sid in 0u8..14 { + locs.insert(sid, vec!["127.0.0.1:255.1".to_string()]); + } + *ecv.shard_locations_refresh_time.lock().unwrap() = + Some(std::time::Instant::now()); + } + + // All shards local: FULL is clean. + let resp = scrub_ec_volume_1(&service, 2).await; + assert!(resp.broken_volume_ids.is_empty(), "{:?}", resp.details); + assert_eq!(resp.total_files, 1); + + service + .volume_ec_shards_unmount(Request::new( + volume_server_pb::VolumeEcShardsUnmountRequest { + volume_id: 1, + shard_ids: vec![0], + encode_ts_ns: 0, + }, + )) + .await + .unwrap(); + + // With a shard unreadable, FULL flags it AND fails the needle... + let resp = scrub_ec_volume_1(&service, 2).await; + assert_eq!(resp.broken_volume_ids, vec![1]); + assert!(resp.broken_shard_infos.iter().any(|s| s.shard_id == 0)); + assert!(!resp.details.is_empty()); + + // ...while READS still reports the shard broken but reconstructs the + // interval from the local survivors, so no needle errors surface. + let resp = scrub_ec_volume_1(&service, 5).await; + assert_eq!(resp.broken_volume_ids, vec![1]); + assert!(resp.broken_shard_infos.iter().any(|s| s.shard_id == 0)); + assert!(resp.details.is_empty(), "{:?}", resp.details); + assert_eq!(resp.total_files, 1); + } }