1use bytes::Bytes;
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11pub mod hibernate;
12pub mod java_migration;
13
14pub const MIN_PROTOCOL_VERSION: u16 = 1;
16
17pub const PROTOCOL_VERSION: u16 = 2;
19
20pub const LOCK_PROTOCOL_VERSION: u16 = 2;
22
23pub const LENGTH_PREFIX_BYTES: usize = 4;
25
26pub const VERSION_BYTES: usize = 2;
28
29pub const MIN_FRAME_BYTES: usize = LENGTH_PREFIX_BYTES + VERSION_BYTES;
31
32#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ClientFrame {
44 protocol_version: u16,
45 payload: Bytes,
46}
47
48impl ClientFrame {
49 pub fn new(payload: impl Into<Bytes>) -> Self {
51 Self {
52 protocol_version: PROTOCOL_VERSION,
53 payload: payload.into(),
54 }
55 }
56
57 pub fn from_message(message: &ClientWireMessage) -> Result<Self, ClientProtocolError> {
59 let payload = postcard::to_allocvec(message)
60 .map_err(|error| ClientProtocolError::Codec(error.to_string()))?;
61 Ok(Self::new(payload))
62 }
63
64 pub fn from_message_with_version(
66 protocol_version: u16,
67 message: &ClientWireMessage,
68 ) -> Result<Self, ClientProtocolError> {
69 let payload = postcard::to_allocvec(message)
70 .map_err(|error| ClientProtocolError::Codec(error.to_string()))?;
71 Ok(Self::with_version(protocol_version, payload))
72 }
73
74 pub fn with_version(protocol_version: u16, payload: impl Into<Bytes>) -> Self {
76 Self {
77 protocol_version,
78 payload: payload.into(),
79 }
80 }
81
82 pub fn protocol_version(&self) -> u16 {
84 self.protocol_version
85 }
86
87 pub fn payload(&self) -> &Bytes {
89 &self.payload
90 }
91
92 pub fn encode(&self) -> Result<Bytes, ClientProtocolError> {
94 let body_len = VERSION_BYTES.checked_add(self.payload.len()).ok_or(
95 ClientProtocolError::FrameTooLarge {
96 actual: usize::MAX,
97 max: u32::MAX as usize,
98 },
99 )?;
100 if body_len > u32::MAX as usize {
101 return Err(ClientProtocolError::FrameTooLarge {
102 actual: body_len,
103 max: u32::MAX as usize,
104 });
105 }
106
107 let mut out = Vec::with_capacity(LENGTH_PREFIX_BYTES + body_len);
108 out.extend_from_slice(&(body_len as u32).to_be_bytes());
109 out.extend_from_slice(&self.protocol_version.to_be_bytes());
110 out.extend_from_slice(&self.payload);
111 Ok(Bytes::from(out))
112 }
113
114 pub fn decode_message(&self) -> Result<ClientWireMessage, ClientProtocolError> {
116 postcard::from_bytes(self.payload.as_ref())
117 .map_err(|error| ClientProtocolError::Codec(error.to_string()))
118 }
119
120 pub fn decode(bytes: &[u8], max_frame_bytes: usize) -> Result<Self, ClientProtocolError> {
122 if bytes.len() > max_frame_bytes {
123 return Err(ClientProtocolError::FrameTooLarge {
124 actual: bytes.len(),
125 max: max_frame_bytes,
126 });
127 }
128 if bytes.len() < MIN_FRAME_BYTES {
129 return Err(ClientProtocolError::TruncatedFrame {
130 actual: bytes.len(),
131 needed: MIN_FRAME_BYTES,
132 });
133 }
134
135 let body_len = u32::from_be_bytes(
136 bytes[0..LENGTH_PREFIX_BYTES]
137 .try_into()
138 .expect("slice length is checked"),
139 ) as usize;
140 if body_len < VERSION_BYTES {
141 return Err(ClientProtocolError::TruncatedFrame {
142 actual: body_len,
143 needed: VERSION_BYTES,
144 });
145 }
146
147 let expected = LENGTH_PREFIX_BYTES + body_len;
148 if expected != bytes.len() {
149 return Err(ClientProtocolError::LengthMismatch {
150 declared: body_len,
151 actual: bytes.len().saturating_sub(LENGTH_PREFIX_BYTES),
152 });
153 }
154
155 let version_start = LENGTH_PREFIX_BYTES;
156 let version_end = version_start + VERSION_BYTES;
157 let protocol_version = u16::from_be_bytes(
158 bytes[version_start..version_end]
159 .try_into()
160 .expect("slice length is checked"),
161 );
162 if !(MIN_PROTOCOL_VERSION..=PROTOCOL_VERSION).contains(&protocol_version) {
163 return Err(ClientProtocolError::UnsupportedVersion {
164 version: protocol_version,
165 supported_max: PROTOCOL_VERSION,
166 });
167 }
168
169 Ok(Self {
170 protocol_version,
171 payload: Bytes::copy_from_slice(&bytes[version_end..]),
172 })
173 }
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
178pub struct VersionHandshake {
179 pub min: u16,
181 pub max: u16,
183}
184
185impl VersionHandshake {
186 pub fn new(min: u16, max: u16) -> Self {
188 Self { min, max }
189 }
190
191 pub fn negotiate(self, server: VersionHandshake) -> Result<u16, ClientErrorEnvelope> {
193 let min = self.min.max(server.min);
194 let max = self.max.min(server.max);
195 if min <= max {
196 Ok(max)
197 } else {
198 Err(ClientErrorEnvelope::new(
199 ClientErrorCode::IncompatibleVersion,
200 false,
201 "no common HydraCache client protocol version",
202 ))
203 }
204 }
205}
206
207impl Default for VersionHandshake {
208 fn default() -> Self {
209 Self {
210 min: MIN_PROTOCOL_VERSION,
211 max: PROTOCOL_VERSION,
212 }
213 }
214}
215
216pub fn protocol_version_supported(protocol_version: u16) -> bool {
218 (MIN_PROTOCOL_VERSION..=PROTOCOL_VERSION).contains(&protocol_version)
219}
220
221pub fn ensure_supported_protocol_version(protocol_version: u16) -> Result<(), ClientErrorEnvelope> {
223 if protocol_version_supported(protocol_version) {
224 Ok(())
225 } else {
226 Err(ClientErrorEnvelope::new(
227 ClientErrorCode::IncompatibleVersion,
228 false,
229 format!(
230 "unsupported HydraCache client protocol version {protocol_version}; supported range is {MIN_PROTOCOL_VERSION}..={PROTOCOL_VERSION}"
231 ),
232 ))
233 }
234}
235
236pub fn require_protocol_version(
238 protocol_version: u16,
239 required_min: u16,
240 operation: &'static str,
241) -> Result<(), ClientErrorEnvelope> {
242 ensure_supported_protocol_version(protocol_version)?;
243 if protocol_version >= required_min {
244 Ok(())
245 } else {
246 Err(ClientErrorEnvelope::new(
247 ClientErrorCode::IncompatibleVersion,
248 false,
249 format!(
250 "{operation} requires HydraCache client protocol version {required_min} or newer"
251 ),
252 ))
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
258pub struct Namespace(String);
259
260impl Namespace {
261 pub fn new(value: impl Into<String>) -> Result<Self, ClientProtocolError> {
263 let value = value.into();
264 if value.trim().is_empty() {
265 return Err(ClientProtocolError::InvalidField("namespace"));
266 }
267 Ok(Self(value))
268 }
269
270 pub fn as_str(&self) -> &str {
272 &self.0
273 }
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
278pub struct RegionId(String);
279
280impl RegionId {
281 pub fn new(value: impl Into<String>) -> Result<Self, ClientProtocolError> {
283 let value = value.into();
284 if value.trim().is_empty() {
285 return Err(ClientProtocolError::InvalidField("region"));
286 }
287 Ok(Self(value))
288 }
289
290 pub fn as_str(&self) -> &str {
292 &self.0
293 }
294}
295
296#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
298pub struct StructuredKey {
299 segments: Vec<String>,
300}
301
302impl StructuredKey {
303 pub fn new(segments: Vec<String>) -> Result<Self, ClientProtocolError> {
305 if segments.is_empty() || segments.iter().any(|segment| segment.trim().is_empty()) {
306 return Err(ClientProtocolError::InvalidField("key_segments"));
307 }
308 Ok(Self { segments })
309 }
310
311 pub fn segments(&self) -> &[String] {
313 &self.segments
314 }
315
316 pub fn stable_key(&self) -> String {
318 self.segments.join(":")
319 }
320}
321
322#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
324#[serde(rename_all = "snake_case")]
325pub enum ReadConsistency {
326 Eventual,
328 Strong,
330 Session,
332}
333
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
336#[serde(rename_all = "snake_case")]
337pub enum WriteConsistency {
338 Local,
340 Quorum,
342}
343
344#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
346#[serde(rename_all = "snake_case")]
347pub enum LockConsistency {
348 One,
350 #[default]
352 Quorum,
353 EachQuorum,
355 All,
357}
358
359#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
361#[serde(rename_all = "snake_case")]
362pub enum CasExpectation {
363 Exact(Vec<u8>),
365 Present,
367}
368
369#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
371#[serde(rename_all = "snake_case")]
372pub enum EntryEventProjection {
373 Invalidation,
375 IMapEntryEvent,
377}
378
379#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
381#[serde(rename_all = "snake_case")]
382pub enum EntryEventSource {
383 Stored,
385 Removed,
387 KeyInvalidated,
389 TagInvalidated,
391 Flushed,
393 Expired,
395 Evicted,
397 StaleLoadDiscarded,
399 Unknown,
401}
402
403#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
405#[serde(rename_all = "snake_case")]
406pub enum EntryEventKind {
407 Upserted,
409 Removed,
411 Evicted,
413 Invalidated,
415}
416
417impl EntryEventKind {
418 pub const fn from_source(source: EntryEventSource) -> Self {
420 match source {
421 EntryEventSource::Stored => Self::Upserted,
422 EntryEventSource::Removed => Self::Removed,
423 EntryEventSource::Expired | EntryEventSource::Evicted => Self::Evicted,
424 EntryEventSource::KeyInvalidated
425 | EntryEventSource::TagInvalidated
426 | EntryEventSource::Flushed
427 | EntryEventSource::StaleLoadDiscarded
428 | EntryEventSource::Unknown => Self::Invalidated,
429 }
430 }
431}
432
433#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
435pub struct EntryEvent {
436 pub ns: Namespace,
438 pub key: Option<StructuredKey>,
440 pub kind: EntryEventKind,
442 pub value: Option<Vec<u8>>,
444 pub residency_degraded: bool,
446 pub watermark: Option<Watermark>,
448}
449
450#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
452pub struct EntryListenerContract {
453 pub coalesced: bool,
455 pub bounded_buffer: bool,
457 pub lag_drop_counter: bool,
459 pub business_event_log: bool,
461}
462
463impl EntryListenerContract {
464 pub const fn cache_signal() -> Self {
466 Self {
467 coalesced: true,
468 bounded_buffer: true,
469 lag_drop_counter: true,
470 business_event_log: false,
471 }
472 }
473}
474
475#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
477pub struct ClientContext {
478 pub session_token: Option<String>,
480 pub read: Option<ReadConsistency>,
482 pub write: Option<WriteConsistency>,
484 pub preferred_region: Option<RegionId>,
486}
487
488#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
490pub struct Watermark {
491 pub source_generation: u64,
493 pub message_id: u64,
495}
496
497impl Watermark {
498 pub const fn new(source_generation: u64, message_id: u64) -> Self {
500 Self {
501 source_generation,
502 message_id,
503 }
504 }
505}
506
507#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
509#[serde(rename_all = "snake_case")]
510pub enum RepairAction {
511 Apply,
513 ClearPartition,
515 InvalidateConservatively,
517}
518
519#[derive(Debug, Clone, Default, PartialEq, Eq)]
521pub struct SubscriptionWatermarkTracker {
522 last: Option<Watermark>,
523}
524
525impl SubscriptionWatermarkTracker {
526 pub fn on_event(&mut self, event: &InvalidationEvent) -> RepairAction {
528 let next = event.watermark();
529 let Some(last) = self.last else {
530 self.last = Some(next);
531 return RepairAction::ClearPartition;
532 };
533
534 if next.source_generation != last.source_generation {
535 self.last = Some(next);
536 return RepairAction::ClearPartition;
537 }
538 if next.message_id > last.message_id.saturating_add(1) {
539 self.last = Some(next);
540 return RepairAction::InvalidateConservatively;
541 }
542 self.last = Some(Watermark::new(
543 last.source_generation,
544 last.message_id.max(next.message_id),
545 ));
546 RepairAction::Apply
547 }
548}
549
550#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
552pub struct ClientRequestEnvelope {
553 pub request_id: String,
555 pub protocol_version: u16,
557 pub context: ClientContext,
559 pub deadline_ms: Option<u64>,
561 pub idempotency_key: Option<String>,
563 pub request: ClientRequest,
565}
566
567impl ClientRequestEnvelope {
568 pub fn new(request_id: impl Into<String>, request: ClientRequest) -> Self {
570 Self {
571 request_id: request_id.into(),
572 protocol_version: PROTOCOL_VERSION,
573 context: ClientContext::default(),
574 deadline_ms: None,
575 idempotency_key: None,
576 request,
577 }
578 }
579
580 pub fn with_context(mut self, context: ClientContext) -> Self {
582 self.context = context;
583 self
584 }
585
586 pub fn with_deadline_ms(mut self, deadline_ms: u64) -> Self {
588 self.deadline_ms = Some(deadline_ms);
589 self
590 }
591
592 pub fn with_idempotency_key(mut self, idempotency_key: impl Into<String>) -> Self {
594 self.idempotency_key = Some(idempotency_key.into());
595 self
596 }
597
598 pub fn deadline_expired(&self, now_ms: u64) -> bool {
600 self.deadline_ms.is_some_and(|deadline| deadline <= now_ms)
601 }
602
603 pub fn validate_protocol(&self) -> Result<(), ClientErrorEnvelope> {
605 self.request.ensure_supported_by(self.protocol_version)
606 }
607}
608
609#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
611#[serde(rename_all = "snake_case")]
612pub enum ClientRequest {
613 Get { ns: Namespace, key: StructuredKey },
615 Put {
617 ns: Namespace,
618 key: StructuredKey,
619 value: Vec<u8>,
620 ttl_ms: Option<u64>,
621 dimensions: Vec<String>,
622 },
623 Invalidate { ns: Namespace, key: StructuredKey },
625 BatchGet {
627 ns: Namespace,
628 keys: Vec<StructuredKey>,
629 },
630 BatchPut {
632 ns: Namespace,
633 entries: Vec<BatchPutEntry>,
634 },
635 EvictRegion { ns: Namespace },
637 SubscribeInvalidations {
639 ns: Namespace,
640 region: Option<RegionId>,
641 from: Option<Watermark>,
642 include_value: bool,
643 },
644 SubscribeEntryEvents {
646 ns: Namespace,
647 region: Option<RegionId>,
648 from: Option<Watermark>,
649 include_value: bool,
650 projection: EntryEventProjection,
651 },
652 TryLock {
654 ns: Namespace,
655 key: StructuredKey,
656 lease_ms: u64,
657 wait_ms: u64,
658 level: LockConsistency,
659 },
660 Unlock {
662 ns: Namespace,
663 key: StructuredKey,
664 fence: u64,
665 },
666 RenewLockLease {
668 ns: Namespace,
669 key: StructuredKey,
670 fence: u64,
671 lease_ms: u64,
672 },
673 ForceUnlock { ns: Namespace, key: StructuredKey },
675 GetLockOwnership { ns: Namespace, key: StructuredKey },
677 CompareAndSet {
679 ns: Namespace,
680 key: StructuredKey,
681 expected: CasExpectation,
682 new_value: Vec<u8>,
683 level: LockConsistency,
684 },
685 RemoveIfValue {
687 ns: Namespace,
688 key: StructuredKey,
689 expected: Vec<u8>,
690 level: LockConsistency,
691 },
692}
693
694impl ClientRequest {
695 pub fn minimum_protocol_version(&self) -> u16 {
697 match self {
698 Self::Get { .. }
699 | Self::Put { .. }
700 | Self::Invalidate { .. }
701 | Self::BatchGet { .. }
702 | Self::BatchPut { .. }
703 | Self::EvictRegion { .. }
704 | Self::SubscribeInvalidations { .. } => MIN_PROTOCOL_VERSION,
705 Self::SubscribeEntryEvents { .. }
706 | Self::TryLock { .. }
707 | Self::Unlock { .. }
708 | Self::RenewLockLease { .. }
709 | Self::ForceUnlock { .. }
710 | Self::GetLockOwnership { .. }
711 | Self::CompareAndSet { .. }
712 | Self::RemoveIfValue { .. } => LOCK_PROTOCOL_VERSION,
713 }
714 }
715
716 pub fn ensure_supported_by(&self, protocol_version: u16) -> Result<(), ClientErrorEnvelope> {
718 require_protocol_version(
719 protocol_version,
720 self.minimum_protocol_version(),
721 self.operation_name(),
722 )
723 }
724
725 fn operation_name(&self) -> &'static str {
726 match self {
727 Self::Get { .. } => "get",
728 Self::Put { .. } => "put",
729 Self::Invalidate { .. } => "invalidate",
730 Self::BatchGet { .. } => "batch_get",
731 Self::BatchPut { .. } => "batch_put",
732 Self::EvictRegion { .. } => "evict_region",
733 Self::SubscribeInvalidations { .. } => "subscribe_invalidations",
734 Self::SubscribeEntryEvents { .. } => "subscribe_entry_events",
735 Self::TryLock { .. } => "try_lock",
736 Self::Unlock { .. } => "unlock",
737 Self::RenewLockLease { .. } => "renew_lock_lease",
738 Self::ForceUnlock { .. } => "force_unlock",
739 Self::GetLockOwnership { .. } => "get_lock_ownership",
740 Self::CompareAndSet { .. } => "compare_and_set",
741 Self::RemoveIfValue { .. } => "remove_if_value",
742 }
743 }
744}
745
746#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
748pub struct BatchPutEntry {
749 pub key: StructuredKey,
751 pub value: Vec<u8>,
753}
754
755#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
757pub struct ClientResponseEnvelope {
758 pub request_id: String,
760 pub protocol_version: u16,
762 pub result: Result<ClientResponse, ClientErrorEnvelope>,
764}
765
766impl ClientResponseEnvelope {
767 pub fn ok(request_id: impl Into<String>, response: ClientResponse) -> Self {
769 Self {
770 request_id: request_id.into(),
771 protocol_version: PROTOCOL_VERSION,
772 result: Ok(response),
773 }
774 }
775
776 pub fn error(request_id: impl Into<String>, error: ClientErrorEnvelope) -> Self {
778 Self {
779 request_id: request_id.into(),
780 protocol_version: PROTOCOL_VERSION,
781 result: Err(error),
782 }
783 }
784
785 pub fn with_protocol_version(mut self, protocol_version: u16) -> Self {
787 self.protocol_version = protocol_version;
788 self
789 }
790}
791
792#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
794#[serde(rename_all = "snake_case")]
795pub enum ClientResponse {
796 Value { value: Option<Vec<u8>> },
798 Stored,
800 Invalidated,
802 Batch { items: Vec<BatchItemStatus> },
804 Evicted,
806 Subscribed { from: Option<Watermark> },
808 LockAcquired { fence: u64 },
810 LockBusy,
812 LockReleased,
814 LockLeaseRenewed,
816 LockOwnership { fence: Option<u64>, locked: bool },
818 CasApplied { new_version: u64 },
820 CasMismatch { current: Option<Vec<u8>> },
822}
823
824#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
826pub struct BatchItemStatus {
827 pub index: usize,
829 pub result: Result<Option<Vec<u8>>, ClientErrorEnvelope>,
831}
832
833#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
835pub struct ClientErrorEnvelope {
836 pub code: ClientErrorCode,
838 pub retryable: bool,
840 pub retry_after_ms: Option<u64>,
842 pub message: String,
844}
845
846impl ClientErrorEnvelope {
847 pub fn new(code: ClientErrorCode, retryable: bool, message: impl Into<String>) -> Self {
849 Self {
850 code,
851 retryable,
852 retry_after_ms: None,
853 message: redact_message(message.into()),
854 }
855 }
856
857 pub fn with_retry_after_ms(mut self, retry_after_ms: u64) -> Self {
859 self.retry_after_ms = Some(retry_after_ms);
860 self
861 }
862}
863
864#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
866#[serde(rename_all = "snake_case")]
867pub enum ClientErrorCode {
868 IncompatibleVersion,
870 Unauthenticated,
872 Unauthorized,
874 TenantQuota,
876 RateLimited,
878 ResidencyDenied,
880 TooLarge,
882 DeadlineExceeded,
884 Conflict,
886 BackendUnavailable,
888 MalformedFrame,
890}
891
892#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
894pub struct InvalidationEvent {
895 pub ns: Namespace,
897 pub key: StructuredKey,
899 pub generation: u64,
901 pub message_id: u64,
903 pub applied_region: Option<RegionId>,
905 pub value: Option<Vec<u8>>,
907 pub residency_degraded: bool,
909 pub affects_subscriber_view: bool,
911}
912
913impl InvalidationEvent {
914 pub fn new(ns: Namespace, key: StructuredKey, generation: u64, message_id: u64) -> Self {
916 Self {
917 ns,
918 key,
919 generation,
920 message_id,
921 applied_region: None,
922 value: None,
923 residency_degraded: false,
924 affects_subscriber_view: false,
925 }
926 }
927
928 pub fn applied_in(mut self, region: RegionId) -> Self {
930 self.applied_region = Some(region);
931 self
932 }
933
934 pub fn with_value(mut self, value: Vec<u8>) -> Self {
936 self.value = Some(value);
937 self
938 }
939
940 pub fn affects_subscriber_view(mut self) -> Self {
942 self.affects_subscriber_view = true;
943 self
944 }
945
946 pub fn watermark(&self) -> Watermark {
948 Watermark::new(self.generation, self.message_id)
949 }
950
951 pub fn should_deliver_to(&self, region: Option<&RegionId>) -> bool {
953 match region {
954 None => true,
955 Some(region) => {
956 self.applied_region.as_ref() == Some(region) || self.affects_subscriber_view
957 }
958 }
959 }
960
961 pub fn residency_gated(mut self, value_allowed: bool) -> Self {
963 if !value_allowed && self.value.is_some() {
964 self.value = None;
965 self.residency_degraded = true;
966 }
967 self
968 }
969}
970
971impl EntryEvent {
972 pub fn from_invalidation(event: InvalidationEvent) -> Self {
974 let watermark = event.watermark();
975 Self {
976 ns: event.ns,
977 key: Some(event.key),
978 kind: EntryEventKind::Invalidated,
979 value: event.value,
980 residency_degraded: event.residency_degraded,
981 watermark: Some(watermark),
982 }
983 }
984
985 pub fn from_source(
987 ns: Namespace,
988 key: Option<StructuredKey>,
989 source: EntryEventSource,
990 value: Option<Vec<u8>>,
991 watermark: Option<Watermark>,
992 ) -> Self {
993 Self {
994 ns,
995 key,
996 kind: EntryEventKind::from_source(source),
997 value,
998 residency_degraded: false,
999 watermark,
1000 }
1001 }
1002}
1003
1004#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1006#[serde(rename_all = "snake_case")]
1007pub enum ClientWireMessage {
1008 Handshake(VersionHandshake),
1010 Request(ClientRequestEnvelope),
1012 Response(ClientResponseEnvelope),
1014 Invalidation(InvalidationEvent),
1016 Heartbeat(Watermark),
1018}
1019
1020impl ClientWireMessage {
1021 pub fn protocol_version(&self) -> u16 {
1023 match self {
1024 Self::Handshake(handshake) => handshake.max,
1025 Self::Request(envelope) => envelope.protocol_version,
1026 Self::Response(envelope) => envelope.protocol_version,
1027 Self::Invalidation(_) | Self::Heartbeat(_) => PROTOCOL_VERSION,
1028 }
1029 }
1030}
1031
1032fn redact_message(message: String) -> String {
1033 let mut redacted = message;
1034 for marker in ["value=", "secret=", "token="] {
1035 if let Some(index) = redacted.find(marker) {
1036 redacted.truncate(index + marker.len());
1037 redacted.push_str("<redacted>");
1038 }
1039 }
1040 redacted
1041}
1042
1043#[derive(Debug, Clone, PartialEq, Eq, Error)]
1045pub enum ClientProtocolError {
1046 #[error("client frame is {actual} bytes, exceeding max_frame_bytes={max}")]
1048 FrameTooLarge {
1049 actual: usize,
1051 max: usize,
1053 },
1054 #[error("truncated client frame: {actual} bytes available, {needed} needed")]
1056 TruncatedFrame {
1057 actual: usize,
1059 needed: usize,
1061 },
1062 #[error(
1064 "client frame length mismatch: declared body {declared} bytes, actual body {actual} bytes"
1065 )]
1066 LengthMismatch {
1067 declared: usize,
1069 actual: usize,
1071 },
1072 #[error("unsupported client protocol version {version}; supported max is {supported_max}")]
1074 UnsupportedVersion {
1075 version: u16,
1077 supported_max: u16,
1079 },
1080 #[error("client protocol codec error: {0}")]
1082 Codec(String),
1083 #[error("invalid client protocol field: {0}")]
1085 InvalidField(&'static str),
1086}