From 11c277765c885c860f0c6c0815e8f767809104fb Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Tue, 30 Jun 2026 19:36:38 -0700 Subject: [PATCH] feat(ec): copy the .ecsum sidecar during VolumeEcShardsCopy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Honor copy_ecsum_file: when set, copy the generation-0 .ecsum alongside the shards so protection travels with them, mirroring Go's non-2PC copy path. Tolerant of a missing source (empty stream) — the 0-byte file is dropped so mount sees no sidecar (protection off) rather than a truncated/invalid one. Claude-Session: https://claude.ai/code/session_015EE9Sc9EvNp8BCVva4RKdo --- seaweed-volume/src/server/grpc_server.rs | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/seaweed-volume/src/server/grpc_server.rs b/seaweed-volume/src/server/grpc_server.rs index c86eed78f8..b2c4a2b5ce 100644 --- a/seaweed-volume/src/server/grpc_server.rs +++ b/seaweed-volume/src/server/grpc_server.rs @@ -2766,6 +2766,58 @@ impl VolumeServer for VolumeGrpcService { } } + // Copy the generation-0 bitrot checksum sidecar (.ecsum) when requested, so + // protection travels with the shards. Tolerant of a missing source (no-op): + // this non-2PC path has no Prepare backstop, and an unprotected source + // simply leaves the copy unprotected. + if req.copy_ecsum_file { + let copy_req = volume_server_pb::CopyFileRequest { + volume_id: req.volume_id, + collection: req.collection.clone(), + is_ec_volume: true, + ext: ".ecsum".to_string(), + compaction_revision: u32::MAX, + stop_offset: i64::MAX as u64, + ignore_source_file_not_found: true, + ..Default::default() + }; + let mut stream = client + .copy_file(copy_req) + .await + .map_err(|e| { + Status::internal(format!( + "VolumeEcShardsCopy volume {} copy .ecsum: {}", + vid, e + )) + })? + .into_inner(); + + let file_path = { + let base = + crate::storage::volume::volume_file_name(&dest_dir, &req.collection, vid); + format!("{}.ecsum", base) + }; + let mut file = std::fs::File::create(&file_path) + .map_err(|e| Status::internal(format!("create {}: {}", file_path, e)))?; + let mut written: u64 = 0; + while let Some(chunk) = stream + .message() + .await + .map_err(|e| Status::internal(format!("recv .ecsum: {}", e)))? + { + use std::io::Write; + file.write_all(&chunk.file_content) + .map_err(|e| Status::internal(format!("write {}: {}", file_path, e)))?; + written += chunk.file_content.len() as u64; + } + // A missing source yields an empty stream; drop the 0-byte file so mount + // sees no sidecar (protection Off) rather than a truncated/invalid one. + if written == 0 { + drop(file); + let _ = std::fs::remove_file(&file_path); + } + } + Ok(Response::new( volume_server_pb::VolumeEcShardsCopyResponse {}, ))