pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub use uhlc;
const ENVELOPE_SIZE_HINT: usize = 512;
pub fn encode<T: serde::Serialize>(value: &T) -> postcard::Result<Vec<u8>> {
encode_presized(value, 0)
}
pub fn encode_presized<T: serde::Serialize>(
value: &T,
bulk_bytes: usize,
) -> postcard::Result<Vec<u8>> {
postcard::to_extend(
value,
Vec::with_capacity(bulk_bytes.saturating_add(ENVELOPE_SIZE_HINT)),
)
}
pub fn decode<'a, T: serde::Deserialize<'a>>(bytes: &'a [u8]) -> eyre::Result<T> {
let (value, rest) = postcard::take_from_bytes(bytes)?;
if !rest.is_empty() {
eyre::bail!(
"trailing bytes after decoded value ({} of {} unconsumed) — \
likely a desynced or incompatible peer",
rest.len(),
bytes.len()
);
}
Ok(value)
}
pub const MAX_MESSAGE_BYTES: usize = 64 * 1024 * 1024;
pub const TCP_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
pub const TOPIC_DATA_PROTOCOL_VERSION: u16 = 2;
pub fn topic_protocol_mismatch_message(peer: &str, peer_version: Option<u16>) -> String {
let ours = TOPIC_DATA_PROTOCOL_VERSION;
let peer_state = match peer_version {
Some(version) => format!("{peer} speaks version {version}"),
None => format!("{peer} predates the topic data protocol handshake (bincode-era frames)"),
};
format!(
"topic data protocol mismatch: {peer_state}, this side speaks {ours}. \
Binary frames are positionally encoded, so subscribing would silently \
misparse rather than fail. Upgrade whichever side is older."
)
}
pub mod auth;
pub mod bulk_bytes;
pub mod common;
pub mod config;
pub mod descriptor;
pub mod id;
pub mod metadata;
pub mod coordinator_to_daemon;
pub mod daemon_to_coordinator;
pub mod daemon_to_daemon;
pub mod daemon_to_node;
pub mod node_to_daemon;
pub mod cli_to_coordinator;
pub mod coordinator_to_cli;
pub mod ws_protocol;
pub mod integration_testing_format;
pub use aligned_vec;
pub use arrow_data;
pub use arrow_schema;
use uuid::{Timestamp, Uuid};
pub type DataflowId = uuid::Uuid;
#[derive(
Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,
)]
pub struct SessionId(uuid::Uuid);
impl SessionId {
pub fn generate() -> Self {
Self(Uuid::new_v7(Timestamp::now(uuid::NoContext)))
}
pub fn uuid(&self) -> uuid::Uuid {
self.0
}
}
#[derive(
Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,
)]
pub struct BuildId(uuid::Uuid);
impl BuildId {
pub fn generate() -> Self {
Self(Uuid::new_v7(Timestamp::now(uuid::NoContext)))
}
pub fn from_display_str(s: &str) -> Option<Self> {
let inner = s
.strip_prefix("BuildId(")
.and_then(|rest| rest.strip_suffix(')'))
.unwrap_or(s);
Uuid::parse_str(inner).ok().map(BuildId)
}
pub fn uuid(&self) -> uuid::Uuid {
self.0
}
}
impl std::fmt::Display for BuildId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "BuildId({})", self.0)
}
}
pub fn current_crate_version() -> semver::Version {
let crate_version_raw = env!("CARGO_PKG_VERSION");
semver::Version::parse(crate_version_raw).unwrap()
}
pub(crate) fn versions_compatible(
crate_version: &semver::Version,
specified_version: &semver::Version,
) -> Result<bool, String> {
let req = semver::VersionReq::parse(&crate_version.to_string()).map_err(|error| {
format!("failed to parse crate version `{crate_version}` as `VersionReq`: {error}")
})?;
let specified_dora_req = semver::VersionReq::parse(&specified_version.to_string())
.map_err(|error| {
format!(
"failed to parse specified dora version `{specified_version}` as `VersionReq`: {error}",
)
})?;
let matches = req.matches(specified_version) || specified_dora_req.matches(crate_version);
Ok(matches)
}
#[cfg(test)]
mod encoding_tests {
use crate::metadata::Metadata;
#[test]
fn decode_rejects_trailing_bytes() {
let mut bytes =
crate::encode(&Metadata::new(uhlc::HLC::default().new_timestamp())).expect("serialize");
bytes.extend_from_slice(b"foreign publisher trailer");
let err = crate::decode::<Metadata>(&bytes).expect_err("trailing bytes must be rejected");
assert!(
format!("{err:#}").contains("trailing bytes"),
"error should name the cause, got: {err:#}"
);
}
}
#[cfg(test)]
mod topic_protocol_tests {
use super::*;
#[test]
fn mismatch_message_names_both_sides_and_the_reason() {
let msg = topic_protocol_mismatch_message("client", Some(1));
assert!(msg.contains("client speaks version 1"), "got: {msg}");
assert!(
msg.contains(&TOPIC_DATA_PROTOCOL_VERSION.to_string()),
"should name our own version, got: {msg}"
);
assert!(
msg.contains("misparse"),
"should say why a mismatch is fatal rather than merely different, got: {msg}"
);
}
#[test]
fn mismatch_message_distinguishes_a_pre_handshake_peer() {
let msg = topic_protocol_mismatch_message("coordinator", None);
assert!(msg.contains("predates"), "got: {msg}");
assert!(
!msg.contains("version None"),
"should not leak a Debug-formatted Option, got: {msg}"
);
}
#[test]
fn peer_label_is_what_varies_between_directions() {
let from_coordinator = topic_protocol_mismatch_message("client", Some(1));
let from_cli = topic_protocol_mismatch_message("coordinator", Some(1));
assert_ne!(from_coordinator, from_cli);
assert!(from_coordinator.contains("client speaks"));
assert!(from_cli.contains("coordinator speaks"));
}
}