use bytes::Bytes;
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub mod hibernate;
pub mod java_migration;
pub const PROTOCOL_VERSION: u16 = 1;
pub const LENGTH_PREFIX_BYTES: usize = 4;
pub const VERSION_BYTES: usize = 2;
pub const MIN_FRAME_BYTES: usize = LENGTH_PREFIX_BYTES + VERSION_BYTES;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientFrame {
protocol_version: u16,
payload: Bytes,
}
impl ClientFrame {
pub fn new(payload: impl Into<Bytes>) -> Self {
Self {
protocol_version: PROTOCOL_VERSION,
payload: payload.into(),
}
}
pub fn from_message(message: &ClientWireMessage) -> Result<Self, ClientProtocolError> {
let payload = postcard::to_allocvec(message)
.map_err(|error| ClientProtocolError::Codec(error.to_string()))?;
Ok(Self::new(payload))
}
pub fn with_version(protocol_version: u16, payload: impl Into<Bytes>) -> Self {
Self {
protocol_version,
payload: payload.into(),
}
}
pub fn protocol_version(&self) -> u16 {
self.protocol_version
}
pub fn payload(&self) -> &Bytes {
&self.payload
}
pub fn encode(&self) -> Result<Bytes, ClientProtocolError> {
let body_len = VERSION_BYTES.checked_add(self.payload.len()).ok_or(
ClientProtocolError::FrameTooLarge {
actual: usize::MAX,
max: u32::MAX as usize,
},
)?;
if body_len > u32::MAX as usize {
return Err(ClientProtocolError::FrameTooLarge {
actual: body_len,
max: u32::MAX as usize,
});
}
let mut out = Vec::with_capacity(LENGTH_PREFIX_BYTES + body_len);
out.extend_from_slice(&(body_len as u32).to_be_bytes());
out.extend_from_slice(&self.protocol_version.to_be_bytes());
out.extend_from_slice(&self.payload);
Ok(Bytes::from(out))
}
pub fn decode_message(&self) -> Result<ClientWireMessage, ClientProtocolError> {
postcard::from_bytes(self.payload.as_ref())
.map_err(|error| ClientProtocolError::Codec(error.to_string()))
}
pub fn decode(bytes: &[u8], max_frame_bytes: usize) -> Result<Self, ClientProtocolError> {
if bytes.len() > max_frame_bytes {
return Err(ClientProtocolError::FrameTooLarge {
actual: bytes.len(),
max: max_frame_bytes,
});
}
if bytes.len() < MIN_FRAME_BYTES {
return Err(ClientProtocolError::TruncatedFrame {
actual: bytes.len(),
needed: MIN_FRAME_BYTES,
});
}
let body_len = u32::from_be_bytes(
bytes[0..LENGTH_PREFIX_BYTES]
.try_into()
.expect("slice length is checked"),
) as usize;
if body_len < VERSION_BYTES {
return Err(ClientProtocolError::TruncatedFrame {
actual: body_len,
needed: VERSION_BYTES,
});
}
let expected = LENGTH_PREFIX_BYTES + body_len;
if expected != bytes.len() {
return Err(ClientProtocolError::LengthMismatch {
declared: body_len,
actual: bytes.len().saturating_sub(LENGTH_PREFIX_BYTES),
});
}
let version_start = LENGTH_PREFIX_BYTES;
let version_end = version_start + VERSION_BYTES;
let protocol_version = u16::from_be_bytes(
bytes[version_start..version_end]
.try_into()
.expect("slice length is checked"),
);
if protocol_version > PROTOCOL_VERSION {
return Err(ClientProtocolError::UnsupportedVersion {
version: protocol_version,
supported_max: PROTOCOL_VERSION,
});
}
Ok(Self {
protocol_version,
payload: Bytes::copy_from_slice(&bytes[version_end..]),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct VersionHandshake {
pub min: u16,
pub max: u16,
}
impl VersionHandshake {
pub fn new(min: u16, max: u16) -> Self {
Self { min, max }
}
pub fn negotiate(self, server: VersionHandshake) -> Result<u16, ClientErrorEnvelope> {
let min = self.min.max(server.min);
let max = self.max.min(server.max);
if min <= max {
Ok(max)
} else {
Err(ClientErrorEnvelope::new(
ClientErrorCode::IncompatibleVersion,
false,
"no common HydraCache client protocol version",
))
}
}
}
impl Default for VersionHandshake {
fn default() -> Self {
Self {
min: PROTOCOL_VERSION,
max: PROTOCOL_VERSION,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Namespace(String);
impl Namespace {
pub fn new(value: impl Into<String>) -> Result<Self, ClientProtocolError> {
let value = value.into();
if value.trim().is_empty() {
return Err(ClientProtocolError::InvalidField("namespace"));
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct RegionId(String);
impl RegionId {
pub fn new(value: impl Into<String>) -> Result<Self, ClientProtocolError> {
let value = value.into();
if value.trim().is_empty() {
return Err(ClientProtocolError::InvalidField("region"));
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct StructuredKey {
segments: Vec<String>,
}
impl StructuredKey {
pub fn new(segments: Vec<String>) -> Result<Self, ClientProtocolError> {
if segments.is_empty() || segments.iter().any(|segment| segment.trim().is_empty()) {
return Err(ClientProtocolError::InvalidField("key_segments"));
}
Ok(Self { segments })
}
pub fn segments(&self) -> &[String] {
&self.segments
}
pub fn stable_key(&self) -> String {
self.segments.join(":")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReadConsistency {
Eventual,
Strong,
Session,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WriteConsistency {
Local,
Quorum,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClientContext {
pub session_token: Option<String>,
pub read: Option<ReadConsistency>,
pub write: Option<WriteConsistency>,
pub preferred_region: Option<RegionId>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Watermark {
pub source_generation: u64,
pub message_id: u64,
}
impl Watermark {
pub const fn new(source_generation: u64, message_id: u64) -> Self {
Self {
source_generation,
message_id,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RepairAction {
Apply,
ClearPartition,
InvalidateConservatively,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SubscriptionWatermarkTracker {
last: Option<Watermark>,
}
impl SubscriptionWatermarkTracker {
pub fn on_event(&mut self, event: &InvalidationEvent) -> RepairAction {
let next = event.watermark();
let Some(last) = self.last else {
self.last = Some(next);
return RepairAction::ClearPartition;
};
if next.source_generation != last.source_generation {
self.last = Some(next);
return RepairAction::ClearPartition;
}
if next.message_id > last.message_id.saturating_add(1) {
self.last = Some(next);
return RepairAction::InvalidateConservatively;
}
self.last = Some(Watermark::new(
last.source_generation,
last.message_id.max(next.message_id),
));
RepairAction::Apply
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClientRequestEnvelope {
pub request_id: String,
pub protocol_version: u16,
pub context: ClientContext,
pub deadline_ms: Option<u64>,
pub idempotency_key: Option<String>,
pub request: ClientRequest,
}
impl ClientRequestEnvelope {
pub fn new(request_id: impl Into<String>, request: ClientRequest) -> Self {
Self {
request_id: request_id.into(),
protocol_version: PROTOCOL_VERSION,
context: ClientContext::default(),
deadline_ms: None,
idempotency_key: None,
request,
}
}
pub fn with_context(mut self, context: ClientContext) -> Self {
self.context = context;
self
}
pub fn with_deadline_ms(mut self, deadline_ms: u64) -> Self {
self.deadline_ms = Some(deadline_ms);
self
}
pub fn with_idempotency_key(mut self, idempotency_key: impl Into<String>) -> Self {
self.idempotency_key = Some(idempotency_key.into());
self
}
pub fn deadline_expired(&self, now_ms: u64) -> bool {
self.deadline_ms.is_some_and(|deadline| deadline <= now_ms)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ClientRequest {
Get { ns: Namespace, key: StructuredKey },
Put {
ns: Namespace,
key: StructuredKey,
value: Vec<u8>,
ttl_ms: Option<u64>,
dimensions: Vec<String>,
},
Invalidate { ns: Namespace, key: StructuredKey },
BatchGet {
ns: Namespace,
keys: Vec<StructuredKey>,
},
BatchPut {
ns: Namespace,
entries: Vec<BatchPutEntry>,
},
EvictRegion { ns: Namespace },
SubscribeInvalidations {
ns: Namespace,
region: Option<RegionId>,
from: Option<Watermark>,
include_value: bool,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BatchPutEntry {
pub key: StructuredKey,
pub value: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClientResponseEnvelope {
pub request_id: String,
pub protocol_version: u16,
pub result: Result<ClientResponse, ClientErrorEnvelope>,
}
impl ClientResponseEnvelope {
pub fn ok(request_id: impl Into<String>, response: ClientResponse) -> Self {
Self {
request_id: request_id.into(),
protocol_version: PROTOCOL_VERSION,
result: Ok(response),
}
}
pub fn error(request_id: impl Into<String>, error: ClientErrorEnvelope) -> Self {
Self {
request_id: request_id.into(),
protocol_version: PROTOCOL_VERSION,
result: Err(error),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ClientResponse {
Value { value: Option<Vec<u8>> },
Stored,
Invalidated,
Batch { items: Vec<BatchItemStatus> },
Evicted,
Subscribed { from: Option<Watermark> },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BatchItemStatus {
pub index: usize,
pub result: Result<Option<Vec<u8>>, ClientErrorEnvelope>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClientErrorEnvelope {
pub code: ClientErrorCode,
pub retryable: bool,
pub retry_after_ms: Option<u64>,
pub message: String,
}
impl ClientErrorEnvelope {
pub fn new(code: ClientErrorCode, retryable: bool, message: impl Into<String>) -> Self {
Self {
code,
retryable,
retry_after_ms: None,
message: redact_message(message.into()),
}
}
pub fn with_retry_after_ms(mut self, retry_after_ms: u64) -> Self {
self.retry_after_ms = Some(retry_after_ms);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ClientErrorCode {
IncompatibleVersion,
Unauthenticated,
Unauthorized,
TenantQuota,
RateLimited,
ResidencyDenied,
TooLarge,
DeadlineExceeded,
Conflict,
BackendUnavailable,
MalformedFrame,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InvalidationEvent {
pub ns: Namespace,
pub key: StructuredKey,
pub generation: u64,
pub message_id: u64,
pub applied_region: Option<RegionId>,
pub value: Option<Vec<u8>>,
pub residency_degraded: bool,
pub affects_subscriber_view: bool,
}
impl InvalidationEvent {
pub fn new(ns: Namespace, key: StructuredKey, generation: u64, message_id: u64) -> Self {
Self {
ns,
key,
generation,
message_id,
applied_region: None,
value: None,
residency_degraded: false,
affects_subscriber_view: false,
}
}
pub fn applied_in(mut self, region: RegionId) -> Self {
self.applied_region = Some(region);
self
}
pub fn with_value(mut self, value: Vec<u8>) -> Self {
self.value = Some(value);
self
}
pub fn affects_subscriber_view(mut self) -> Self {
self.affects_subscriber_view = true;
self
}
pub fn watermark(&self) -> Watermark {
Watermark::new(self.generation, self.message_id)
}
pub fn should_deliver_to(&self, region: Option<&RegionId>) -> bool {
match region {
None => true,
Some(region) => {
self.applied_region.as_ref() == Some(region) || self.affects_subscriber_view
}
}
}
pub fn residency_gated(mut self, value_allowed: bool) -> Self {
if !value_allowed && self.value.is_some() {
self.value = None;
self.residency_degraded = true;
}
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ClientWireMessage {
Handshake(VersionHandshake),
Request(ClientRequestEnvelope),
Response(ClientResponseEnvelope),
Invalidation(InvalidationEvent),
Heartbeat(Watermark),
}
fn redact_message(message: String) -> String {
let mut redacted = message;
for marker in ["value=", "secret=", "token="] {
if let Some(index) = redacted.find(marker) {
redacted.truncate(index + marker.len());
redacted.push_str("<redacted>");
}
}
redacted
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ClientProtocolError {
#[error("client frame is {actual} bytes, exceeding max_frame_bytes={max}")]
FrameTooLarge {
actual: usize,
max: usize,
},
#[error("truncated client frame: {actual} bytes available, {needed} needed")]
TruncatedFrame {
actual: usize,
needed: usize,
},
#[error(
"client frame length mismatch: declared body {declared} bytes, actual body {actual} bytes"
)]
LengthMismatch {
declared: usize,
actual: usize,
},
#[error("unsupported client protocol version {version}; supported max is {supported_max}")]
UnsupportedVersion {
version: u16,
supported_max: u16,
},
#[error("client protocol codec error: {0}")]
Codec(String),
#[error("invalid client protocol field: {0}")]
InvalidField(&'static str),
}