Skip to main content

ignite_client/
cache.rs

1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3
4use crate::protocol::codec::read_bool;
5use crate::protocol::error::{ProtocolError, value_type_name};
6use crate::protocol::messages::{
7    decode_cache_get_all_response, decode_cache_get_size_response, decode_cache_value_response,
8    encode_cache_create_with_name, encode_cache_destroy_req, encode_cache_get_size,
9    encode_cache_key_req, encode_cache_kv_req, encode_cache_multi_key_req,
10    encode_cache_multi_kv_req,
11};
12use crate::protocol::op_code;
13use crate::protocol::{ExpiryPolicy, IgniteValue};
14use crate::transport::{IgniteConnection, next_request_id};
15use bytes::Bytes;
16use uuid::Uuid;
17
18use crate::affinity::AffinityContext;
19use crate::binary::{ReadBinary, WriteBinary};
20use crate::channel::ChannelRegistry;
21use crate::error::{IgniteError, Result};
22use crate::protocol::binary::client_ops::{
23    BinaryTypeCache, RegistryResolver, fetch_binary_type, metadata_compatible,
24    prefetch_nested_schemas, register_binary_type, schema_field_ids_with_refetch,
25};
26use crate::protocol::binary::header::{BinaryHeader, flags};
27use crate::protocol::binary::reader::BinaryObjectReader;
28
29// ─── CacheSource ─────────────────────────────────────────────────────────────
30
31/// Backing connection source for an [`IgniteCache`] handle.
32///
33/// - `Routed` — non-transactional: each operation routes through the channel
34///   registry, preferring the key's owning node (partition awareness) and
35///   falling back to the default channel.
36/// - `Tx` — transactional: every operation goes through the transaction's
37///   dedicated connection and embeds the `tx_id` in the cache-header flags byte.
38#[derive(Clone)]
39pub(crate) enum CacheSource {
40    Routed {
41        registry: Arc<ChannelRegistry>,
42        affinity: Arc<AffinityContext>,
43    },
44    Tx {
45        tx_id: i32,
46        conn: Arc<IgniteConnection>,
47    },
48}
49
50// ─── IgniteCache ─────────────────────────────────────────────────────────────
51
52/// A handle to an Ignite cache, obtained via [`crate::IgniteClient::cache`],
53/// [`crate::IgniteClient::get_or_create_cache`], or [`crate::transaction::Transaction::cache`].
54///
55/// Cheap to clone — internally holds an `i32` cache-id and either a pool
56/// reference (Arc-backed) or a transaction connection (Arc-backed).
57#[derive(Clone)]
58pub struct IgniteCache {
59    pub(crate) cache_id: i32,
60    source: CacheSource,
61    /// Optional expiry policy applied to every operation on this handle.
62    expiry: Option<ExpiryPolicy>,
63    /// Client-side cache of binary-type metadata, shared with the
64    /// [`crate::IgniteClient`] this handle was obtained from (all clones and
65    /// all cache handles refer to the same logical client). Used by
66    /// [`Self::get_binary`]/[`Self::put_binary`] to decode/register
67    /// compact-footer binary objects without a metadata round-trip on every
68    /// call. Transactional handles ([`CacheSource::Tx`]) get a fresh,
69    /// unshared map since they have no registry to fetch/register against.
70    binary_types: Arc<BinaryTypeCache>,
71}
72
73impl std::fmt::Debug for IgniteCache {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        let mut s = f.debug_struct("IgniteCache");
76        s.field("cache_id", &self.cache_id);
77        match &self.source {
78            CacheSource::Routed { .. } => {
79                s.field("source", &"routed");
80            }
81            CacheSource::Tx { tx_id, .. } => {
82                s.field("tx_id", tx_id);
83            }
84        }
85        s.field("expiry", &self.expiry).finish()
86    }
87}
88
89impl IgniteCache {
90    /// Create a non-transactional cache handle backed by the channel registry,
91    /// routing keys to their owning node via the shared affinity context.
92    pub(crate) fn new(
93        cache_id: i32,
94        registry: Arc<ChannelRegistry>,
95        affinity: Arc<AffinityContext>,
96        binary_types: Arc<BinaryTypeCache>,
97    ) -> Self {
98        Self {
99            cache_id,
100            source: CacheSource::Routed { registry, affinity },
101            expiry: None,
102            binary_types,
103        }
104    }
105
106    /// Create a transactional cache handle that routes all ops through a
107    /// transaction's dedicated connection and embeds `tx_id` in every request.
108    ///
109    /// `binary_types` is a fresh, unshared cache: `Transaction::cache` has no
110    /// access to the owning client's registry, so `get_binary`/`put_binary`
111    /// error out on this handle anyway (see [`Self::registry`]) and never
112    /// populate it.
113    pub(crate) fn new_tx(cache_id: i32, tx_id: i32, conn: Arc<IgniteConnection>) -> Self {
114        Self {
115            cache_id,
116            source: CacheSource::Tx { tx_id, conn },
117            expiry: None,
118            binary_types: Arc::new(Mutex::new(HashMap::new())),
119        }
120    }
121
122    /// Return a new handle to the same cache whose operations apply `policy`
123    /// (per-entry time-to-live on create / update / access).  Cheap to clone.
124    ///
125    /// ```no_run
126    /// # use ignite_client::{IgniteCache, ExpiryPolicy, ExpiryDuration, IgniteValue};
127    /// # async fn f(cache: IgniteCache) -> ignite_client::Result<()> {
128    /// // New entries live 60s; updates and reads leave the TTL unchanged.
129    /// let ttl = ExpiryPolicy::new(
130    ///     ExpiryDuration::Millis(60_000),
131    ///     ExpiryDuration::Unchanged,
132    ///     ExpiryDuration::Unchanged,
133    /// );
134    /// cache.with_expiry_policy(ttl).put(IgniteValue::Int(1), IgniteValue::Int(2)).await?;
135    /// # Ok(()) }
136    /// ```
137    pub fn with_expiry_policy(&self, policy: ExpiryPolicy) -> IgniteCache {
138        IgniteCache {
139            cache_id: self.cache_id,
140            source: self.source.clone(),
141            expiry: Some(policy),
142            binary_types: self.binary_types.clone(),
143        }
144    }
145
146    // ── Internal helpers ──────────────────────────────────────────────────────
147
148    /// Returns the active transaction ID, or `None` for non-transactional handles.
149    fn tx_id(&self) -> Option<i32> {
150        match &self.source {
151            CacheSource::Routed { .. } => None,
152            CacheSource::Tx { tx_id, .. } => Some(*tx_id),
153        }
154    }
155
156    /// The expiry policy applied to this handle's operations, if any.
157    fn expiry(&self) -> Option<&ExpiryPolicy> {
158        self.expiry.as_ref()
159    }
160
161    /// The channel registry backing this handle, or an error for
162    /// transactional handles (which route through a dedicated connection and
163    /// carry no registry). Binary-object metadata lookups (`get_binary`/
164    /// `put_binary`) need a registry to reach `OP_BINARY_TYPE_GET`/`_PUT`.
165    fn registry(&self) -> Result<&ChannelRegistry> {
166        match &self.source {
167            CacheSource::Routed { registry, .. } => Ok(registry),
168            CacheSource::Tx { .. } => Err(IgniteError::UnsupportedOnTransaction(
169                "binary-object metadata operations (get_binary/put_binary)",
170            )),
171        }
172    }
173
174    /// Resolve the node owning `key`, lazily refreshing the affinity mapping
175    /// first.  `primary = false` (read-only ops) may resolve to a same-data
176    /// centre backup owner.  Returns `None` (route to the default channel) for
177    /// transactional handles, when PA is disabled, or for unsupported keys.
178    async fn route(&self, key: &IgniteValue, primary: bool) -> Option<Uuid> {
179        match &self.source {
180            CacheSource::Tx { .. } => None,
181            CacheSource::Routed { registry, affinity } => {
182                registry.ensure_affinity(affinity, self.cache_id).await;
183                affinity.affinity_node(self.cache_id, key, primary)
184            }
185        }
186    }
187
188    /// Send `payload` to `target` (its owning node when known, else the default
189    /// channel), returning the post-header response bytes.
190    /// `IgniteConnection::request` already strips and validates the response
191    /// header, so the returned `Bytes` contains only the operation payload.
192    async fn send(&self, req_id: i64, payload: Bytes, target: Option<Uuid>) -> Result<Bytes> {
193        match &self.source {
194            CacheSource::Routed { registry, .. } => {
195                let conn = registry.get(target).await?;
196                let out = conn
197                    .request(req_id, payload)
198                    .await
199                    .map_err(IgniteError::Transport)?;
200                registry.observe_topology(&conn);
201                Ok(out)
202            }
203            CacheSource::Tx { conn, .. } => conn
204                .request(req_id, payload)
205                .await
206                .map_err(IgniteError::Transport),
207        }
208    }
209
210    // ── Public API ────────────────────────────────────────────────────────────
211
212    /// Retrieve a value by key.  Returns `IgniteValue::Null` if the key is not present.
213    pub async fn get(&self, key: IgniteValue) -> Result<IgniteValue> {
214        let req_id = next_request_id();
215        // Read-only op: a same-DC backup owner is acceptable.
216        let target = self.route(&key, false).await;
217        let payload = encode_cache_key_req(
218            op_code::CACHE_GET,
219            req_id,
220            self.cache_id,
221            &key,
222            self.tx_id(),
223            self.expiry(),
224        );
225        let mut resp = self.send(req_id, payload, target).await?;
226        decode_cache_value_response(&mut resp).map_err(IgniteError::Protocol)
227    }
228
229    /// Store a key-value pair.  Overwrites any existing value.
230    #[must_use = "futures do nothing unless you `.await` them"]
231    pub async fn put(&self, key: IgniteValue, value: IgniteValue) -> Result<()> {
232        let req_id = next_request_id();
233        let target = self.route(&key, true).await;
234        let payload = encode_cache_kv_req(
235            op_code::CACHE_PUT,
236            req_id,
237            self.cache_id,
238            &key,
239            &value,
240            self.tx_id(),
241            self.expiry(),
242        );
243        self.send(req_id, payload, target).await?;
244        Ok(())
245    }
246
247    /// Store a key-value pair **only if the key is not already present**.
248    /// Returns `true` if the value was stored, `false` if the key already existed.
249    pub async fn put_if_absent(&self, key: IgniteValue, value: IgniteValue) -> Result<bool> {
250        let req_id = next_request_id();
251        let target = self.route(&key, true).await;
252        let payload = encode_cache_kv_req(
253            op_code::CACHE_PUT_IF_ABSENT,
254            req_id,
255            self.cache_id,
256            &key,
257            &value,
258            self.tx_id(),
259            self.expiry(),
260        );
261        let mut resp = self.send(req_id, payload, target).await?;
262        read_bool(&mut resp).map_err(IgniteError::Protocol)
263    }
264
265    /// Retrieve values for multiple keys.
266    /// Returns only the pairs for keys that exist in the cache; absent keys are omitted.
267    pub async fn get_all(&self, keys: Vec<IgniteValue>) -> Result<Vec<(IgniteValue, IgniteValue)>> {
268        let req_id = next_request_id();
269        let payload = encode_cache_multi_key_req(
270            op_code::CACHE_GET_ALL,
271            req_id,
272            self.cache_id,
273            &keys,
274            self.tx_id(),
275            self.expiry(),
276        );
277        // Multi-key requests span partitions; route via the default channel.
278        let mut resp = self.send(req_id, payload, None).await?;
279        decode_cache_get_all_response(&mut resp).map_err(IgniteError::Protocol)
280    }
281
282    /// Store multiple key-value pairs.
283    #[must_use = "futures do nothing unless you `.await` them"]
284    pub async fn put_all(&self, entries: Vec<(IgniteValue, IgniteValue)>) -> Result<()> {
285        let req_id = next_request_id();
286        let payload = encode_cache_multi_kv_req(
287            op_code::CACHE_PUT_ALL,
288            req_id,
289            self.cache_id,
290            &entries,
291            self.tx_id(),
292            self.expiry(),
293        );
294        self.send(req_id, payload, None).await?;
295        Ok(())
296    }
297
298    /// Returns `true` if the cache contains the given key.
299    pub async fn contains_key(&self, key: IgniteValue) -> Result<bool> {
300        let req_id = next_request_id();
301        // Read-only op: a same-DC backup owner is acceptable.
302        let target = self.route(&key, false).await;
303        let payload = encode_cache_key_req(
304            op_code::CACHE_CONTAINS_KEY,
305            req_id,
306            self.cache_id,
307            &key,
308            self.tx_id(),
309            self.expiry(),
310        );
311        let mut resp = self.send(req_id, payload, target).await?;
312        read_bool(&mut resp).map_err(IgniteError::Protocol)
313    }
314
315    /// Remove a key.
316    ///
317    /// Implemented via `CACHE_GET_AND_REMOVE` (op 1007) because the standalone
318    /// `CACHE_REMOVE_KEY` (op 1019) does not remove only the specified key across
319    /// all Ignite 2.x server configurations; the returned previous value is
320    /// discarded.
321    #[must_use = "futures do nothing unless you `.await` them"]
322    pub async fn remove(&self, key: IgniteValue) -> Result<()> {
323        self.get_and_remove(key).await?;
324        Ok(())
325    }
326
327    /// Replace the value only if the key is already present.
328    /// Returns `true` if the value was replaced, `false` if the key was absent.
329    pub async fn replace(&self, key: IgniteValue, value: IgniteValue) -> Result<bool> {
330        let req_id = next_request_id();
331        let target = self.route(&key, true).await;
332        let payload = encode_cache_kv_req(
333            op_code::CACHE_REPLACE,
334            req_id,
335            self.cache_id,
336            &key,
337            &value,
338            self.tx_id(),
339            self.expiry(),
340        );
341        let mut resp = self.send(req_id, payload, target).await?;
342        read_bool(&mut resp).map_err(IgniteError::Protocol)
343    }
344
345    /// Atomically store a new value and return the previous value.
346    /// Returns `IgniteValue::Null` if the key was not previously present.
347    pub async fn get_and_put(&self, key: IgniteValue, value: IgniteValue) -> Result<IgniteValue> {
348        let req_id = next_request_id();
349        let target = self.route(&key, true).await;
350        let payload = encode_cache_kv_req(
351            op_code::CACHE_GET_AND_PUT,
352            req_id,
353            self.cache_id,
354            &key,
355            &value,
356            self.tx_id(),
357            self.expiry(),
358        );
359        let mut resp = self.send(req_id, payload, target).await?;
360        decode_cache_value_response(&mut resp).map_err(IgniteError::Protocol)
361    }
362
363    /// Atomically remove a key and return its previous value.
364    /// Returns `IgniteValue::Null` if the key was not present.
365    pub async fn get_and_remove(&self, key: IgniteValue) -> Result<IgniteValue> {
366        let req_id = next_request_id();
367        let target = self.route(&key, true).await;
368        let payload = encode_cache_key_req(
369            op_code::CACHE_GET_AND_REMOVE,
370            req_id,
371            self.cache_id,
372            &key,
373            self.tx_id(),
374            self.expiry(),
375        );
376        let mut resp = self.send(req_id, payload, target).await?;
377        decode_cache_value_response(&mut resp).map_err(IgniteError::Protocol)
378    }
379
380    /// Replace the value only if the key is already present, and return the old value.
381    /// Returns `IgniteValue::Null` if the key was absent (no change is made).
382    pub async fn get_and_replace(
383        &self,
384        key: IgniteValue,
385        value: IgniteValue,
386    ) -> Result<IgniteValue> {
387        let req_id = next_request_id();
388        let target = self.route(&key, true).await;
389        let payload = encode_cache_kv_req(
390            op_code::CACHE_GET_AND_REPLACE,
391            req_id,
392            self.cache_id,
393            &key,
394            &value,
395            self.tx_id(),
396            self.expiry(),
397        );
398        let mut resp = self.send(req_id, payload, target).await?;
399        decode_cache_value_response(&mut resp).map_err(IgniteError::Protocol)
400    }
401
402    /// Remove all specified keys from the cache.
403    ///
404    /// Implemented as individual [`Self::remove`] calls because the server-side
405    /// bulk `CACHE_REMOVE_KEYS` operation does not behave as expected across all
406    /// Ignite 2.x versions with the thin-client wire format.
407    #[must_use = "futures do nothing unless you `.await` them"]
408    pub async fn remove_all(&self, keys: Vec<IgniteValue>) -> Result<()> {
409        for key in keys {
410            self.remove(key).await?;
411        }
412        Ok(())
413    }
414
415    /// Return the number of entries in the cache.
416    pub async fn get_size(&self) -> Result<i64> {
417        let req_id = next_request_id();
418        let payload = encode_cache_get_size(
419            op_code::CACHE_GET_SIZE,
420            req_id,
421            self.cache_id,
422            self.tx_id(),
423            self.expiry(),
424        );
425        let mut resp = self.send(req_id, payload, None).await?;
426        decode_cache_get_size_response(&mut resp).map_err(IgniteError::Protocol)
427    }
428
429    // ── Binary-object KV API ─────────────────────────────────────────────────
430
431    /// Retrieve a value stored as an Ignite binary (complex) object, decoded
432    /// via `V`'s [`ReadBinary`] impl. `key` is looked up as an
433    /// `IgniteValue::String`. Returns `Ok(None)` if the key is absent.
434    ///
435    /// If the stored object uses a compact footer (the Java thin client's
436    /// default), this fetches the object's registered
437    /// [`BinaryType`](crate::binary::BinaryType) metadata
438    /// (cached after the first lookup) to recover the schema's field ids —
439    /// full (non-compact) footers carry field ids inline and need no lookup.
440    ///
441    /// A binary-object cache **value** (as opposed to a nested field of type
442    /// Object) comes back from `CACHE_GET` as `IgniteValue::RawObject`, not
443    /// `IgniteValue::Object`: real Ignite wraps top-level complex-object
444    /// values in the "wrapped binary object" envelope (type code 27 —
445    /// `[i32 len][payload][i32 offset]`), whereas `Object` is decoded
446    /// straight off a raw `COMPLEX_OBJECT` (103) frame as seen when a complex
447    /// object is *nested* inside another one's field data. Both cases carry
448    /// the identical self-describing frame once unwrapped, so both are
449    /// accepted here.
450    ///
451    /// Only supported on non-transactional (`Routed`) handles: transactional
452    /// handles carry no channel registry to fetch metadata with, so this
453    /// returns [`IgniteError::UnsupportedOnTransaction`] on a `Transaction::cache`
454    /// handle.
455    pub async fn get_binary<V: ReadBinary>(&self, key: &str) -> Result<Option<V>> {
456        let registry = self.registry()?;
457        let value = self.get(IgniteValue::String(key.to_string())).await?;
458        let frame = match value {
459            IgniteValue::Null => return Ok(None),
460            IgniteValue::Object(o) => o.bytes,
461            IgniteValue::RawObject(data) => Bytes::from(data),
462            other => {
463                return Err(IgniteError::Protocol(ProtocolError::TypeMismatch {
464                    expected: "Object",
465                    got: value_type_name(&other),
466                }));
467            }
468        };
469        let header = BinaryHeader::read(&mut frame.clone()).map_err(IgniteError::Protocol)?;
470        let reader = if header.flags & flags::COMPACT_FOOTER != 0 {
471            // Resolve the schema's field ids, refetching metadata once if our
472            // cached view doesn't know this schema (a peer may have evolved the
473            // type since we cached it).
474            let resolver = RegistryResolver { registry, cache: &self.binary_types };
475            let field_ids =
476                schema_field_ids_with_refetch(&resolver, header.type_id, header.schema_id).await?;
477            BinaryObjectReader::with_schema(frame, &field_ids)
478        } else {
479            BinaryObjectReader::new(frame)
480        }
481        .map_err(IgniteError::Protocol)?;
482
483        // A nested `Object` field may itself be compact-footer-encoded (real
484        // Ignite peers use compact footers throughout, not just at the top
485        // level), which needs that nested type's own schema to decode —
486        // discoverable only from its own bytes, not ahead of time like the
487        // top-level type above. Walk the object graph now (while `.await` is
488        // still available) and install what's found for the synchronous
489        // `V::read` below. See `crate::binary::with_nested_schemas` for the
490        // full rationale.
491        let mut nested_schemas = HashMap::new();
492        prefetch_nested_schemas(registry, &self.binary_types, &reader, &mut nested_schemas)
493            .await?;
494
495        crate::binary::with_nested_schemas(nested_schemas, || V::read(&reader)).map(Some)
496    }
497
498    /// Store `v` as an Ignite binary (complex) object under `key` (looked up
499    /// as an `IgniteValue::String`), encoded via `V`'s [`WriteBinary`] impl.
500    ///
501    /// Registers `V::binary_type()` with the cluster first so other clients
502    /// (e.g. a Java peer) can decode it. Ignite merges compatible metadata
503    /// idempotently, so re-registering an existing type with a matching
504    /// subset of fields is safe; if the server rejects the PUT (e.g. an
505    /// incompatible field type for an already-registered field), this falls
506    /// back to treating it as non-fatal when metadata already exists for the
507    /// type id (some other writer already registered it) — logging a warning
508    /// when that existing metadata is actually incompatible — and only surfaces
509    /// the error when nothing is registered at all.
510    ///
511    /// Only supported on non-transactional (`Routed`) handles — see
512    /// [`Self::get_binary`].
513    #[must_use = "futures do nothing unless you `.await` them"]
514    pub async fn put_binary<V: WriteBinary>(&self, key: &str, v: &V) -> Result<()> {
515        let registry = self.registry()?;
516        let type_def = V::binary_type();
517        if let Err(e) = register_binary_type(registry, &self.binary_types, &type_def).await {
518            // The server rejected the metadata registration. If the type is
519            // entirely unregistered, propagate — nothing could decode this
520            // object later. Otherwise proceed: a binary object is self-typed on
521            // the wire, so the write itself is valid (verified by the
522            // cross-language gate). But warn when the registered metadata is
523            // actually incompatible with what we're writing, rather than
524            // swallowing it silently, since SQL/queryable typing may disagree.
525            match fetch_binary_type(registry, &self.binary_types, type_def.type_id).await? {
526                None => return Err(e),
527                Some(existing) if !metadata_compatible(&existing, &type_def) => {
528                    tracing::warn!(
529                        type_name = %type_def.type_name,
530                        error = %e,
531                        "binary-type metadata differs from the cluster's; writing anyway",
532                    );
533                }
534                Some(_) => {}
535            }
536        }
537
538        let obj = v.to_binary();
539        self.put(
540            IgniteValue::String(key.to_string()),
541            IgniteValue::Object(obj),
542        )
543        .await
544    }
545}
546
547// ─── Cache management helpers (used by IgniteClient) ─────────────────────────
548
549/// Send `CACHE_GET_OR_CREATE_WITH_NAME` and return an `IgniteCache` handle.
550/// `IgniteConnection::request` strips the response header automatically.
551pub(crate) async fn get_or_create_cache_by_name(
552    name: &str,
553    registry: &Arc<ChannelRegistry>,
554    affinity: &Arc<AffinityContext>,
555    binary_types: &Arc<BinaryTypeCache>,
556) -> Result<IgniteCache> {
557    let req_id = next_request_id();
558    let payload =
559        encode_cache_create_with_name(op_code::CACHE_GET_OR_CREATE_WITH_NAME, req_id, name);
560
561    let conn = registry.get(None).await?;
562    // request() strips the response header; for this op the remaining body is empty.
563    conn.request(req_id, payload)
564        .await
565        .map_err(IgniteError::Transport)?;
566
567    let cid = crate::protocol::cache_id(name);
568    Ok(IgniteCache::new(
569        cid,
570        registry.clone(),
571        affinity.clone(),
572        binary_types.clone(),
573    ))
574}
575
576/// Send `CACHE_DESTROY` for the given cache name.
577/// `IgniteConnection::request` strips the response header automatically.
578pub(crate) async fn destroy_cache_by_name(name: &str, registry: &ChannelRegistry) -> Result<()> {
579    let cid = crate::protocol::cache_id(name);
580    let req_id = next_request_id();
581    let payload = encode_cache_destroy_req(op_code::CACHE_DESTROY, req_id, cid);
582
583    let conn = registry.get(None).await?;
584    // request() strips the response header; for this op the remaining body is empty.
585    conn.request(req_id, payload)
586        .await
587        .map_err(IgniteError::Transport)?;
588    Ok(())
589}