rust volume: mirror the VolumeConsolidateIndex RPC from Go (#10752)

The Go volume server has VolumeConsolidateIndex, which moves a volume's
.idx out of the data directory into the configured -dir.idx directory
(where an EC decode/reconstruct can leave it co-located) and reloads the
volume in place. The Rust port's proto omitted the RPC entirely, so its
generated VolumeServer trait was one method short of Go's.

Add the proto message and rpc, the gated grpc handler, and
Store::consolidate_volume_index / Volume::relocate_index_to, mirroring
Go's Store.ConsolidateVolumeIndex and Volume.RelocateIndexTo -- including
the cross-device copy fallback and the reopen-against-the-old-dir path
when the move fails.

Integration tests cover the real move (index relocated, volume still
serves reads and the move is idempotent), the no-op paths (index already
in place, no separate idx dir) and the not-found error, plus the grpc
handler end to end.
This commit is contained in:
Chris Lu authored and GitHub committed 2026-08-13 13:30:58 -07:00
1 parent 7f27c572c4
commit ae2cc8225e
4 files changed
+309 -1

No files matched your search

+39
View File
@@ -952,6 +952,22 @@ impl VolumeServer for VolumeGrpcService {
Ok(Response::new(volume_server_pb::VolumeUnmountResponse {}))
}
async fn volume_consolidate_index(
&self,
request: Request<volume_server_pb::VolumeConsolidateIndexRequest>,
) -> Result<Response<volume_server_pb::VolumeConsolidateIndexResponse>, Status> {
self.check_grpc_admin_auth(&request)?;
self.state.check_maintenance()?;
let vid = VolumeId(request.into_inner().volume_id);
let mut store = self.state.store.write().unwrap();
store
.consolidate_volume_index(vid)
.map_err(|e| Status::internal(e.to_string()))?;
Ok(Response::new(
volume_server_pb::VolumeConsolidateIndexResponse {},
))
}
async fn volume_delete(
&self,
request: Request<volume_server_pb::VolumeDeleteRequest>,
@@ -5416,6 +5432,29 @@ mod tests {
(VolumeGrpcService { state }, tmp)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_volume_consolidate_index_rpc() {
let (service, _tmp) = make_local_service_with_volume("consolidate_rpc", None);
// Carry a peer address so the admin gate sees a caller; the test guard
// has an empty whitelist, so any peer is accepted.
let mut request = Request::new(volume_server_pb::VolumeConsolidateIndexRequest {
volume_id: 1,
});
request.extensions_mut().insert(tonic::transport::server::TcpConnectInfo {
local_addr: None,
remote_addr: Some("127.0.0.1:65000".parse().unwrap()),
});
service.volume_consolidate_index(request).await.unwrap();
// Data and index share a directory here, so there is nothing to move;
// the volume stays mounted and keeps its needle.
let store = service.state.store.read().unwrap();
let (_, v) = store.find_volume(VolumeId(1)).unwrap();
assert_eq!(v.file_count(), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_volume_incremental_copy_streams_remote_only_volume_data() {
let (service, _tmp, shutdown_tx, dat_bytes, super_block_size, _delete_count) =