use bytes::Bytes;
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub mod hibernate;
pub mod java_migration;
pub const MIN_PROTOCOL_VERSION: u16 = 1;
pub const PROTOCOL_VERSION: u16 = 2;
pub const LOCK_PROTOCOL_VERSION: u16 = 2;
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 from_message_with_version(
protocol_version: u16,
message: &ClientWireMessage,
) -> Result<Self, ClientProtocolError> {
let payload = postcard::to_allocvec(message)
.map_err(|error| ClientProtocolError::Codec(error.to_string()))?;
Ok(Self::with_version(protocol_version, 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 !(MIN_PROTOCOL_VERSION..=PROTOCOL_VERSION).contains(&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: MIN_PROTOCOL_VERSION,
max: PROTOCOL_VERSION,
}
}
}
pub fn protocol_version_supported(protocol_version: u16) -> bool {
(MIN_PROTOCOL_VERSION..=PROTOCOL_VERSION).contains(&protocol_version)
}
pub fn ensure_supported_protocol_version(protocol_version: u16) -> Result<(), ClientErrorEnvelope> {
if protocol_version_supported(protocol_version) {
Ok(())
} else {
Err(ClientErrorEnvelope::new(
ClientErrorCode::IncompatibleVersion,
false,
format!(
"unsupported HydraCache client protocol version {protocol_version}; supported range is {MIN_PROTOCOL_VERSION}..={PROTOCOL_VERSION}"
),
))
}
}
pub fn require_protocol_version(
protocol_version: u16,
required_min: u16,
operation: &'static str,
) -> Result<(), ClientErrorEnvelope> {
ensure_supported_protocol_version(protocol_version)?;
if protocol_version >= required_min {
Ok(())
} else {
Err(ClientErrorEnvelope::new(
ClientErrorCode::IncompatibleVersion,
false,
format!(
"{operation} requires HydraCache client protocol version {required_min} or newer"
),
))
}
}
#[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, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LockConsistency {
One,
#[default]
Quorum,
EachQuorum,
All,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CasExpectation {
Exact(Vec<u8>),
Present,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EntryEventProjection {
Invalidation,
IMapEntryEvent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EntryEventSource {
Stored,
Removed,
KeyInvalidated,
TagInvalidated,
Flushed,
Expired,
Evicted,
StaleLoadDiscarded,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EntryEventKind {
Upserted,
Removed,
Evicted,
Invalidated,
}
impl EntryEventKind {
pub const fn from_source(source: EntryEventSource) -> Self {
match source {
EntryEventSource::Stored => Self::Upserted,
EntryEventSource::Removed => Self::Removed,
EntryEventSource::Expired | EntryEventSource::Evicted => Self::Evicted,
EntryEventSource::KeyInvalidated
| EntryEventSource::TagInvalidated
| EntryEventSource::Flushed
| EntryEventSource::StaleLoadDiscarded
| EntryEventSource::Unknown => Self::Invalidated,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EntryEvent {
pub ns: Namespace,
pub key: Option<StructuredKey>,
pub kind: EntryEventKind,
pub value: Option<Vec<u8>>,
pub residency_degraded: bool,
pub watermark: Option<Watermark>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct EntryListenerContract {
pub coalesced: bool,
pub bounded_buffer: bool,
pub lag_drop_counter: bool,
pub business_event_log: bool,
}
impl EntryListenerContract {
pub const fn cache_signal() -> Self {
Self {
coalesced: true,
bounded_buffer: true,
lag_drop_counter: true,
business_event_log: false,
}
}
}
#[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)
}
pub fn validate_protocol(&self) -> Result<(), ClientErrorEnvelope> {
self.request.ensure_supported_by(self.protocol_version)
}
}
#[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,
},
SubscribeEntryEvents {
ns: Namespace,
region: Option<RegionId>,
from: Option<Watermark>,
include_value: bool,
projection: EntryEventProjection,
},
TryLock {
ns: Namespace,
key: StructuredKey,
lease_ms: u64,
wait_ms: u64,
level: LockConsistency,
},
Unlock {
ns: Namespace,
key: StructuredKey,
fence: u64,
},
RenewLockLease {
ns: Namespace,
key: StructuredKey,
fence: u64,
lease_ms: u64,
},
ForceUnlock { ns: Namespace, key: StructuredKey },
GetLockOwnership { ns: Namespace, key: StructuredKey },
CompareAndSet {
ns: Namespace,
key: StructuredKey,
expected: CasExpectation,
new_value: Vec<u8>,
level: LockConsistency,
},
RemoveIfValue {
ns: Namespace,
key: StructuredKey,
expected: Vec<u8>,
level: LockConsistency,
},
}
impl ClientRequest {
pub fn minimum_protocol_version(&self) -> u16 {
match self {
Self::Get { .. }
| Self::Put { .. }
| Self::Invalidate { .. }
| Self::BatchGet { .. }
| Self::BatchPut { .. }
| Self::EvictRegion { .. }
| Self::SubscribeInvalidations { .. } => MIN_PROTOCOL_VERSION,
Self::SubscribeEntryEvents { .. }
| Self::TryLock { .. }
| Self::Unlock { .. }
| Self::RenewLockLease { .. }
| Self::ForceUnlock { .. }
| Self::GetLockOwnership { .. }
| Self::CompareAndSet { .. }
| Self::RemoveIfValue { .. } => LOCK_PROTOCOL_VERSION,
}
}
pub fn ensure_supported_by(&self, protocol_version: u16) -> Result<(), ClientErrorEnvelope> {
require_protocol_version(
protocol_version,
self.minimum_protocol_version(),
self.operation_name(),
)
}
fn operation_name(&self) -> &'static str {
match self {
Self::Get { .. } => "get",
Self::Put { .. } => "put",
Self::Invalidate { .. } => "invalidate",
Self::BatchGet { .. } => "batch_get",
Self::BatchPut { .. } => "batch_put",
Self::EvictRegion { .. } => "evict_region",
Self::SubscribeInvalidations { .. } => "subscribe_invalidations",
Self::SubscribeEntryEvents { .. } => "subscribe_entry_events",
Self::TryLock { .. } => "try_lock",
Self::Unlock { .. } => "unlock",
Self::RenewLockLease { .. } => "renew_lock_lease",
Self::ForceUnlock { .. } => "force_unlock",
Self::GetLockOwnership { .. } => "get_lock_ownership",
Self::CompareAndSet { .. } => "compare_and_set",
Self::RemoveIfValue { .. } => "remove_if_value",
}
}
}
#[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),
}
}
pub fn with_protocol_version(mut self, protocol_version: u16) -> Self {
self.protocol_version = protocol_version;
self
}
}
#[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> },
LockAcquired { fence: u64 },
LockBusy,
LockReleased,
LockLeaseRenewed,
LockOwnership { fence: Option<u64>, locked: bool },
CasApplied { new_version: u64 },
CasMismatch { current: Option<Vec<u8>> },
}
#[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
}
}
impl EntryEvent {
pub fn from_invalidation(event: InvalidationEvent) -> Self {
let watermark = event.watermark();
Self {
ns: event.ns,
key: Some(event.key),
kind: EntryEventKind::Invalidated,
value: event.value,
residency_degraded: event.residency_degraded,
watermark: Some(watermark),
}
}
pub fn from_source(
ns: Namespace,
key: Option<StructuredKey>,
source: EntryEventSource,
value: Option<Vec<u8>>,
watermark: Option<Watermark>,
) -> Self {
Self {
ns,
key,
kind: EntryEventKind::from_source(source),
value,
residency_degraded: false,
watermark,
}
}
}
#[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),
}
impl ClientWireMessage {
pub fn protocol_version(&self) -> u16 {
match self {
Self::Handshake(handshake) => handshake.max,
Self::Request(envelope) => envelope.protocol_version,
Self::Response(envelope) => envelope.protocol_version,
Self::Invalidation(_) | Self::Heartbeat(_) => PROTOCOL_VERSION,
}
}
}
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),
}