dialcache/remote.rs
1//! The remote (Redis) adapter boundary.
2
3use futures::future::BoxFuture;
4
5use crate::cancel::CancelToken;
6use crate::codec::Payload;
7use crate::error::BoxError;
8
9/// One stored value: the writer's wall-clock stamp and the serialized payload.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Frame {
12 /// Epoch milliseconds from the writer's clock; a nonnegative safe integer.
13 pub created_at_ms: u64,
14 /// The stored payload, envelope included;
15 /// [`decompress_payload`](crate::protocol::decompress_payload) unwraps it.
16 pub payload: Payload,
17}
18
19/// Bounded cause of a semantic miss.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum MissReason {
23 /// The layer had no retrievable value.
24 ValueAbsent,
25 /// A valid frame's logical age reached its ceiling.
26 Expired,
27 /// A tracked frame was rejected by an invalidation watermark.
28 WatermarkFenced,
29 /// A real miss with no decisive cause: malformed frames, unknown replies,
30 /// invalid or future timestamps, decode failures.
31 Unclassified,
32}
33
34impl MissReason {
35 /// The `snake_case` label value shared with the TypeScript and Go ports.
36 pub fn as_str(self) -> &'static str {
37 match self {
38 MissReason::ValueAbsent => "value_absent",
39 MissReason::Expired => "expired",
40 MissReason::WatermarkFenced => "watermark_fenced",
41 MissReason::Unclassified => "unclassified",
42 }
43 }
44
45 /// The inverse of [`as_str`](Self::as_str). `None` for any other text,
46 /// which callers treat as [`MissReason::Unclassified`].
47 pub fn parse(text: &str) -> Option<Self> {
48 Some(match text {
49 "value_absent" => MissReason::ValueAbsent,
50 "expired" => MissReason::Expired,
51 "watermark_fenced" => MissReason::WatermarkFenced,
52 "unclassified" => MissReason::Unclassified,
53 _ => return None,
54 })
55 }
56}
57
58/// Semantic result of one remote read.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum ReadResult {
61 /// A complete frame that passed decoding and, for tracked keys, the fence.
62 Hit(Frame),
63 /// A classified miss. `observed_watermark_ms` is the valid invalidation
64 /// watermark read atomically with a tracked value, when one existed; a
65 /// refill stamped at or before it is known to remain unreadable.
66 Miss {
67 /// Why no servable frame was produced.
68 reason: MissReason,
69 /// The fence described on the variant; always `None` for untracked
70 /// keys once normalized.
71 observed_watermark_ms: Option<u64>,
72 },
73}
74
75impl ReadResult {
76 /// A miss that observed no watermark.
77 pub fn miss(reason: MissReason) -> Self {
78 ReadResult::Miss {
79 reason,
80 observed_watermark_ms: None,
81 }
82 }
83
84 /// `true` for [`ReadResult::Miss`] of any reason.
85 pub fn is_miss(&self) -> bool {
86 matches!(self, ReadResult::Miss { .. })
87 }
88}
89
90/// Keys of one read: the value key and, for tracked identities, the watermark key.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct ReadRequest {
93 /// The stored value key: the logical key plus the frame suffix.
94 pub value_key: String,
95 /// The entity watermark key, present only for tracked identities, whose
96 /// value and watermark must be read atomically.
97 pub watermark_key: Option<String>,
98}
99
100/// The effective read budget and a cooperative cancellation request.
101///
102/// The cache's own deadline remains authoritative; adapters may use the token
103/// to stop waiting, but a dispatched command may still execute.
104#[derive(Debug, Clone)]
105pub struct ReadContext {
106 /// The resolved read budget in milliseconds; the cache stops waiting and
107 /// cancels the token once it elapses.
108 pub timeout_ms: u64,
109 /// Cancelled by the cache when the budget elapses; adapters may stop
110 /// waiting on it.
111 pub cancel: CancelToken,
112}
113
114/// One complete-frame write: a single native `SET value_key frame PX ttl_ms`.
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct WriteRequest {
117 /// The stored value key to `SET`.
118 pub value_key: String,
119 /// The complete frame, stamped by the cache; store it exactly.
120 pub frame: Frame,
121 /// Positive milliseconds no greater than 365 days.
122 pub ttl_ms: u64,
123}
124
125/// One invalidation: advance the entity watermark monotonically.
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct InvalidateRequest {
128 /// The entity watermark key, `{namespace:keyType:id}#watermark`.
129 pub watermark_key: String,
130 /// The invalidating process's wall clock sample; reused through retries.
131 pub invalidated_at_ms: u64,
132 /// Nonnegative milliseconds no greater than 365 days.
133 pub future_buffer_ms: u64,
134}
135
136/// Caller-owned semantic Redis boundary.
137///
138/// The cache borrows the adapter and never connects, drains or closes it.
139/// Tracked reads must observe the value and watermark atomically from an
140/// authoritative primary. Writes are one native `SET` of a complete frame
141/// stamped by the cache; they never create or extend watermarks.
142/// Invalidation runs the shared Lua transition. Use
143/// [`crate::protocol::decode_frame`] and [`crate::protocol::encode_frame`]
144/// for the wire format.
145pub trait Remote: Send + Sync + 'static {
146 /// Read one value, and for tracked requests its watermark atomically from
147 /// a primary, then classify the result. An `Err` counts as a `cache_read`
148 /// error and the call falls through to the source.
149 fn read(
150 &self,
151 request: ReadRequest,
152 context: ReadContext,
153 ) -> BoxFuture<'_, Result<ReadResult, BoxError>>;
154 /// Store the complete frame under `value_key` with `ttl_ms` retention as
155 /// one native `SET ... PX`, without touching watermarks.
156 fn write(&self, request: WriteRequest) -> BoxFuture<'_, Result<(), BoxError>>;
157 /// Advance the entity watermark to at least
158 /// `invalidated_at_ms + future_buffer_ms`, only ever widening its retention.
159 fn invalidate(&self, request: InvalidateRequest) -> BoxFuture<'_, Result<(), BoxError>>;
160}