Skip to main content

Value

Enum Value 

Source
pub enum Value {
Show 18 variants Str(SmallBytes), Int(i64), ArcBulk(Arc<Box<[u8]>>), Hash(Arc<KevyMap<SmallBytes, SmallBytes>>), SegHash(Arc<SegMap<SmallBytes>>), List(Arc<VecDeque<Vec<u8>>>), SegList(Arc<SegListData>), Set(Arc<KevySet<SmallBytes>>), SegSet(Arc<SegMap<()>>), ZSet(Arc<ZSetData>), SegZSet(Arc<SegZSetData>), Stream(Arc<StreamData>), SmallSetInline(SmallSetData), SmallHashInline(SmallHashData), PackedRow(PackedRow), SmallListInline(SmallListData), SmallZSetInline(SmallZSetData), Cold(ColdRef),
}
Expand description

A stored value. One variant per Redis type.

The collection variants live behind a shared pointer (Arc) so the enum is only as big as Str (24 B) + tag = 32 B, not the 56 B ZSetData — every Entry (incl. the common string case) is then ~48 B instead of ~80 B, so the bucket array is ~40% denser/smaller (fewer cache misses on a large keyspace, less RSS). The extra pointer-chase lands only on collection ops, not the hot string GET path.

Arc (same 8 B as the previous Box) is what makes O(short-pause) persistence possible: crate::Store::collect_snapshot bumps each collection’s refcount instead of serializing it, and a background thread walks the frozen payloads at leisure. Mutations go through std::sync::Arc::make_mut — a single uniqueness check (the steady state, no snapshot in flight) or a copy-on-write deep clone when a snapshot still holds the payload.

Str holds a SmallBytes (24 B, same size as Vec<u8>) so byte strings up to 22 bytes live inline inside the bucket, killing the second cache miss the value pointer-chase used to cost on large-keyspace GETs. Clone is the snapshot-collect primitive: Str copies its bytes (inline = 24 B memcpy; heap = one allocation), collections bump a refcount. See crate::Store::collect_snapshot.

use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
s.set(b"k", b"v".to_vec(), None, SetCondition::Always);
s.rpush(b"l", &[b"a".as_slice()])?;
let mut types = Vec::new();
s.snapshot_each(|_, v: &Value, _| types.push(v.type_name()));
types.sort();
assert_eq!(types, ["list", "string"]);

Variants§

§

Str(SmallBytes)

A byte string, inline up to 22 bytes — see the type doc above for why that boundary is where it is.

use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
s.set(b"k", b"hello".to_vec(), None, SetCondition::Always);
let mut hit = false;
s.snapshot_each(|_, v, _| hit |= matches!(v, Value::Str(b) if b.as_ref() == b"hello"));
assert!(hit);
§

Int(i64)

Following valkey’s OBJ_ENCODING_INT: when a SET stores a clean canonical i64 ASCII string (parses round-trip), we keep the integer as i64 rather than as 22 B of inline bytes. Wins on INCR (in-place += delta, no parse / no format / no SmallBytes wrap) and on memory (8 B vs 24 B). GET formats it via a per-Store scratch buffer.

use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
s.set(b"n", b"41".to_vec(), None, SetCondition::Always);
assert_eq!(s.incr_by(b"n", 1)?, 42);
let mut hit = false;
s.snapshot_each(|_, v, _| hit |= matches!(v, Value::Int(42)));
assert!(hit);
§

ArcBulk(Arc<Box<[u8]>>)

Values larger than BULK_THRESHOLD bytes get stored behind an Arc<Box<[u8]>> instead of a heap-backed SmallBytes. The Arc lets the io_uring reactor’s reply path borrow the bytes across the SQE→CQE window safely (Arc clone keeps them alive even if the keyspace mutates) — the prerequisite for the writev zero-copy bulk reply path, which skips the per-GET memcpy from value storage into the per-conn output buffer.

