Skip to main content

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}