Skip to main content

kevy_store/
string_set.rs

1//! `Store` SET-family write path: encoding pick (`Int` / `ArcBulk` /
2//! inline `Str`), NX/XX guards, the single-probe `maxmemory == 0`
3//! fast path, and the bio-drop hand-off of displaced values.
4//! Split out of `string.rs` (GET family + INCR) to keep both under the
5//! 500-LOC house cap.
6
7#[cfg(not(feature = "std"))]
8use crate::nostd_prelude::*;
9use crate::value::{BULK_THRESHOLD, SmallBytes, Value};
10use crate::{Entry, Store, deadline_at, now_ns};
11use crate::util::parse_canonical_i64;
12use alloc::sync::Arc;
13use core::time::Duration;
14
15
16/// L2 + L1: pick the optimal encoding for `bytes` at SET time:
17/// 1. Canonical i64 ASCII → `Value::Int(n)` (smallest + INCR fast path)
18/// 2. > [`BULK_THRESHOLD`] bytes → `Value::ArcBulk(Arc<[u8]>)` (lets the
19///    > reactor reply path borrow the bytes for `writev` zero-copy GET)
20/// 3. Else → `Value::Str(SmallBytes::from_slice(bytes))` (inline-cache-
21///    line storage, beats Arc indirection for small values)
22#[inline]
23fn pick_value_for_set(bytes: &[u8]) -> Value {
24    if let Some(n) = parse_canonical_i64(bytes) {
25        return Value::Int(n);
26    }
27    if bytes.len() > BULK_THRESHOLD {
28        return Value::ArcBulk(Arc::new(Box::<[u8]>::from(bytes)));
29    }
30    Value::Str(SmallBytes::from_slice(bytes))
31}
32
33#[inline]
34pub(crate) fn pick_value_for_set_owned(bytes: Vec<u8>) -> Value {
35    if let Some(n) = parse_canonical_i64(&bytes) {
36        return Value::Int(n);
37    }
38    if bytes.len() > BULK_THRESHOLD {
39        // `Arc::new(box)` is zero-copy when `len ==
40        // capacity` (shrink-to-fit no-ops). See `Value::ArcBulk` doc.
41        return Value::ArcBulk(Arc::new(bytes.into_boxed_slice()));
42    }
43    Value::Str(SmallBytes::from_vec(bytes))
44}
45
46impl Store {
47
48    /// `SET` — overwrites any existing value/type. NX/XX guards; clears TTL.
49    /// Takes an owned `Vec` so a >22 B value's allocation is adopted as-is
50    /// (no copy). For callers holding a borrowed slice, prefer
51    /// [`Self::set_slice`] — it skips the `to_vec` entirely for values that
52    /// inline.
53    pub fn set(
54        &mut self,
55        key: &[u8],
56        value: Vec<u8>,
57        expire: Option<Duration>,
58        nx: bool,
59        xx: bool,
60    ) -> bool {
61        self.set_value(key, pick_value_for_set_owned(value), expire, nx, xx)
62    }
63
64    /// [`Self::set`] for a borrowed value. Values ≤ 22 B store inline in the
65    /// entry — zero allocator traffic, where `set(key, value.to_vec(), …)`
66    /// paid a malloc for the `Vec` and a free when the inline copy dropped
67    /// it (the dominant overwrite-SET pattern). Larger values pay the same
68    /// single allocation either way.
69    pub fn set_slice(
70        &mut self,
71        key: &[u8],
72        value: &[u8],
73        expire: Option<Duration>,
74        nx: bool,
75        xx: bool,
76    ) -> bool {
77        self.set_value(key, pick_value_for_set(value), expire, nx, xx)
78    }
79
80    fn set_value(
81        &mut self,
82        key: &[u8],
83        new_value: Value,
84        expire: Option<Duration>,
85        nx: bool,
86        xx: bool,
87    ) -> bool {
88        // Single-probe overwrite-SET fast path for default
89        // `maxmemory == 0` (the bench and production-common case). Goes
90        // through `kevy_map::RawEntryMut` so the
91        // Occupied arm mutates the entry in place and returns owned
92        // (delta, ttl_delta) — no escaping reference, no second probe.
93        // Overwrite path drops from 2 probes (live_entry_mut: get+get_mut)
94        // to 1 probe. New-key + expired-removed paths still pay the
95        // insert_entry probe (same as before).
96        if !self.clock_on() {
97            return self.set_value_no_evict(key, new_value, expire, nx, xx);
98        }
99        self.set_value_evict(key, new_value, expire, nx, xx)
100    }
101
102    /// Eviction path (maxmemory > 0) of [`Self::set_value`]: keeps the
103    /// 2-probe shape so `live_entry_mut`'s touch_on_access bookkeeping runs.
104    fn set_value_evict(
105        &mut self,
106        key: &[u8],
107        new_value: Value,
108        expire: Option<Duration>,
109        nx: bool,
110        xx: bool,
111    ) -> bool {
112        let expire_at = expire.map(|d| deadline_at(now_ns(), d));
113        let key_heap = crate::key_heap_bytes_for(key);
114        #[allow(clippy::single_match_else)]
115        // Phase 1: in-place overwrite or remember we need to insert.
116        // The old value (if any) is taken via `mem::replace` so the bio
117        // hand-off in phase 2 happens AFTER `self.live_entry_mut`'s
118        // borrow on `self.map` is released — without splitting the
119        // borrow we couldn't call `self.maybe_offload_drop`.
120        let (outcome, old_value) = match self.live_entry_mut(key) {
121            Some(e) => {
122                if nx {
123                    return false;
124                }
125                let (delta, ttl_delta, old) =
126                    overwrite_in_place(e, new_value, expire_at, key_heap);
127                (Ok((delta, ttl_delta)), Some(old))
128            }
129            None => {
130                if xx {
131                    return false;
132                }
133                (Err(Entry::new(new_value, expire_at)), None)
134            }
135        };
136        match outcome {
137            Ok((delta, ttl_delta)) => {
138                self.apply_weight_delta(delta);
139                self.adjust_expires(ttl_delta);
140            }
141            Err(entry) => {
142                self.insert_entry(SmallBytes::from_slice(key), entry);
143            }
144        }
145        // Phase 2: hand the old value off if heavy. Done last so the
146        // critical mutation + bookkeeping commit before any (sub-µs in
147        // steady state) channel send. A cold stub's record dies here.
148        if let Some(old) = old_value {
149            self.tier_note_dead(key_heap, &old);
150            self.maybe_offload_drop(old);
151        }
152        true
153    }
154
155    /// Single-probe overwrite-SET via `kevy_map::RawEntryMut` for the
156    /// `maxmemory == 0` fast path. Skips the `live_entry_mut`
157    /// 2-probe shape: Occupied arm mutates in place + returns owned
158    /// (delta, ttl_delta); Expired arm removes via raw-entry handle + falls
159    /// through to insert; Vacant arm goes to insert.
160    fn set_value_no_evict(
161        &mut self,
162        key: &[u8],
163        new_value: Value,
164        expire: Option<Duration>,
165        nx: bool,
166        xx: bool,
167    ) -> bool {
168        let expire_at = expire.map(|d| deadline_at(now_ns(), d));
169        let key_heap = crate::key_heap_bytes_for(key);
170        // Hold new_value behind Option so the multi-arm consumption
171        // (overwrite arm vs insert-after-expired arm) is moved-once.
172        let mut value_slot = Some(new_value);
173        let outcome = self.set_probe_no_evict(key, &mut value_slot, expire_at, key_heap, nx, xx);
174        // Phase 2: bookkeeping + maybe insert. Borrow on self.map is gone.
175        let old_value: Option<Value> = match outcome {
176            SetOutcome::Refused { drop_first } => {
177                if let Some(old) = drop_first {
178                    // No insert; the expired `Value` still needs
179                    // to drop. Ship via the bio path on its way out.
180                    self.tier_note_dead(key_heap, &old);
181                    self.maybe_offload_drop(old);
182                }
183                return false;
184            }
185            SetOutcome::Updated { delta, ttl_delta, old } => {
186                self.apply_weight_delta(delta);
187                self.adjust_expires(ttl_delta);
188                Some(old)
189            }
190            SetOutcome::ExpiredThenInsert { old } => {
191                let entry = Entry::new(value_slot.take().unwrap(), expire_at);
192                self.insert_entry(SmallBytes::from_slice(key), entry);
193                Some(old)
194            }
195            SetOutcome::NeedInsert => {
196                let entry = Entry::new(value_slot.take().unwrap(), expire_at);
197                self.insert_entry(SmallBytes::from_slice(key), entry);
198                None
199            }
200        };
201        // Phase 3: ship the displaced Value if any. Last so the keyspace
202        // commit precedes any (sub-µs steady-state) channel send. A
203        // displaced cold stub credits its vlog record dead first.
204        if let Some(old) = old_value {
205            self.tier_note_dead(key_heap, &old);
206            self.maybe_offload_drop(old);
207        }
208        true
209    }
210
211    /// Phase 1 of [`Self::set_value_no_evict`]: probe + decide. Four
212    /// outcomes — overwrite-finished (delta + ttl_delta + the displaced
213    /// old `Value` to maybe ship to the bio thread), an expired-removed
214    /// `Entry` whose `Value` ditto needs offload, needs-insert with no
215    /// prior value, or refused by an NX/XX guard.
216    #[inline]
217    fn set_probe_no_evict(
218        &mut self,
219        key: &[u8],
220        value_slot: &mut Option<Value>,
221        expire_at: Option<u64>,
222        key_heap: u64,
223        nx: bool,
224        xx: bool,
225    ) -> SetOutcome {
226        use kevy_map::RawEntryMut;
227        let (uc, cn) = (self.cached_clock, self.cached_ns);
228        match self.map.raw_entry_mut(key) {
229            RawEntryMut::Occupied(mut occ) => {
230                // Decide via shared ref first so we don't touch the entry
231                // (avoiding a needless cache-line dirtying) on the
232                // is_expired+remove path.
233                let expired = occ.get().is_expired(uc, cn);
234                if expired {
235                    let old = occ.remove();
236                    // borrow on self.map released by remove(self).
237                    self.note_expired_removed(&old);
238                    if xx {
239                        return SetOutcome::Refused { drop_first: Some(old.value) };
240                    }
241                    SetOutcome::ExpiredThenInsert { old: old.value }
242                } else {
243                    if nx {
244                        return SetOutcome::Refused { drop_first: None };
245                    }
246                    // Take the old Value before overwriting
247                    // so phase 2 can hand it to the bio thread instead
248                    // of dropping inline (a large-value latency-tail
249                    // amplifier).
250                    let (delta, ttl_delta, old) = overwrite_in_place(
251                        occ.get_mut(),
252                        value_slot.take().unwrap(),
253                        expire_at,
254                        key_heap,
255                    );
256                    SetOutcome::Updated { delta, ttl_delta, old }
257                }
258            }
259            RawEntryMut::Vacant(_) => {
260                if xx {
261                    return SetOutcome::Refused { drop_first: None };
262                }
263                SetOutcome::NeedInsert
264            }
265        }
266    }
267
268    /// Bookkeeping for an entry removed because the SET probe found it
269    /// expired: subtract its weight, decrement the TTL gauge, bump the
270    /// expired counter.
271    #[inline]
272    fn note_expired_removed(&mut self, old: &Entry) {
273        self.used_memory = self
274            .used_memory
275            .saturating_sub(old.weight() + crate::value::ENTRY_OVERHEAD);
276        if old.expire_at_ns.is_some() {
277            self.adjust_expires(-1);
278        }
279        self.expired_keys_total = self.expired_keys_total.saturating_add(1);
280    }
281}
282
283
284/// Phase-1 verdict of the SET probe (see [`Store::set_probe_no_evict`]).
285enum SetOutcome {
286    Updated { delta: i64, ttl_delta: i64, old: Value },
287    ExpiredThenInsert { old: Value },
288    NeedInsert,
289    /// An NX/XX guard stopped the write; `drop_first` carries an
290    /// expired-removed `Value` that still needs the bio-drop hand-off.
291    Refused { drop_first: Option<Value> },
292}
293
294/// Overwrite one live entry's value + TTL in place; returns
295/// `(weight_delta, ttl_delta, displaced_old_value)`. Shared by the
296/// eviction-path SET ([`Store::set_value`]) and the `maxmemory == 0`
297/// single-probe SET ([`Store::set_probe_no_evict`]) — the displaced
298/// `Value` is returned so the caller can hand it to the
299/// bio thread AFTER the keyspace borrow is released rather than
300/// dropping inline (the Drop of a `Value::ArcBulk` over the heap-heavy
301/// threshold amplifies the large-value SET latency tail).
302#[inline]
303fn overwrite_in_place(
304    e: &mut Entry,
305    new_value: Value,
306    expire_at: Option<u64>,
307    key_heap: u64,
308) -> (i64, i64, Value) {
309    let had_ttl = e.expire_at_ns.is_some();
310    let old = core::mem::replace(&mut e.value, new_value);
311    e.expire_at_ns = expire_at.and_then(crate::pack_deadline);
312    let new_w = key_heap + e.value.weight();
313    let delta = new_w as i64 - e.weight() as i64;
314    let ttl_delta = i64::from(e.expire_at_ns.is_some()) - i64::from(had_ttl);
315    e.set_weight(new_w);
316    (delta, ttl_delta, old)
317}