Why Arc<Box<[u8]>> and not Arc<[u8]>: Arc<[u8]> is a DST-backed ArcInner<[u8]> = { strong, weak, [u8; N] } whose data slot sits past the refcount words. Arc::from(Vec<u8>) allocates a fresh ArcInner and copy_from_slices the bytes — a hard mandatory 64 KiB memcpy on every big SET. With Arc<Box<[u8]>>, the Box<[u8]> wrapper occupies the Arc’s data slot (16 B), pointing AT an unchanged heap buffer; so Arc::new(vec.into_boxed_slice()) is truly zero-copy (the boxed slice’s allocation stays put — only the 32-byte ArcInner is freshly malloced). Per-GET cost: one extra pointer dereference (&**arc to get &[u8]), measured to be negligible vs the per-SET memcpy savings. The Arc<[u8]> mandatory copy was confirmed with perf-record before switching to Arc<Box<[u8]>>.

Small values stay on Str(SmallBytes) because the inline cache-line storage beats an Arc indirection for the common case.

use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
s.set(b"k", vec![b'x'; 1024], None, SetCondition::Always);
let mut hit = false;
s.snapshot_each(|_, v, _| hit |= matches!(v, Value::ArcBulk(b) if b.len() == 1024));
assert!(hit);
§

Hash(Arc<KevyMap<SmallBytes, SmallBytes>>)

A hash below HS_PROMOTE elements: one map behind one Arc, so a snapshot pins it whole and the first write during that window deep-clones it. Past that size it becomes SegHash.

use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
let items: Vec<Vec<u8>> = (0..10).map(|i| format!("field-{i}").into_bytes()).collect();
let refs: Vec<&[u8]> = items.iter().map(|m| m.as_slice()).collect();
let pairs: Vec<(&[u8], &[u8])> = refs.iter().map(|f| (*f, &b"v"[..])).collect();
s.hset(b"h", &pairs)?;
let mut hit = false;
s.snapshot_each(|_, v, _| hit |= matches!(v, Value::Hash(h) if h.len() == 10));
assert!(hit);
§

SegHash(Arc<SegMap<SmallBytes>>)

A hash past seg_map::HS_PROMOTE fields: an extendible-hash directory of Arc-shared buckets — a COW write under a live snapshot view clones one bucket, not the whole value.

use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
let many: Vec<Vec<u8>> = (0..16 * 1024 + 1).map(|i| format!("m{i}").into_bytes()).collect();
let refs: Vec<&[u8]> = many.iter().map(|m| m.as_slice()).collect();
let pairs: Vec<(&[u8], &[u8])> = refs.iter().map(|m| (*m, *m)).collect();
s.hset(b"h", &pairs)?;
let mut hit = false;
s.snapshot_each(|_, v, _| hit |= matches!(v, Value::SegHash(_)));
assert!(hit);
§

List(Arc<VecDeque<Vec<u8>>>)

A list below SEG_PROMOTE elements: one deque behind one Arc, with the same whole-value copy-on-write. Past that size it becomes SegList.

use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
let items: Vec<Vec<u8>> = (0..10).map(|i| format!("item-{i}").into_bytes()).collect();
let refs: Vec<&[u8]> = items.iter().map(|m| m.as_slice()).collect();
s.rpush(b"l", &refs)?;
let mut hit = false;
s.snapshot_each(|_, v, _| hit |= matches!(v, Value::List(l) if l.len() == 10));
assert!(hit);
§

SegList(Arc<SegListData>)

A list past crate::list_seg::SEG_PROMOTE elements: a deque of Arc-shared segments so a COW write under a live snapshot view clones one segment, not the whole (possibly multi-GB) value. See list_seg.rs for the promotion contract.

use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
let many: Vec<Vec<u8>> = (0..16 * 1024 + 1).map(|i| format!("m{i}").into_bytes()).collect();
let refs: Vec<&[u8]> = many.iter().map(|m| m.as_slice()).collect();
s.rpush(b"l", &refs)?;
let mut hit = false;
s.snapshot_each(|_, v, _| hit |= matches!(v, Value::SegList(_)));
assert!(hit);
§

