vm-migration: add protocol versioning

Add protocol-side support for migration protocol versioning.

Use the existing 6-byte Start command header, which was
previously zero padding, to carry the sender's migration
protocol version without changing the wire layout.

Store the version as a little-endian u16 in the first two
bytes and ignore the remaining four bytes. A zeroed command
header continues to mean a legacy v0 sender.

This keeps the message flow unchanged for rollout:
Start is still followed by plain OK or Error (Aborted), and no new
command is needed.

On-behalf-of: SAP leander.kohler@sap.com
Signed-off-by: Leander Kohler <leander.kohler@cyberus-technology.de>
This commit is contained in:
Leander Kohler
2026-03-18 13:36:33 +01:00
committed by Rob Bradford
parent 181d29ee90
commit b27faaaa45

View File

@@ -73,8 +73,22 @@
//! Source->>Destination: Complete
//! Destination-->>Source: OK
//! ```
//!
//! ## Protocol Versioning
//!
//! `Start` carries the sender's migration protocol version.
//! A zeroed version field is treated as legacy protocol `v0`.
//!
//! The destination validates that version and replies with a plain `OK` or
//! `Error`.
//!
//! Only the current and immediately previous protocol versions are
//! supported. Compatibility is one-way, from older protocol versions
//! to newer ones.
use std::io::{Read, Write};
use std::mem::size_of;
use std::ops::RangeInclusive;
use std::{mem, slice};
use itertools::Itertools;
@@ -127,18 +141,33 @@ pub enum Command {
CompletePaused = 8,
}
/// Newest migration protocol version sent by this implementation.
pub const CURRENT_PROTOCOL_VERSION: u16 = 0;
/// Returns the current migration protocol version and the previous version, if any.
pub fn supported_protocol_versions() -> RangeInclusive<u16> {
CURRENT_PROTOCOL_VERSION.saturating_sub(1)..=CURRENT_PROTOCOL_VERSION
}
#[repr(C)]
#[derive(Default, Copy, Clone)]
pub struct Request {
command: Command,
padding: [u8; 6],
length: u64, // Length of payload for command excluding the Request struct
command_headers: [u8; 6],
/// Length of payload for command excluding the Request struct
length: u64,
}
// SAFETY: Request contains a series of integers with no implicit padding
unsafe impl ByteValued for Request {}
impl Request {
fn encode_sender_version(version: u16) -> [u8; 6] {
let mut command_headers = [0; 6];
command_headers[..size_of::<u16>()].copy_from_slice(&version.to_le_bytes());
command_headers
}
pub fn new(command: Command, length: u64) -> Self {
Self {
command,
@@ -148,7 +177,11 @@ impl Request {
}
pub fn start() -> Self {
Self::new(Command::Start, 0)
Self {
command: Command::Start,
command_headers: Self::encode_sender_version(CURRENT_PROTOCOL_VERSION),
length: 0,
}
}
pub fn state(length: u64) -> Self {
@@ -189,6 +222,10 @@ impl Request {
self.length
}
pub fn command_headers(&self) -> &[u8; 6] {
&self.command_headers
}
pub fn read_from(fd: &mut dyn Read) -> Result<Request, MigratableError> {
let mut request = Request::default();
fd.read_exact(Self::as_mut_slice(&mut request))
@@ -485,7 +522,21 @@ impl MemoryRangeTable {
#[cfg(test)]
mod unit_tests {
use crate::protocol::{MemoryRange, MemoryRangeTable};
use crate::protocol::{Command, MemoryRange, MemoryRangeTable, Request};
#[test]
fn test_start_request_ignores_residual_command_headers_bytes() {
let request = Request {
command: Command::Start,
command_headers: [1, 0, 0xaa, 0xbb, 0xcc, 0xdd],
length: 0,
};
assert_eq!(
u16::from_le_bytes([request.command_headers()[0], request.command_headers()[1]]),
1
);
}
#[test]
fn test_memory_range_table_from_dirty_ranges_iter() {