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
}