Set(Arc<KevySet<SmallBytes>>)

A set below HS_PROMOTE elements, on the same terms as Hash.

use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
let items: Vec<Vec<u8>> = (0..10).map(|i| format!("member-{i}").into_bytes()).collect();
let refs: Vec<&[u8]> = items.iter().map(|m| m.as_slice()).collect();
s.sadd(b"s", &refs)?;
let mut hit = false;
s.snapshot_each(|_, v, _| hit |= matches!(v, Value::Set(m) if m.len() == 10));
assert!(hit);
§

SegSet(Arc<SegMap<()>>)

A set past seg_map::HS_PROMOTE members — the set door of the same bucket-sharded COW as Value::SegHash.

use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
let many: Vec<Vec<u8>> = (0..16 * 1024 + 1).map(|i| format!("m{i}").into_bytes()).collect();
let refs: Vec<&[u8]> = many.iter().map(|m| m.as_slice()).collect();
s.sadd(b"s", &refs)?;
let mut hit = false;
s.snapshot_each(|_, v, _| hit |= matches!(v, Value::SegSet(_)));
assert!(hit);
§

ZSet(Arc<ZSetData>)

A sorted set: members with scores, plus the order-statistic tree that makes rank queries a lookup rather than a scan.

use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
s.zadd(b"z", &[(1.0, b"a".as_slice()), (2.0, b"b"), (3.0, b"c"), (4.0, b"d"), (5.0, b"e")])?;
let mut hit = false;
s.snapshot_each(|_, v, _| hit |= matches!(v, Value::ZSet(_)));
assert!(hit);
§

SegZSet(Arc<SegZSetData>)

A zset past zset_seg::Z_PROMOTE members — sharded member map

  • ordered segments; COW writes clone one bucket + one segment.
use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
let many: Vec<Vec<u8>> = (0..16 * 1024 + 1).map(|i| format!("m{i}").into_bytes()).collect();
let refs: Vec<&[u8]> = many.iter().map(|m| m.as_slice()).collect();
let pairs: Vec<(f64, &[u8])> = refs.iter().enumerate().map(|(i, m)| (i as f64, *m)).collect();
s.zadd(b"z", &pairs)?;
let mut hit = false;
s.snapshot_each(|_, v, _| hit |= matches!(v, Value::SegZSet(_)));
assert!(hit);
§

Stream(Arc<StreamData>)

A stream: entries, consumer groups and their pending lists. Never segmented — a stream trims from the front instead of growing without bound.

use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
use kevy_store::{MissingStream, XAddIdSpec};
let fields = vec![(b"temp".to_vec(), b"21".to_vec())];
s.xadd(b"st", XAddIdSpec::AutoAll, fields, MissingStream::Create, 1_000)?;
let mut hit = false;
s.snapshot_each(|_, v, _| hit |= matches!(v, Value::Stream(_)));
assert!(hit);
§

SmallSetInline(SmallSetData)

Valkey-orthodox encoding switch: tiny sets (1-N short members) live inline in 24 bytes instead of behind Arc<SetData> — matches valkey’s OBJ_ENCODING_LISTPACK for sets, which is what redis-benchmark -t sadd default -r 0 (cardinality stays at 1 forever, single 20-byte literal member) measures. On overflow (the inline form has no room for another member) the set is promoted to Value::Set(Arc<SetData>) — the Swiss-table path that wins for larger cardinalities.

use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
s.sadd(b"s", &[b"only".as_slice()])?;
let mut hit = false;
s.snapshot_each(|_, v, _| hit |= matches!(v, Value::SmallSetInline(_)));
assert!(hit);
§

SmallHashInline(SmallHashData)

Tiny hashes (1-2 short field-value pairs) live inline in 24 bytes; promoted to Value::Hash(Arc<HashData>) on overflow. Mirrors valkey’s OBJ_ENCODING_LISTPACK for hashes.

