Skip to main content

hydracache_client_protocol/
hibernate.rs

1//! Hibernate second-level cache contract types for external JVM providers.
2//!
3//! This module is intentionally protocol-only. The Java `RegionFactory` lives in
4//! a Maven module outside the Cargo workspace; these types define the stable
5//! mapping that provider must use over the W1 client protocol.
6
7use serde::{Deserialize, Serialize};
8
9use crate::{
10    ClientContext, ClientProtocolError, ClientRequest, Namespace, ReadConsistency, StructuredKey,
11    WriteConsistency,
12};
13
14/// Supported Hibernate major line for the first provider contract.
15pub const HIBERNATE_SUPPORTED_MAJOR: u8 = 6;
16
17/// Human-readable supported Hibernate range for docs and bootstrap checks.
18pub const HIBERNATE_SUPPORTED_RANGE: &str = "Hibernate ORM 6.x";
19
20/// Version of the Hibernate-to-HydraCache mapping contract.
21pub const HIBERNATE_CONTRACT_VERSION: u16 = 1;
22
23/// Hibernate L2 access strategy as seen by the provider.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "kebab-case")]
26pub enum L2AccessMode {
27    /// Immutable data: strong reads, cache puts, no write-driven invalidation.
28    ReadOnly,
29    /// Best-effort invalidation on write.
30    NonStrictReadWrite,
31    /// Invalidate on transaction completion, driven by the consumer.
32    ReadWrite,
33    /// Same boundary as `ReadWrite`; HydraCache still does not join the JVM transaction.
34    Transactional,
35}
36
37impl L2AccessMode {
38    /// Return the documented HydraCache consistency mapping for this access mode.
39    pub const fn consistency_mapping(self) -> L2ConsistencyMapping {
40        match self {
41            Self::ReadOnly => L2ConsistencyMapping {
42                label: L2ConsistencyLabel::StrongImmutable,
43                read: ReadConsistency::Strong,
44                write: None,
45                immutable: true,
46                invalidates_on_write: false,
47                invalidates_on_commit: false,
48                joins_jvm_transaction: false,
49            },
50            Self::NonStrictReadWrite => L2ConsistencyMapping {
51                label: L2ConsistencyLabel::BestEffortInvalidate,
52                read: ReadConsistency::Eventual,
53                write: Some(WriteConsistency::Local),
54                immutable: false,
55                invalidates_on_write: true,
56                invalidates_on_commit: false,
57                joins_jvm_transaction: false,
58            },
59            Self::ReadWrite | Self::Transactional => L2ConsistencyMapping {
60                label: L2ConsistencyLabel::InvalidateOnCommit,
61                read: ReadConsistency::Session,
62                write: Some(WriteConsistency::Quorum),
63                immutable: false,
64                invalidates_on_write: false,
65                invalidates_on_commit: true,
66                joins_jvm_transaction: false,
67            },
68        }
69    }
70}
71
72/// Stable labels used in docs and conformance output.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "kebab-case")]
75pub enum L2ConsistencyLabel {
76    /// Read-only immutable region.
77    StrongImmutable,
78    /// Non-strict best-effort invalidation region.
79    BestEffortInvalidate,
80    /// Transaction-boundary invalidation region.
81    InvalidateOnCommit,
82}
83
84impl L2ConsistencyLabel {
85    /// Stable string used by Java conformance reports.
86    pub const fn as_str(self) -> &'static str {
87        match self {
88            Self::StrongImmutable => "strong-immutable",
89            Self::BestEffortInvalidate => "best-effort-invalidate",
90            Self::InvalidateOnCommit => "invalidate-on-commit",
91        }
92    }
93}
94
95/// Full consistency decision for one Hibernate L2 access mode.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
97pub struct L2ConsistencyMapping {
98    /// Stable label.
99    pub label: L2ConsistencyLabel,
100    /// Read consistency sent in the W1 request context.
101    pub read: ReadConsistency,
102    /// Write consistency sent for puts/invalidations when relevant.
103    pub write: Option<WriteConsistency>,
104    /// Region contents are immutable from Hibernate's perspective.
105    pub immutable: bool,
106    /// Provider invalidates when it observes a write.
107    pub invalidates_on_write: bool,
108    /// Provider invalidates only from transaction-completion callbacks.
109    pub invalidates_on_commit: bool,
110    /// Always false: HydraCache never joins the JVM transaction.
111    pub joins_jvm_transaction: bool,
112}
113
114impl L2ConsistencyMapping {
115    /// Build the W1 client context for requests using this mapping.
116    pub fn client_context(self) -> ClientContext {
117        ClientContext {
118            session_token: None,
119            read: Some(self.read),
120            write: self.write,
121            preferred_region: None,
122        }
123    }
124}
125
126/// Hibernate region kind.
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(rename_all = "kebab-case")]
129pub enum HibernateRegionKind {
130    /// Entity region.
131    Entity,
132    /// Collection region.
133    Collection,
134    /// Natural-id region.
135    NaturalId,
136    /// Query result region.
137    Query,
138    /// Update timestamp region required for query cache correctness.
139    Timestamps,
140}
141
142impl HibernateRegionKind {
143    /// Stable segment used in structured keys.
144    pub const fn key_segment(self) -> &'static str {
145        match self {
146            Self::Entity => "entity",
147            Self::Collection => "collection",
148            Self::NaturalId => "natural-id",
149            Self::Query => "query",
150            Self::Timestamps => "timestamps",
151        }
152    }
153
154    /// Query cache behavior for this region kind.
155    pub const fn query_cache_behavior(self) -> QueryCacheBehavior {
156        match self {
157            Self::Query | Self::Timestamps => QueryCacheBehavior::TimestampBulkInvalidation,
158            Self::Entity | Self::Collection | Self::NaturalId => QueryCacheBehavior::NotQueryCache,
159        }
160    }
161}
162
163/// Explicit query-cache support stance.
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
165#[serde(rename_all = "kebab-case")]
166pub enum QueryCacheBehavior {
167    /// Query cache is implemented with query-result namespaces plus timestamp invalidation.
168    TimestampBulkInvalidation,
169    /// This region is not a query-cache region.
170    NotQueryCache,
171}
172
173/// A Hibernate region mapped to a HydraCache namespace.
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175pub struct RegionMapping {
176    /// Hibernate region name as configured by the app.
177    pub region: String,
178    /// HydraCache namespace used on the W1 protocol.
179    pub ns: Namespace,
180    /// Hibernate region kind.
181    pub kind: HibernateRegionKind,
182    /// Access mode and consistency contract.
183    pub mode: L2AccessMode,
184}
185
186impl RegionMapping {
187    /// Build an explicit region mapping.
188    pub fn new(
189        region: impl Into<String>,
190        ns: Namespace,
191        kind: HibernateRegionKind,
192        mode: L2AccessMode,
193    ) -> Result<Self, ClientProtocolError> {
194        let region = region.into();
195        if region.trim().is_empty() {
196            return Err(ClientProtocolError::InvalidField("hibernate_region"));
197        }
198        Ok(Self {
199            region,
200            ns,
201            kind,
202            mode,
203        })
204    }
205
206    /// Build the default namespace mapping for a Hibernate region.
207    pub fn from_region(
208        region: impl Into<String>,
209        kind: HibernateRegionKind,
210        mode: L2AccessMode,
211    ) -> Result<Self, ClientProtocolError> {
212        let region = region.into();
213        let ns = Namespace::new(format!("hibernate:{}", region.trim()))?;
214        Self::new(region, ns, kind, mode)
215    }
216
217    /// Return the consistency mapping for this region.
218    pub const fn consistency_mapping(&self) -> L2ConsistencyMapping {
219        self.mode.consistency_mapping()
220    }
221
222    /// Return a W1 request context for this region.
223    pub fn client_context(&self) -> ClientContext {
224        self.consistency_mapping().client_context()
225    }
226
227    /// Build a namespaced structured key for this Hibernate region.
228    pub fn key<I, S>(&self, segments: I) -> Result<StructuredKey, ClientProtocolError>
229    where
230        I: IntoIterator<Item = S>,
231        S: Into<String>,
232    {
233        let mut key_segments = vec![self.kind.key_segment().to_owned()];
234        key_segments.extend(segments.into_iter().map(Into::into));
235        StructuredKey::new(key_segments)
236    }
237
238    /// Build a W1 get request for this region.
239    pub fn get(&self, key: StructuredKey) -> ClientRequest {
240        ClientRequest::Get {
241            ns: self.ns.clone(),
242            key,
243        }
244    }
245
246    /// Build a W1 put request for this region.
247    pub fn put(&self, key: StructuredKey, value: Vec<u8>, ttl_ms: Option<u64>) -> ClientRequest {
248        ClientRequest::Put {
249            ns: self.ns.clone(),
250            key,
251            value,
252            ttl_ms,
253            dimensions: vec![
254                "hibernate".to_owned(),
255                self.kind.key_segment().to_owned(),
256                self.region.clone(),
257            ],
258        }
259    }
260
261    /// Build a W1 invalidation request for this region.
262    pub fn invalidate(&self, key: StructuredKey) -> ClientRequest {
263        ClientRequest::Invalidate {
264            ns: self.ns.clone(),
265            key,
266        }
267    }
268
269    /// Build a W1 region eviction request for this region's namespace.
270    pub fn evict_region(&self) -> ClientRequest {
271        ClientRequest::EvictRegion {
272            ns: self.ns.clone(),
273        }
274    }
275}
276
277/// Query cache region contract.
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279pub struct QueryCacheMapping {
280    /// Region containing cached query result keys.
281    pub query_region: RegionMapping,
282    /// Region containing Hibernate update timestamps.
283    pub timestamps_region: RegionMapping,
284}
285
286impl QueryCacheMapping {
287    /// Build a query-cache mapping that uses timestamp/bulk invalidation.
288    pub fn new(
289        query_region: RegionMapping,
290        timestamps_region: RegionMapping,
291    ) -> Result<Self, ClientProtocolError> {
292        if query_region.kind != HibernateRegionKind::Query {
293            return Err(ClientProtocolError::InvalidField("query_region_kind"));
294        }
295        if timestamps_region.kind != HibernateRegionKind::Timestamps {
296            return Err(ClientProtocolError::InvalidField("timestamps_region_kind"));
297        }
298        Ok(Self {
299            query_region,
300            timestamps_region,
301        })
302    }
303
304    /// Build the two W1 evictions needed after a bulk update.
305    pub fn bulk_update_evictions(&self) -> [ClientRequest; 2] {
306        [
307            self.query_region.evict_region(),
308            self.timestamps_region.evict_region(),
309        ]
310    }
311}