use crate::consensus::{
Command2, ConsensusError, ConsensusHeader, GenericHeader, PrepareHeader, PrepareOkHeader,
RequestHeader,
};
use bytes::Bytes;
use std::marker::PhantomData;
pub trait ConsensusMessage<H: ConsensusHeader> {
fn header(&self) -> &H;
}
impl<H: ConsensusHeader> ConsensusMessage<H> for Message<H> {
fn header(&self) -> &H {
let header_bytes = &self.buffer[..size_of::<H>()];
bytemuck::checked::try_from_bytes(header_bytes)
.expect("header validated at construction time")
}
}
#[repr(C)]
#[derive(Debug, Clone)]
pub struct Message<H: ConsensusHeader> {
buffer: Bytes,
_marker: PhantomData<H>,
}
impl<H: ConsensusHeader> Message<H> {
pub fn from_bytes(buffer: Bytes) -> Result<Self, ConsensusError> {
if buffer.len() < size_of::<H>() {
return Err(ConsensusError::InvalidCommand {
expected: H::COMMAND,
found: Command2::Reserved,
});
}
let header_bytes = &buffer[..size_of::<H>()];
let header = bytemuck::checked::try_from_bytes::<H>(header_bytes)
.map_err(|_| ConsensusError::InvalidBitPattern)?;
header.validate()?;
let header_size = header.size() as usize;
if buffer.len() < header_size {
return Err(ConsensusError::InvalidCommand {
expected: H::COMMAND,
found: Command2::Reserved,
});
}
Ok(Self {
buffer,
_marker: PhantomData,
})
}
#[must_use]
pub fn new(size: usize) -> Self {
assert!(
size >= size_of::<H>(),
"size must be at least header size ({})",
size_of::<H>()
);
Self {
buffer: Bytes::from(vec![0u8; size]),
_marker: PhantomData,
}
}
#[must_use]
#[allow(clippy::missing_panics_doc)]
#[inline]
pub fn header(&self) -> &H {
let header_bytes = &self.buffer[..size_of::<H>()];
bytemuck::checked::try_from_bytes(header_bytes)
.expect("header validated at construction time")
}
#[must_use]
#[inline]
pub fn body(&self) -> &[u8] {
let header_size = size_of::<H>();
let total_size = self.header().size() as usize;
if total_size > header_size {
&self.buffer[header_size..total_size]
} else {
&[]
}
}
#[must_use]
#[inline]
pub fn body_bytes(&self) -> Bytes {
let header_size = size_of::<H>();
let total_size = self.header().size() as usize;
if total_size > header_size {
self.buffer.slice(header_size..total_size)
} else {
Bytes::new()
}
}
#[must_use]
#[inline]
pub fn as_bytes(&self) -> &[u8] {
let total_size = self.header().size() as usize;
&self.buffer[..total_size]
}
#[must_use]
#[inline]
pub fn into_inner(self) -> Bytes {
self.buffer
}
#[must_use]
pub fn into_generic(self) -> Message<GenericHeader> {
unsafe { Message::from_buffer_unchecked(self.buffer) }
}
#[must_use]
#[allow(clippy::missing_const_for_fn)]
pub fn as_generic(&self) -> &Message<GenericHeader> {
unsafe { &*std::ptr::from_ref(self).cast::<Message<GenericHeader>>() }
}
pub fn try_into_typed<T: ConsensusHeader>(self) -> Result<Message<T>, ConsensusError> {
if self.buffer.len() < size_of::<T>() {
return Err(ConsensusError::InvalidCommand {
expected: T::COMMAND,
found: Command2::Reserved,
});
}
let generic = self.as_generic();
if generic.header().command != T::COMMAND {
return Err(ConsensusError::InvalidCommand {
expected: T::COMMAND,
found: generic.header().command,
});
}
let header_bytes = &self.buffer[..size_of::<T>()];
let header = bytemuck::checked::try_from_bytes::<T>(header_bytes)
.map_err(|_| ConsensusError::InvalidBitPattern)?;
header.validate()?;
Ok(unsafe { Message::<T>::from_buffer_unchecked(self.buffer) })
}
pub fn try_as_typed<T: ConsensusHeader>(&self) -> Result<&Message<T>, ConsensusError> {
if self.buffer.len() < size_of::<T>() {
return Err(ConsensusError::InvalidCommand {
expected: T::COMMAND,
found: Command2::Reserved,
});
}
let generic = self.as_generic();
if generic.header().command != T::COMMAND {
return Err(ConsensusError::InvalidCommand {
expected: T::COMMAND,
found: generic.header().command,
});
}
let header_bytes = &self.buffer[..size_of::<T>()];
bytemuck::checked::try_from_bytes::<T>(header_bytes)
.map_err(|_| ConsensusError::InvalidBitPattern)?
.validate()?;
Ok(unsafe { &*std::ptr::from_ref(self).cast::<Message<T>>() })
}
#[must_use]
pub fn transmute_header<T: ConsensusHeader>(self, f: impl FnOnce(H, &mut T)) -> Message<T> {
assert_eq!(size_of::<H>(), size_of::<T>());
let old_header = *self.header();
let buffer = self.into_inner();
unsafe {
let ptr = buffer.as_ptr().cast_mut();
let slice = std::slice::from_raw_parts_mut(ptr, size_of::<T>());
slice.fill(0);
let new_header =
bytemuck::checked::try_from_bytes_mut(slice).expect("zeroed bytes are valid");
f(old_header, new_header);
}
Message {
buffer,
_marker: PhantomData,
}
}
#[inline]
const unsafe fn from_buffer_unchecked(buffer: Bytes) -> Self {
Self {
buffer,
_marker: PhantomData,
}
}
}
#[derive(Debug)]
pub enum MessageBag {
Request(Message<RequestHeader>),
Prepare(Message<PrepareHeader>),
PrepareOk(Message<PrepareOkHeader>),
}
impl MessageBag {
#[must_use]
pub fn command(&self) -> Command2 {
match self {
Self::Request(m) => m.header().command,
Self::Prepare(m) => m.header().command,
Self::PrepareOk(m) => m.header().command,
}
}
#[must_use]
pub fn size(&self) -> u32 {
match self {
Self::Request(m) => m.header().size(),
Self::Prepare(m) => m.header().size(),
Self::PrepareOk(m) => m.header().size(),
}
}
#[must_use]
pub fn operation(&self) -> crate::consensus::Operation {
match self {
Self::Request(m) => m.header().operation,
Self::Prepare(m) => m.header().operation,
Self::PrepareOk(m) => m.header().operation,
}
}
}
impl<T: ConsensusHeader> TryFrom<Message<T>> for MessageBag {
type Error = ConsensusError;
fn try_from(value: Message<T>) -> Result<Self, Self::Error> {
let command = value.as_generic().header().command;
let buffer = value.into_inner();
match command {
Command2::Request => {
let msg = unsafe { Message::<RequestHeader>::from_buffer_unchecked(buffer) };
Ok(Self::Request(msg))
}
Command2::Prepare => {
let msg = unsafe { Message::<PrepareHeader>::from_buffer_unchecked(buffer) };
Ok(Self::Prepare(msg))
}
Command2::PrepareOk => {
let msg = unsafe { Message::<PrepareOkHeader>::from_buffer_unchecked(buffer) };
Ok(Self::PrepareOk(msg))
}
other => Err(ConsensusError::InvalidCommand {
expected: Command2::Reserved,
found: other,
}),
}
}
}
#[cfg(test)]
mod tests {
use super::Message;
use crate::consensus::{Command2, GenericHeader, RequestHeader};
use bytes::{Bytes, BytesMut};
#[test]
fn generic_message_from_zeroed_buffer() {
let mut buf = BytesMut::zeroed(384);
let header =
bytemuck::checked::try_from_bytes_mut::<GenericHeader>(&mut buf[..256]).unwrap();
header.size = 384;
header.cluster = 42;
let msg = Message::<GenericHeader>::from_bytes(buf.freeze()).unwrap();
assert_eq!(msg.header().cluster, 42);
assert_eq!(msg.header().size, 384);
assert_eq!(msg.body().len(), 128);
}
#[test]
fn request_message_roundtrip() {
let mut buf = BytesMut::zeroed(320);
let header =
bytemuck::checked::try_from_bytes_mut::<RequestHeader>(&mut buf[..256]).unwrap();
header.size = 320;
header.command = Command2::Request;
header.client = 12345;
header.operation = crate::consensus::Operation::CreateStream;
let msg = Message::<RequestHeader>::from_bytes(buf.freeze()).unwrap();
assert_eq!(msg.header().client, 12345);
assert_eq!(
msg.header().operation,
crate::consensus::Operation::CreateStream
);
assert_eq!(msg.body().len(), 64);
}
#[test]
fn too_small_buffer_rejected() {
let buf = Bytes::from(vec![0u8; 100]); assert!(Message::<GenericHeader>::from_bytes(buf).is_err());
}
#[test]
fn into_generic_and_back() {
let mut buf = BytesMut::zeroed(256);
let header =
bytemuck::checked::try_from_bytes_mut::<RequestHeader>(&mut buf[..256]).unwrap();
header.size = 256;
header.command = Command2::Request;
let msg = Message::<RequestHeader>::from_bytes(buf.freeze()).unwrap();
let generic = msg.into_generic();
assert_eq!(generic.header().command, Command2::Request);
let back = generic.try_into_typed::<RequestHeader>().unwrap();
assert_eq!(back.header().command, Command2::Request);
}
#[test]
fn transmute_header_generic_to_request() {
let mut buf = BytesMut::zeroed(320);
let header =
bytemuck::checked::try_from_bytes_mut::<GenericHeader>(&mut buf[..256]).unwrap();
header.size = 320;
header.cluster = 99;
let generic = Message::<GenericHeader>::from_bytes(buf.freeze()).unwrap();
let request = generic.transmute_header(|old, new: &mut RequestHeader| {
new.size = old.size;
new.command = Command2::Request;
new.cluster = old.cluster;
new.client = 42;
new.operation = crate::consensus::Operation::CreateStream;
});
assert_eq!(request.header().command, Command2::Request);
assert_eq!(request.header().client, 42);
assert_eq!(request.header().cluster, 99);
assert_eq!(request.header().size, 320);
assert_eq!(request.body().len(), 64);
}
#[test]
fn wrong_type_conversion_fails() {
let mut buf = BytesMut::zeroed(256);
let header =
bytemuck::checked::try_from_bytes_mut::<GenericHeader>(&mut buf[..256]).unwrap();
header.size = 256;
header.command = Command2::Reserved;
let msg = Message::<GenericHeader>::from_bytes(buf.freeze()).unwrap();
assert!(msg.try_into_typed::<RequestHeader>().is_err());
}
}