use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
s.hset(b"h", &[(b"f".as_slice(), b"v".as_slice())])?;
let mut hit = false;
s.snapshot_each(|_, v, _| hit |= matches!(v, Value::SmallHashInline(_)));
assert!(hit);
§

PackedRow(PackedRow)

A declared table’s row: the columns in declared order, in one payload buffer, with no field names and no per-row table.

Reachable only for a key under a declared prefix — an undeclared hash keeps Value::Hash.

use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
s.hset(b"user:1", &[(b"id".as_slice(), b"7".as_slice())])?;
s.pack_row(b"user:1", &vec![b"id".to_vec(), b"name".to_vec()].into());
let mut hit = false;
s.snapshot_each(|_, v, _| hit |= matches!(v, Value::PackedRow(r) if r.get_named(b"id") == Some(&b"7"[..])));
assert!(hit);
§

SmallListInline(SmallListData)

Tiny lists inline encoding; promoted to Value::List(Arc<ListData>) on overflow.

use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
s.rpush(b"l", &[b"a".as_slice(), b"b"])?;
let mut hit = false;
s.snapshot_each(|_, v, _| hit |= matches!(v, Value::SmallListInline(_)));
assert!(hit);
§

SmallZSetInline(SmallZSetData)

Tiny sorted sets inline encoding; promoted to Value::ZSet(Arc<ZSetData>) on overflow.

use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
s.zadd(b"z", &[(1.0, b"a".as_slice())])?;
let mut hit = false;
s.snapshot_each(|_, v, _| hit |= matches!(v, Value::SmallZSetInline(_)));
assert!(hit);
§

Cold(ColdRef)

A demoted (tiered-to-disk) value’s in-map stub. The two-stage funnel (tier module) resolves this before any typed match sees it: stage 1 answers existence/TYPE/TTL from the stub with zero IO; stage 2 materializes (serve or promote) only on a type match. Cloning a Cold clones the STUB, not the record — paths that duplicate values (COPY, cross-shard ship) materialize first so two stubs never alias one vlog record.

use kevy_store::{SetCondition, Store, Value};
let mut s = Store::new();
s.enable_tiering(&dir, 1 << 20)?;
s.set(b"k", vec![b'x'; 4096], None, SetCondition::Always);
s.set_tier_budget(1);
s.demote_to_watermark();
let mut cold = false;
s.snapshot_each(|_, v, _| cold |= matches!(v, Value::Cold(_)));
assert!(cold);
// the stub still answers TYPE without touching disk
assert_eq!(s.type_of(b"k"), "string");

Implementations§

Source§

impl Value

Source

pub fn weight(&self) -> u64

Approximate heap bytes the value owns. Excludes the inline Entry / bucket slot — that’s a separate per-entry constant accounted by the store. Walks collections, so prefer the cached Entry::weight for hot-path accounting and only call this when bootstrapping or after a load-from-snapshot.

Source

pub fn is_heap_heavy(&self) -> bool

Whether this value’s Drop is heavy enough to deserve being shipped to the bio thread instead of freed inline. Fast: every variant decides off a sub-field cheap to inspect (no recursive walk), so it’s safe to call on every overwrite-SET on the hot path. The threshold is intentionally conservative — small Arcs and every short string stay on inline-drop where jemalloc small- class is sub-µs and a cross-thread hand-off would lose.

Source§

impl Value

Source

pub fn type_name(&self) -> &'static str

The Redis type name (TYPE command).

Trait Implementations§

Source§

impl Clone for Value

Source§

fn clone(&self) -> Value

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Value

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

Formats the value using the given formatter. Read more

Auto Trait Implementations§

§

impl Freeze for Value

§

impl RefUnwindSafe for Value

§

impl Send for Value

§

impl Sync for Value

§

impl Unpin for Value

§

impl UnsafeUnpin for Value

§

impl UnwindSafe for Value

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.