1use bytes::Bytes;
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11pub mod hibernate;
12pub mod java_migration;
13
14pub const PROTOCOL_VERSION: u16 = 1;
16
17pub const LENGTH_PREFIX_BYTES: usize = 4;
19
20pub const VERSION_BYTES: usize = 2;
22
23pub const MIN_FRAME_BYTES: usize = LENGTH_PREFIX_BYTES + VERSION_BYTES;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct ClientFrame {
38 protocol_version: u16,
39 payload: Bytes,
40}
41
42impl ClientFrame {
43 pub fn new(payload: impl Into<Bytes>) -> Self {
45 Self {
46 protocol_version: PROTOCOL_VERSION,
47 payload: payload.into(),
48 }
49 }
50
51 pub fn from_message(message: &ClientWireMessage) -> Result<Self, ClientProtocolError> {
53 let payload = postcard::to_allocvec(message)
54 .map_err(|error| ClientProtocolError::Codec(error.to_string()))?;
55 Ok(Self::new(payload))
56 }
57
58 pub fn with_version(protocol_version: u16, payload: impl Into<Bytes>) -> Self {
60 Self {
61 protocol_version,
62 payload: payload.into(),
63 }
64 }
65
66 pub fn protocol_version(&self) -> u16 {
68 self.protocol_version
69 }
70
71 pub fn payload(&self) -> &Bytes {
73 &self.payload
74 }
75
76 pub fn encode(&self) -> Result<Bytes, ClientProtocolError> {
78 let body_len = VERSION_BYTES.checked_add(self.payload.len()).ok_or(
79 ClientProtocolError::FrameTooLarge {
80 actual: usize::MAX,
81 max: u32::MAX as usize,
82 },
83 )?;
84 if body_len > u32::MAX as usize {
85 return Err(ClientProtocolError::FrameTooLarge {
86 actual: body_len,
87 max: u32::MAX as usize,
88 });
89 }
90
91 let mut out = Vec::with_capacity(LENGTH_PREFIX_BYTES + body_len);
92 out.extend_from_slice(&(body_len as u32).to_be_bytes());
93 out.extend_from_slice(&self.protocol_version.to_be_bytes());
94 out.extend_from_slice(&self.payload);
95 Ok(Bytes::from(out))
96 }
97
98 pub fn decode_message(&self) -> Result<ClientWireMessage, ClientProtocolError> {
100 postcard::from_bytes(self.payload.as_ref())
101 .map_err(|error| ClientProtocolError::Codec(error.to_string()))
102 }
103
104 pub fn decode(bytes: &[u8], max_frame_bytes: usize) -> Result<Self, ClientProtocolError> {
106 if bytes.len() > max_frame_bytes {
107 return Err(ClientProtocolError::FrameTooLarge {
108 actual: bytes.len(),
109 max: max_frame_bytes,
110 });
111 }
112 if bytes.len() < MIN_FRAME_BYTES {
113 return Err(ClientProtocolError::TruncatedFrame {
114 actual: bytes.len(),
115 needed: MIN_FRAME_BYTES,
116 });
117 }
118
119 let body_len = u32::from_be_bytes(
120 bytes[0..LENGTH_PREFIX_BYTES]
121 .try_into()
122 .expect("slice length is checked"),
123 ) as usize;
124 if body_len < VERSION_BYTES {
125 return Err(ClientProtocolError::TruncatedFrame {
126 actual: body_len,
127 needed: VERSION_BYTES,
128 });
129 }
130
131 let expected = LENGTH_PREFIX_BYTES + body_len;
132 if expected != bytes.len() {
133 return Err(ClientProtocolError::LengthMismatch {
134 declared: body_len,
135 actual: bytes.len().saturating_sub(LENGTH_PREFIX_BYTES),
136 });
137 }
138
139 let version_start = LENGTH_PREFIX_BYTES;
140 let version_end = version_start + VERSION_BYTES;
141 let protocol_version = u16::from_be_bytes(
142 bytes[version_start..version_end]
143 .try_into()
144 .expect("slice length is checked"),
145 );
146 if protocol_version > PROTOCOL_VERSION {
147 return Err(ClientProtocolError::UnsupportedVersion {
148 version: protocol_version,
149 supported_max: PROTOCOL_VERSION,
150 });
151 }
152
153 Ok(Self {
154 protocol_version,
155 payload: Bytes::copy_from_slice(&bytes[version_end..]),
156 })
157 }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
162pub struct VersionHandshake {
163 pub min: u16,
165 pub max: u16,
167}
168
169impl VersionHandshake {
170 pub fn new(min: u16, max: u16) -> Self {
172 Self { min, max }
173 }
174
175 pub fn negotiate(self, server: VersionHandshake) -> Result<u16, ClientErrorEnvelope> {
177 let min = self.min.max(server.min);
178 let max = self.max.min(server.max);
179 if min <= max {
180 Ok(max)
181 } else {
182 Err(ClientErrorEnvelope::new(
183 ClientErrorCode::IncompatibleVersion,
184 false,
185 "no common HydraCache client protocol version",
186 ))
187 }
188 }
189}
190
191impl Default for VersionHandshake {
192 fn default() -> Self {
193 Self {
194 min: PROTOCOL_VERSION,
195 max: PROTOCOL_VERSION,
196 }
197 }
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
202pub struct Namespace(String);
203
204impl Namespace {
205 pub fn new(value: impl Into<String>) -> Result<Self, ClientProtocolError> {
207 let value = value.into();
208 if value.trim().is_empty() {
209 return Err(ClientProtocolError::InvalidField("namespace"));
210 }
211 Ok(Self(value))
212 }
213
214 pub fn as_str(&self) -> &str {
216 &self.0
217 }
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
222pub struct RegionId(String);
223
224impl RegionId {
225 pub fn new(value: impl Into<String>) -> Result<Self, ClientProtocolError> {
227 let value = value.into();
228 if value.trim().is_empty() {
229 return Err(ClientProtocolError::InvalidField("region"));
230 }
231 Ok(Self(value))
232 }
233
234 pub fn as_str(&self) -> &str {
236 &self.0
237 }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
242pub struct StructuredKey {
243 segments: Vec<String>,
244}
245
246impl StructuredKey {
247 pub fn new(segments: Vec<String>) -> Result<Self, ClientProtocolError> {
249 if segments.is_empty() || segments.iter().any(|segment| segment.trim().is_empty()) {
250 return Err(ClientProtocolError::InvalidField("key_segments"));
251 }
252 Ok(Self { segments })
253 }
254
255 pub fn segments(&self) -> &[String] {
257 &self.segments
258 }
259
260 pub fn stable_key(&self) -> String {
262 self.segments.join(":")
263 }
264}
265
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
268#[serde(rename_all = "snake_case")]
269pub enum ReadConsistency {
270 Eventual,
272 Strong,
274 Session,
276}
277
278#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
280#[serde(rename_all = "snake_case")]
281pub enum WriteConsistency {
282 Local,
284 Quorum,
286}
287
288#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
290pub struct ClientContext {
291 pub session_token: Option<String>,
293 pub read: Option<ReadConsistency>,
295 pub write: Option<WriteConsistency>,
297 pub preferred_region: Option<RegionId>,
299}
300
301#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
303pub struct Watermark {
304 pub source_generation: u64,
306 pub message_id: u64,
308}
309
310impl Watermark {
311 pub const fn new(source_generation: u64, message_id: u64) -> Self {
313 Self {
314 source_generation,
315 message_id,
316 }
317 }
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
322#[serde(rename_all = "snake_case")]
323pub enum RepairAction {
324 Apply,
326 ClearPartition,
328 InvalidateConservatively,
330}
331
332#[derive(Debug, Clone, Default, PartialEq, Eq)]
334pub struct SubscriptionWatermarkTracker {
335 last: Option<Watermark>,
336}
337
338impl SubscriptionWatermarkTracker {
339 pub fn on_event(&mut self, event: &InvalidationEvent) -> RepairAction {
341 let next = event.watermark();
342 let Some(last) = self.last else {
343 self.last = Some(next);
344 return RepairAction::ClearPartition;
345 };
346
347 if next.source_generation != last.source_generation {
348 self.last = Some(next);
349 return RepairAction::ClearPartition;
350 }
351 if next.message_id > last.message_id.saturating_add(1) {
352 self.last = Some(next);
353 return RepairAction::InvalidateConservatively;
354 }
355 self.last = Some(Watermark::new(
356 last.source_generation,
357 last.message_id.max(next.message_id),
358 ));
359 RepairAction::Apply
360 }
361}
362
363#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
365pub struct ClientRequestEnvelope {
366 pub request_id: String,
368 pub protocol_version: u16,
370 pub context: ClientContext,
372 pub deadline_ms: Option<u64>,
374 pub idempotency_key: Option<String>,
376 pub request: ClientRequest,
378}
379
380impl ClientRequestEnvelope {
381 pub fn new(request_id: impl Into<String>, request: ClientRequest) -> Self {
383 Self {
384 request_id: request_id.into(),
385 protocol_version: PROTOCOL_VERSION,
386 context: ClientContext::default(),
387 deadline_ms: None,
388 idempotency_key: None,
389 request,
390 }
391 }
392
393 pub fn with_context(mut self, context: ClientContext) -> Self {
395 self.context = context;
396 self
397 }
398
399 pub fn with_deadline_ms(mut self, deadline_ms: u64) -> Self {
401 self.deadline_ms = Some(deadline_ms);
402 self
403 }
404
405 pub fn with_idempotency_key(mut self, idempotency_key: impl Into<String>) -> Self {
407 self.idempotency_key = Some(idempotency_key.into());
408 self
409 }
410
411 pub fn deadline_expired(&self, now_ms: u64) -> bool {
413 self.deadline_ms.is_some_and(|deadline| deadline <= now_ms)
414 }
415}
416
417#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
419#[serde(rename_all = "snake_case")]
420pub enum ClientRequest {
421 Get { ns: Namespace, key: StructuredKey },
423 Put {
425 ns: Namespace,
426 key: StructuredKey,
427 value: Vec<u8>,
428 ttl_ms: Option<u64>,
429 dimensions: Vec<String>,
430 },
431 Invalidate { ns: Namespace, key: StructuredKey },
433 BatchGet {
435 ns: Namespace,
436 keys: Vec<StructuredKey>,
437 },
438 BatchPut {
440 ns: Namespace,
441 entries: Vec<BatchPutEntry>,
442 },
443 EvictRegion { ns: Namespace },
445 SubscribeInvalidations {
447 ns: Namespace,
448 region: Option<RegionId>,
449 from: Option<Watermark>,
450 include_value: bool,
451 },
452}
453
454#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
456pub struct BatchPutEntry {
457 pub key: StructuredKey,
459 pub value: Vec<u8>,
461}
462
463#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
465pub struct ClientResponseEnvelope {
466 pub request_id: String,
468 pub protocol_version: u16,
470 pub result: Result<ClientResponse, ClientErrorEnvelope>,
472}
473
474impl ClientResponseEnvelope {
475 pub fn ok(request_id: impl Into<String>, response: ClientResponse) -> Self {
477 Self {
478 request_id: request_id.into(),
479 protocol_version: PROTOCOL_VERSION,
480 result: Ok(response),
481 }
482 }
483
484 pub fn error(request_id: impl Into<String>, error: ClientErrorEnvelope) -> Self {
486 Self {
487 request_id: request_id.into(),
488 protocol_version: PROTOCOL_VERSION,
489 result: Err(error),
490 }
491 }
492}
493
494#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
496#[serde(rename_all = "snake_case")]
497pub enum ClientResponse {
498 Value { value: Option<Vec<u8>> },
500 Stored,
502 Invalidated,
504 Batch { items: Vec<BatchItemStatus> },
506 Evicted,
508 Subscribed { from: Option<Watermark> },
510}
511
512#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
514pub struct BatchItemStatus {
515 pub index: usize,
517 pub result: Result<Option<Vec<u8>>, ClientErrorEnvelope>,
519}
520
521#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
523pub struct ClientErrorEnvelope {
524 pub code: ClientErrorCode,
526 pub retryable: bool,
528 pub retry_after_ms: Option<u64>,
530 pub message: String,
532}
533
534impl ClientErrorEnvelope {
535 pub fn new(code: ClientErrorCode, retryable: bool, message: impl Into<String>) -> Self {
537 Self {
538 code,
539 retryable,
540 retry_after_ms: None,
541 message: redact_message(message.into()),
542 }
543 }
544
545 pub fn with_retry_after_ms(mut self, retry_after_ms: u64) -> Self {
547 self.retry_after_ms = Some(retry_after_ms);
548 self
549 }
550}
551
552#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
554#[serde(rename_all = "snake_case")]
555pub enum ClientErrorCode {
556 IncompatibleVersion,
558 Unauthenticated,
560 Unauthorized,
562 TenantQuota,
564 RateLimited,
566 ResidencyDenied,
568 TooLarge,
570 DeadlineExceeded,
572 Conflict,
574 BackendUnavailable,
576 MalformedFrame,
578}
579
580#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
582pub struct InvalidationEvent {
583 pub ns: Namespace,
585 pub key: StructuredKey,
587 pub generation: u64,
589 pub message_id: u64,
591 pub applied_region: Option<RegionId>,
593 pub value: Option<Vec<u8>>,
595 pub residency_degraded: bool,
597 pub affects_subscriber_view: bool,
599}
600
601impl InvalidationEvent {
602 pub fn new(ns: Namespace, key: StructuredKey, generation: u64, message_id: u64) -> Self {
604 Self {
605 ns,
606 key,
607 generation,
608 message_id,
609 applied_region: None,
610 value: None,
611 residency_degraded: false,
612 affects_subscriber_view: false,
613 }
614 }
615
616 pub fn applied_in(mut self, region: RegionId) -> Self {
618 self.applied_region = Some(region);
619 self
620 }
621
622 pub fn with_value(mut self, value: Vec<u8>) -> Self {
624 self.value = Some(value);
625 self
626 }
627
628 pub fn affects_subscriber_view(mut self) -> Self {
630 self.affects_subscriber_view = true;
631 self
632 }
633
634 pub fn watermark(&self) -> Watermark {
636 Watermark::new(self.generation, self.message_id)
637 }
638
639 pub fn should_deliver_to(&self, region: Option<&RegionId>) -> bool {
641 match region {
642 None => true,
643 Some(region) => {
644 self.applied_region.as_ref() == Some(region) || self.affects_subscriber_view
645 }
646 }
647 }
648
649 pub fn residency_gated(mut self, value_allowed: bool) -> Self {
651 if !value_allowed && self.value.is_some() {
652 self.value = None;
653 self.residency_degraded = true;
654 }
655 self
656 }
657}
658
659#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
661#[serde(rename_all = "snake_case")]
662pub enum ClientWireMessage {
663 Handshake(VersionHandshake),
665 Request(ClientRequestEnvelope),
667 Response(ClientResponseEnvelope),
669 Invalidation(InvalidationEvent),
671 Heartbeat(Watermark),
673}
674
675fn redact_message(message: String) -> String {
676 let mut redacted = message;
677 for marker in ["value=", "secret=", "token="] {
678 if let Some(index) = redacted.find(marker) {
679 redacted.truncate(index + marker.len());
680 redacted.push_str("<redacted>");
681 }
682 }
683 redacted
684}
685
686#[derive(Debug, Clone, PartialEq, Eq, Error)]
688pub enum ClientProtocolError {
689 #[error("client frame is {actual} bytes, exceeding max_frame_bytes={max}")]
691 FrameTooLarge {
692 actual: usize,
694 max: usize,
696 },
697 #[error("truncated client frame: {actual} bytes available, {needed} needed")]
699 TruncatedFrame {
700 actual: usize,
702 needed: usize,
704 },
705 #[error(
707 "client frame length mismatch: declared body {declared} bytes, actual body {actual} bytes"
708 )]
709 LengthMismatch {
710 declared: usize,
712 actual: usize,
714 },
715 #[error("unsupported client protocol version {version}; supported max is {supported_max}")]
717 UnsupportedVersion {
718 version: u16,
720 supported_max: u16,
722 },
723 #[error("client protocol codec error: {0}")]
725 Codec(String),
726 #[error("invalid client protocol field: {0}")]
728 InvalidField(&'static str),
729}