diff --git a/seaweed-volume/src/server/grpc_server.rs b/seaweed-volume/src/server/grpc_server.rs index 3df3e0a5f..d1cc8d68e 100644 --- a/seaweed-volume/src/server/grpc_server.rs +++ b/seaweed-volume/src/server/grpc_server.rs @@ -2609,7 +2609,7 @@ impl VolumeServer for VolumeGrpcService { let mut store = self.state.store.write().unwrap(); let _ = store.remove_ec_volume(vid); for loc in &store.locations { - loc.remove_ec_volume_files(&req.collection, vid); + loc.remove_ec_volume_files_full_teardown(&req.collection, vid); } } self.state.volume_state_notify.notify_one(); diff --git a/seaweed-volume/src/storage/disk_location.rs b/seaweed-volume/src/storage/disk_location.rs index 21d478b11..9d21fc2ae 100644 --- a/seaweed-volume/src/storage/disk_location.rs +++ b/seaweed-volume/src/storage/disk_location.rs @@ -328,6 +328,28 @@ impl DiskLocation { } } + /// Full-teardown variant: everything remove_ec_volume_files clears, PLUS the + /// normal-volume .vif on a SHARD-ONLY node. An interrupted shard copy + /// (which installs shards + .ecx before .vif) could otherwise mount a fresh + /// generation under the prior run's identity / ratio / dat_file_size carried by + /// a stale .vif. Gated on .idx absence so a source-volume holder keeps its live + /// .vif. Reconcile/load-fallback call remove_ec_volume_files directly and + /// intentionally preserve it, mirroring Go's removeEcVolumeFiles (reconcile) vs + /// removeStaleEcArtifacts (teardown) split. + pub(crate) fn remove_ec_volume_files_full_teardown(&self, collection: &str, vid: VolumeId) { + self.remove_ec_volume_files(collection, vid); + let base = volume_file_name(&self.directory, collection, vid); + let idx_base = volume_file_name(&self.idx_directory, collection, vid); + if !std::path::Path::new(&format!("{}.idx", idx_base)).exists() { + let _ = fs::remove_file(format!("{}.vif", idx_base)); + if self.idx_directory != self.directory + && !std::path::Path::new(&format!("{}.idx", base)).exists() + { + let _ = fs::remove_file(format!("{}.vif", base)); + } + } + } + /// Find a volume by ID. pub fn find_volume(&self, vid: VolumeId) -> Option<&Volume> { self.volumes.get(&vid) diff --git a/weed/server/volume_grpc_erasure_coding.go b/weed/server/volume_grpc_erasure_coding.go index 87954001c..c19c21088 100644 --- a/weed/server/volume_grpc_erasure_coding.go +++ b/weed/server/volume_grpc_erasure_coding.go @@ -557,6 +557,21 @@ func removeStaleEcArtifacts(dataBaseFileName, indexBaseFileName string, total in record(removeFileIfExists(dataBaseFileName + ".ecj")) record(removeBitrotSidecars(dataBaseFileName)) } + + // Canonical .vif. A shard copy installs shards + .ecx before .vif, so an + // interrupted copy can leave a stale .vif whose run identity / shard ratio / + // dat_file_size a fresh generation would inherit. Remove it only on a shard-only + // EC node: where a normal .idx exists this is the source volume holder and + // the .vif belongs to that live volume — keep it. This mirrors the !hasIdxFile + // gate in the per-shard delete path. + if _, statErr := os.Stat(indexBaseFileName + ".idx"); os.IsNotExist(statErr) { + record(removeFileIfExists(indexBaseFileName + ".vif")) + if dataBaseFileName != indexBaseFileName { + if _, dStatErr := os.Stat(dataBaseFileName + ".idx"); os.IsNotExist(dStatErr) { + record(removeFileIfExists(dataBaseFileName + ".vif")) + } + } + } return firstErr }