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}