Skip to main content

submilli_engine/runtime/
session_kv.rs

1//! Session-scoped key-value store: the trait an embedder implements and a
2//! bounded in-memory implementation.
3//!
4//! Keys and payloads are UTF-16 code units end to end. A payload can contain
5//! lone surrogates — a value a Submilli `string` may legally hold — so nothing
6//! here round-trips through a Rust `String`, which would replace them.
7//!
8//! The store outlives the Wasm store it is read from, so it never retains guest
9//! GC references: serialization to code units happens above this layer.
10
11use std::collections::BTreeMap;
12use std::sync::Arc;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::{Mutex, PoisonError};
15
16/// Retained serialized payload per session.
17pub const DEFAULT_MAX_SESSION_BYTES: u64 = 16 * 1024 * 1024;
18/// Serialized payload of a single value.
19pub const DEFAULT_MAX_VALUE_BYTES: u64 = 1024 * 1024;
20pub const DEFAULT_MAX_ENTRIES: u64 = 1024;
21/// UTF-16 code units in a nonempty key.
22pub const DEFAULT_MAX_KEY_UNITS: u64 = 256;
23
24/// Bytes charged per stored UTF-16 code unit, for both keys and payloads.
25const BYTES_PER_UNIT: u64 = 2;
26
27/// Per-key implementation overhead, bounded separately from payload bytes so a
28/// session holding many tiny values cannot outgrow its map accounting.
29const ENTRY_OVERHEAD_BYTES: u64 = 64;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct SessionKvLimits {
33    pub max_session_bytes: u64,
34    pub max_value_bytes: u64,
35    pub max_entries: u64,
36    pub max_key_units: u64,
37}
38
39impl Default for SessionKvLimits {
40    fn default() -> Self {
41        Self {
42            max_session_bytes: DEFAULT_MAX_SESSION_BYTES,
43            max_value_bytes: DEFAULT_MAX_VALUE_BYTES,
44            max_entries: DEFAULT_MAX_ENTRIES,
45            max_key_units: DEFAULT_MAX_KEY_UNITS,
46        }
47    }
48}
49
50/// Which bound a rejected operation ran into. Carries the numbers, never the
51/// value — an error message about a quota must not leak session contents.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum SessionKvLimitKind {
54    KeyUnits {
55        units: u64,
56        limit: u64,
57    },
58    ValueBytes {
59        bytes: u64,
60        limit: u64,
61    },
62    /// This session's own retained bytes.
63    PerSessionBytes {
64        requested: u64,
65        limit: u64,
66    },
67    /// The server-wide budget summed across every live session. Distinct from
68    /// [`Self::PerSessionBytes`] because it wants the opposite response: the
69    /// bytes in the way belong to other sessions, so shrinking this one's data
70    /// need not help.
71    AllSessionsBytes {
72        requested: u64,
73        limit: u64,
74    },
75    EntryCount {
76        limit: u64,
77    },
78}
79
80impl std::fmt::Display for SessionKvLimitKind {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        match self {
83            Self::KeyUnits { units, limit } => write!(
84                f,
85                "key length limit: {units} UTF-16 code units exceeds the {limit} allowed"
86            ),
87            Self::ValueBytes { bytes, limit } => write!(
88                f,
89                "value size limit: {bytes} serialized bytes exceeds the {limit} allowed"
90            ),
91            Self::PerSessionBytes { requested, limit } => write!(
92                f,
93                "session payload limit: {requested} retained bytes exceeds the {limit} this \
94                 session may hold — remove entries this session no longer needs, or store \
95                 less per key"
96            ),
97            Self::AllSessionsBytes { requested, limit } => write!(
98                f,
99                "server session-state budget: {requested} retained bytes exceeds the {limit} \
100                 allowed across all live sessions — this session's own data is not what is \
101                 in the way, so shrinking it need not help; the operator raises the budget \
102                 with `--max-session-state-memory`"
103            ),
104            Self::EntryCount { limit } => {
105                write!(f, "entry count limit: the session already holds {limit}")
106            }
107        }
108    }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub enum SessionKvError {
113    /// A bound was exceeded. `key` is rendered lossily for the message only;
114    /// the value never appears.
115    LimitExceeded {
116        operation: &'static str,
117        key: String,
118        limit: SessionKvLimitKind,
119    },
120    /// An empty key, or one the implementation otherwise refuses.
121    InvalidKey {
122        operation: &'static str,
123        key: String,
124        reason: &'static str,
125    },
126    /// The backing store failed for a reason the caller cannot fix by retrying
127    /// with a smaller value.
128    Backend {
129        operation: &'static str,
130        message: String,
131    },
132}
133
134impl std::fmt::Display for SessionKvError {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        match self {
137            Self::LimitExceeded {
138                operation,
139                key,
140                limit,
141            } => write!(f, "session.{operation}(\"{key}\") exceeded the {limit}"),
142            Self::InvalidKey {
143                operation,
144                key,
145                reason,
146            } => write!(f, "session.{operation}(\"{key}\"): {reason}"),
147            Self::Backend { operation, message } => {
148                write!(f, "session.{operation} failed: {message}")
149            }
150        }
151    }
152}
153
154impl std::error::Error for SessionKvError {}
155
156/// Key plus retained payload size — what `list` may disclose. Never contents.
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct SessionKvEntry {
159    pub key: Vec<u16>,
160    pub size_bytes: u64,
161}
162
163/// One page of a key scan.
164///
165/// `scanned_all` is false when the scan stopped on its bound rather than on
166/// exhausting the keyspace, so a paginating caller can tell "no more matching
167/// entries" from "look again past `last_scanned`".
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct SessionKvPage {
170    pub entries: Vec<SessionKvEntry>,
171    pub last_scanned: Option<Vec<u16>>,
172    pub scanned_all: bool,
173}
174
175/// Session-scoped key-value storage.
176///
177/// Every operation is atomic and commits only after validation and capacity
178/// reservation succeed, so a rejected write leaves the previous entry intact.
179/// Concurrent writes to one key resolve last-committed-write.
180pub trait SessionKvStore: Send + Sync {
181    /// `Ok(None)` is an absent key. A present entry holding a serialized
182    /// `null` returns `Ok(Some(..))` — callers distinguish the two.
183    fn get(&self, key: &[u16]) -> Result<Option<Vec<u16>>, SessionKvError>;
184
185    fn has(&self, key: &[u16]) -> Result<bool, SessionKvError>;
186
187    fn set(&self, key: &[u16], payload: &[u16]) -> Result<(), SessionKvError>;
188
189    /// `Ok(true)` when an entry existed and was removed.
190    fn remove(&self, key: &[u16]) -> Result<bool, SessionKvError>;
191
192    /// Keys in UTF-16 code-unit order, strictly after `after` when given and
193    /// starting with `prefix`. Scans at most `max_scan` keys; matching is exact
194    /// code units, with no normalization or path semantics.
195    fn scan(
196        &self,
197        after: Option<&[u16]>,
198        prefix: &[u16],
199        max_scan: usize,
200    ) -> Result<SessionKvPage, SessionKvError>;
201
202    fn limits(&self) -> SessionKvLimits;
203}
204
205/// Bounded in-memory store.
206///
207/// The optional `shared_bytes` counter is where a server-wide KV budget across
208/// live sessions attaches: the per-session and aggregate reservations are taken
209/// together and both unwound if either fails.
210pub struct InMemorySessionKv {
211    limits: SessionKvLimits,
212    state: Mutex<KvState>,
213    shared_bytes: Option<SharedKvBudget>,
214}
215
216/// An aggregate byte budget several sessions reserve against.
217#[derive(Debug, Clone)]
218pub struct SharedKvBudget {
219    used: Arc<AtomicU64>,
220    cap: u64,
221}
222
223impl SharedKvBudget {
224    pub fn new(cap: u64) -> Self {
225        Self {
226            used: Arc::new(AtomicU64::new(0)),
227            cap,
228        }
229    }
230
231    pub fn used(&self) -> u64 {
232        self.used.load(Ordering::Relaxed)
233    }
234
235    pub fn cap(&self) -> u64 {
236        self.cap
237    }
238
239    /// Compare-and-swap so two sessions reserving at once cannot both observe
240    /// the same headroom.
241    fn reserve(&self, delta: i64) -> Result<(), u64> {
242        let mut current = self.used.load(Ordering::Relaxed);
243        loop {
244            let next = apply_delta(current, delta);
245            if delta > 0 && next > self.cap {
246                return Err(next);
247            }
248            match self.used.compare_exchange_weak(
249                current,
250                next,
251                Ordering::Relaxed,
252                Ordering::Relaxed,
253            ) {
254                Ok(_) => return Ok(()),
255                Err(observed) => current = observed,
256            }
257        }
258    }
259}
260
261fn apply_delta(current: u64, delta: i64) -> u64 {
262    if delta >= 0 {
263        current.saturating_add(delta as u64)
264    } else {
265        current.saturating_sub(delta.unsigned_abs())
266    }
267}
268
269#[derive(Default)]
270struct KvState {
271    entries: BTreeMap<Vec<u16>, Vec<u16>>,
272    used_bytes: u64,
273}
274
275impl Default for InMemorySessionKv {
276    fn default() -> Self {
277        Self::new(SessionKvLimits::default())
278    }
279}
280
281impl InMemorySessionKv {
282    pub fn new(limits: SessionKvLimits) -> Self {
283        Self {
284            limits,
285            state: Mutex::new(KvState::default()),
286            shared_bytes: None,
287        }
288    }
289
290    pub fn with_shared_budget(limits: SessionKvLimits, budget: SharedKvBudget) -> Self {
291        Self {
292            limits,
293            state: Mutex::new(KvState::default()),
294            shared_bytes: Some(budget),
295        }
296    }
297
298    /// Retained bytes: payloads plus keys plus per-entry overhead.
299    pub fn used_bytes(&self) -> u64 {
300        self.locked().map_or(0, |s| s.used_bytes)
301    }
302
303    pub fn len(&self) -> usize {
304        self.locked().map_or(0, |s| s.entries.len())
305    }
306
307    pub fn is_empty(&self) -> bool {
308        self.len() == 0
309    }
310
311    fn locked(&self) -> Result<std::sync::MutexGuard<'_, KvState>, SessionKvError> {
312        self.state.lock().map_err(|e: PoisonError<_>| {
313            let _ = e;
314            SessionKvError::Backend {
315                operation: "state",
316                message: "session store lock poisoned".into(),
317            }
318        })
319    }
320
321    fn check_key(&self, operation: &'static str, key: &[u16]) -> Result<(), SessionKvError> {
322        if key.is_empty() {
323            return Err(SessionKvError::InvalidKey {
324                operation,
325                key: String::new(),
326                reason: "key must not be empty",
327            });
328        }
329        let units = key.len() as u64;
330        if units > self.limits.max_key_units {
331            return Err(SessionKvError::LimitExceeded {
332                operation,
333                key: render_key(key),
334                limit: SessionKvLimitKind::KeyUnits {
335                    units,
336                    limit: self.limits.max_key_units,
337                },
338            });
339        }
340        Ok(())
341    }
342}
343
344fn entry_bytes(key: &[u16], payload: &[u16]) -> u64 {
345    (key.len() as u64 + payload.len() as u64) * BYTES_PER_UNIT + ENTRY_OVERHEAD_BYTES
346}
347
348/// Only ever used to name the key in a diagnostic. Lone surrogates become
349/// replacement characters here, which is acceptable in a message and is why the
350/// stored key itself is kept as code units.
351fn render_key(key: &[u16]) -> String {
352    String::from_utf16_lossy(key)
353}
354
355/// Releasing on drop is what makes the aggregate budget usable for session
356/// lifetime: a session ends by having its store dropped, not by removing every
357/// key first, so without this the reservation of every ended session would be
358/// held forever and the budget would ratchet to its cap.
359impl Drop for InMemorySessionKv {
360    fn drop(&mut self) {
361        let Some(budget) = &self.shared_bytes else {
362            return;
363        };
364        // `get_mut` on our own `&mut self` cannot contend; a poisoned lock still
365        // yields the state, whose byte count is what we owe back.
366        let held = match self.state.get_mut() {
367            Ok(state) => state.used_bytes,
368            Err(poisoned) => poisoned.into_inner().used_bytes,
369        };
370        if held > 0 {
371            let _ = budget.reserve(-(held as i64));
372        }
373    }
374}
375
376impl SessionKvStore for InMemorySessionKv {
377    fn get(&self, key: &[u16]) -> Result<Option<Vec<u16>>, SessionKvError> {
378        self.check_key("get", key)?;
379        Ok(self.locked()?.entries.get(key).cloned())
380    }
381
382    fn has(&self, key: &[u16]) -> Result<bool, SessionKvError> {
383        self.check_key("has", key)?;
384        Ok(self.locked()?.entries.contains_key(key))
385    }
386
387    fn set(&self, key: &[u16], payload: &[u16]) -> Result<(), SessionKvError> {
388        self.check_key("set", key)?;
389
390        let payload_bytes = payload.len() as u64 * BYTES_PER_UNIT;
391        if payload_bytes > self.limits.max_value_bytes {
392            return Err(SessionKvError::LimitExceeded {
393                operation: "set",
394                key: render_key(key),
395                limit: SessionKvLimitKind::ValueBytes {
396                    bytes: payload_bytes,
397                    limit: self.limits.max_value_bytes,
398                },
399            });
400        }
401
402        let mut state = self.locked()?;
403        let previous_bytes = state
404            .entries
405            .get(key)
406            .map(|existing| entry_bytes(key, existing));
407
408        if previous_bytes.is_none() && state.entries.len() as u64 >= self.limits.max_entries {
409            return Err(SessionKvError::LimitExceeded {
410                operation: "set",
411                key: render_key(key),
412                limit: SessionKvLimitKind::EntryCount {
413                    limit: self.limits.max_entries,
414                },
415            });
416        }
417
418        let new_bytes = entry_bytes(key, payload);
419        let released = previous_bytes.unwrap_or(0);
420        let next_used = state.used_bytes.saturating_sub(released) + new_bytes;
421        if next_used > self.limits.max_session_bytes {
422            return Err(SessionKvError::LimitExceeded {
423                operation: "set",
424                key: render_key(key),
425                limit: SessionKvLimitKind::PerSessionBytes {
426                    requested: next_used,
427                    limit: self.limits.max_session_bytes,
428                },
429            });
430        }
431
432        let delta = new_bytes as i64 - released as i64;
433        if let Some(budget) = &self.shared_bytes
434            && let Err(requested) = budget.reserve(delta)
435        {
436            return Err(SessionKvError::LimitExceeded {
437                operation: "set",
438                key: render_key(key),
439                limit: SessionKvLimitKind::AllSessionsBytes {
440                    requested,
441                    limit: budget.cap(),
442                },
443            });
444        }
445
446        state.entries.insert(key.to_vec(), payload.to_vec());
447        state.used_bytes = next_used;
448        Ok(())
449    }
450
451    fn remove(&self, key: &[u16]) -> Result<bool, SessionKvError> {
452        self.check_key("remove", key)?;
453        let mut state = self.locked()?;
454        let Some(previous) = state.entries.remove(key) else {
455            return Ok(false);
456        };
457        let released = entry_bytes(key, &previous);
458        state.used_bytes = state.used_bytes.saturating_sub(released);
459        if let Some(budget) = &self.shared_bytes {
460            let _ = budget.reserve(-(released as i64));
461        }
462        Ok(true)
463    }
464
465    fn scan(
466        &self,
467        after: Option<&[u16]>,
468        prefix: &[u16],
469        max_scan: usize,
470    ) -> Result<SessionKvPage, SessionKvError> {
471        let state = self.locked()?;
472        let lower = match after {
473            Some(key) => std::ops::Bound::Excluded(key.to_vec()),
474            None => std::ops::Bound::Unbounded,
475        };
476
477        let candidates = state
478            .entries
479            .range((lower, std::ops::Bound::Unbounded))
480            .take(max_scan);
481
482        let mut page = SessionKvPage {
483            entries: Vec::new(),
484            last_scanned: None,
485            scanned_all: true,
486        };
487        for (key, payload) in candidates {
488            page.last_scanned = Some(key.clone());
489            if key.starts_with(prefix) {
490                page.entries.push(SessionKvEntry {
491                    key: key.clone(),
492                    size_bytes: payload.len() as u64 * BYTES_PER_UNIT,
493                });
494            }
495        }
496        // `take` ended the walk either because the keyspace ran out or because the
497        // bound did; only the second leaves unvisited keys behind `last_scanned`.
498        if let Some(last) = &page.last_scanned
499            && state
500                .entries
501                .range((
502                    std::ops::Bound::Excluded(last.clone()),
503                    std::ops::Bound::Unbounded,
504                ))
505                .next()
506                .is_some()
507        {
508            page.scanned_all = false;
509        }
510        Ok(page)
511    }
512
513    fn limits(&self) -> SessionKvLimits {
514        self.limits
515    }
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521
522    fn u(s: &str) -> Vec<u16> {
523        s.encode_utf16().collect()
524    }
525
526    fn store() -> InMemorySessionKv {
527        InMemorySessionKv::default()
528    }
529
530    #[test]
531    fn set_then_get_returns_the_value_and_missing_keys_are_none() {
532        let kv = store();
533        kv.set(&u("a"), &u("{\"x\":1}")).expect("set");
534        assert_eq!(kv.get(&u("a")).expect("get"), Some(u("{\"x\":1}")));
535        assert_eq!(kv.get(&u("b")).expect("get"), None);
536    }
537
538    #[test]
539    fn stored_null_is_distinguishable_from_a_missing_key() {
540        let kv = store();
541        kv.set(&u("n"), &u("null")).expect("set");
542        assert_eq!(kv.get(&u("n")).expect("get"), Some(u("null")));
543        assert!(kv.has(&u("n")).expect("has"));
544        assert!(!kv.has(&u("missing")).expect("has"));
545        assert_eq!(kv.get(&u("missing")).expect("get"), None);
546    }
547
548    #[test]
549    fn empty_payload_is_a_present_value() {
550        let kv = store();
551        kv.set(&u("e"), &[]).expect("set");
552        assert!(kv.has(&u("e")).expect("has"));
553        assert_eq!(kv.get(&u("e")).expect("get"), Some(Vec::new()));
554    }
555
556    #[test]
557    fn replacement_charges_only_the_size_delta() {
558        let kv = store();
559        kv.set(&u("k"), &u("aaaa")).expect("set");
560        let after_first = kv.used_bytes();
561        kv.set(&u("k"), &u("aaaaaa")).expect("replace");
562        assert_eq!(kv.len(), 1);
563        assert_eq!(kv.used_bytes(), after_first + 4);
564    }
565
566    #[test]
567    fn remove_reports_existence_and_releases_capacity() {
568        let kv = store();
569        kv.set(&u("k"), &u("value")).expect("set");
570        let held = kv.used_bytes();
571        assert!(held > 0);
572        assert!(kv.remove(&u("k")).expect("remove"));
573        assert_eq!(kv.used_bytes(), 0);
574        assert!(!kv.remove(&u("k")).expect("remove"));
575    }
576
577    #[test]
578    fn oversized_value_leaves_the_previous_entry_intact() {
579        let kv = store();
580        kv.set(&u("k"), &u("kept")).expect("set");
581        let before = kv.used_bytes();
582        let huge = vec![b'x' as u16; (DEFAULT_MAX_VALUE_BYTES as usize / 2) + 1];
583        let err = kv.set(&u("k"), &huge).expect_err("must reject");
584        assert!(matches!(
585            err,
586            SessionKvError::LimitExceeded {
587                operation: "set",
588                limit: SessionKvLimitKind::ValueBytes { .. },
589                ..
590            }
591        ));
592        assert_eq!(kv.get(&u("k")).expect("get"), Some(u("kept")));
593        assert_eq!(kv.used_bytes(), before);
594    }
595
596    #[test]
597    fn entry_count_limit_rejects_without_partial_mutation() {
598        let kv = InMemorySessionKv::new(SessionKvLimits {
599            max_entries: 4,
600            ..SessionKvLimits::default()
601        });
602        for i in 0..4 {
603            kv.set(&u(&format!("k{i}")), &u("v")).expect("set");
604        }
605        let before = kv.used_bytes();
606        let err = kv.set(&u("overflow"), &u("v")).expect_err("must reject");
607        assert!(matches!(
608            err,
609            SessionKvError::LimitExceeded {
610                limit: SessionKvLimitKind::EntryCount { limit: 4 },
611                ..
612            }
613        ));
614        assert_eq!(kv.len(), 4);
615        assert_eq!(kv.used_bytes(), before);
616        assert!(!kv.has(&u("overflow")).expect("has"));
617        // An existing key still writes: the count bound only gates insertion.
618        kv.set(&u("k0"), &u("vv")).expect("replace");
619    }
620
621    #[test]
622    fn session_payload_limit_names_operation_key_and_limit_but_not_the_value() {
623        let kv = InMemorySessionKv::new(SessionKvLimits {
624            max_session_bytes: 512,
625            ..SessionKvLimits::default()
626        });
627        let filler = vec![b'y' as u16; 300];
628        let err = kv.set(&u("bigkey"), &filler).expect_err("must reject");
629        let rendered = err.to_string();
630        assert!(rendered.contains("set"), "{rendered}");
631        assert!(rendered.contains("bigkey"), "{rendered}");
632        assert!(rendered.contains("512"), "{rendered}");
633        assert!(!rendered.contains("yyyy"), "{rendered}");
634        assert_eq!(kv.len(), 0);
635
636        kv.set(&u("held"), &vec![b'z' as u16; 200])
637            .expect("first write fits");
638        let secret = u("SUPER_SECRET_PAYLOAD");
639        let err = kv.set(&u("other"), &secret).expect_err("must reject");
640        assert!(!err.to_string().contains("SUPER_SECRET_PAYLOAD"));
641        assert_eq!(kv.len(), 1);
642    }
643
644    #[test]
645    fn key_at_the_unit_limit_is_accepted_and_one_over_is_rejected() {
646        let kv = store();
647        let at_limit = vec![b'k' as u16; DEFAULT_MAX_KEY_UNITS as usize];
648        kv.set(&at_limit, &u("v")).expect("256 units accepted");
649
650        let over = vec![b'k' as u16; DEFAULT_MAX_KEY_UNITS as usize + 1];
651        let err = kv.set(&over, &u("v")).expect_err("257 units rejected");
652        assert!(matches!(
653            err,
654            SessionKvError::LimitExceeded {
655                limit: SessionKvLimitKind::KeyUnits {
656                    units: 257,
657                    limit: 256
658                },
659                ..
660            }
661        ));
662        assert_eq!(kv.len(), 1);
663    }
664
665    #[test]
666    fn empty_key_is_rejected() {
667        let kv = store();
668        assert!(matches!(
669            kv.set(&[], &u("v")),
670            Err(SessionKvError::InvalidKey { .. })
671        ));
672    }
673
674    #[test]
675    fn lone_surrogates_survive_a_round_trip() {
676        let kv = store();
677        let key = vec![0x0061, 0xD800, 0x0062];
678        let payload = vec![0x0022, 0xDC00, 0xD83D, 0x0022];
679        kv.set(&key, &payload).expect("set");
680        assert_eq!(kv.get(&key).expect("get"), Some(payload.clone()));
681        // A Rust String round trip would have replaced these.
682        assert_ne!(u(&String::from_utf16_lossy(&payload)), payload);
683    }
684
685    #[test]
686    fn scan_walks_keys_in_code_unit_order_and_reports_its_bound() {
687        let kv = store();
688        for key in ["a", "ab", "b", "ba", "c"] {
689            kv.set(&u(key), &u("v")).expect("set");
690        }
691        let all = kv.scan(None, &[], 10).expect("scan");
692        assert!(all.scanned_all);
693        let keys: Vec<Vec<u16>> = all.entries.iter().map(|e| e.key.clone()).collect();
694        assert_eq!(keys, vec![u("a"), u("ab"), u("b"), u("ba"), u("c")]);
695        assert_eq!(all.entries[0].size_bytes, 2);
696
697        let bounded = kv.scan(None, &u("z"), 2).expect("scan");
698        assert!(bounded.entries.is_empty());
699        assert!(!bounded.scanned_all);
700        assert_eq!(bounded.last_scanned, Some(u("ab")));
701
702        let after = kv.scan(Some(&u("ab")), &u("b"), 10).expect("scan");
703        assert!(after.scanned_all);
704        let keys: Vec<Vec<u16>> = after.entries.iter().map(|e| e.key.clone()).collect();
705        assert_eq!(keys, vec![u("b"), u("ba")]);
706    }
707
708    #[test]
709    fn concurrent_writes_to_one_key_resolve_last_committed_write() {
710        let kv = Arc::new(store());
711        let writers: Vec<_> = (0..8)
712            .map(|i| {
713                let kv = Arc::clone(&kv);
714                std::thread::spawn(move || {
715                    for _ in 0..200 {
716                        kv.set(&u("hot"), &u(&format!("writer-{i}"))).expect("set");
717                    }
718                })
719            })
720            .collect();
721        for w in writers {
722            w.join().expect("writer thread");
723        }
724        let final_value = kv.get(&u("hot")).expect("get").expect("present");
725        let rendered = String::from_utf16_lossy(&final_value);
726        assert!(rendered.starts_with("writer-"), "{rendered}");
727        assert_eq!(kv.len(), 1);
728        assert_eq!(kv.used_bytes(), entry_bytes(&u("hot"), &final_value));
729    }
730
731    /// A session ends by having its store dropped, not by removing every key
732    /// first. Without the release on drop the budget would ratchet to its cap
733    /// as sessions came and went, and every later session would be refused.
734    #[test]
735    fn dropping_a_store_returns_its_bytes_to_the_shared_budget() {
736        let budget = SharedKvBudget::new(entry_bytes(&u("k"), &u("aaaa")));
737        {
738            let held =
739                InMemorySessionKv::with_shared_budget(SessionKvLimits::default(), budget.clone());
740            held.set(&u("k"), &u("aaaa")).expect("first session fits");
741            assert_eq!(budget.used(), budget.cap());
742        }
743        assert_eq!(budget.used(), 0, "the ended session released its bytes");
744
745        let next = InMemorySessionKv::with_shared_budget(SessionKvLimits::default(), budget);
746        next.set(&u("k"), &u("aaaa"))
747            .expect("a later session reuses the capacity");
748    }
749
750    #[test]
751    fn shared_budget_bounds_the_sum_across_sessions() {
752        let budget = SharedKvBudget::new(entry_bytes(&u("k"), &u("aaaa")));
753        let a = InMemorySessionKv::with_shared_budget(SessionKvLimits::default(), budget.clone());
754        let b = InMemorySessionKv::with_shared_budget(SessionKvLimits::default(), budget.clone());
755        a.set(&u("k"), &u("aaaa")).expect("first session fits");
756        let err = b.set(&u("k"), &u("aaaa")).expect_err("second exceeds");
757        assert!(matches!(
758            err,
759            SessionKvError::LimitExceeded {
760                limit: SessionKvLimitKind::AllSessionsBytes { .. },
761                ..
762            }
763        ));
764        assert_eq!(b.len(), 0);
765        assert!(a.remove(&u("k")).expect("remove"));
766        assert_eq!(budget.used(), 0);
767        b.set(&u("k"), &u("aaaa")).expect("capacity released");
768    }
769
770    /// The two byte limits refuse for opposite reasons and want opposite
771    /// responses: shrink your own data, versus wait or ask an operator. A
772    /// message that reads the same for both sends a program — or the model
773    /// writing it — to shrink data that was never what filled the budget.
774    #[test]
775    fn the_per_session_and_server_wide_byte_limits_name_different_fixes() {
776        let per_session = InMemorySessionKv::new(SessionKvLimits {
777            max_session_bytes: 128,
778            ..SessionKvLimits::default()
779        });
780        let own = per_session
781            .set(&u("k"), &vec![b'x' as u16; 200])
782            .expect_err("over this session's own limit")
783            .to_string();
784
785        // A budget with no headroom left by another session, so this one is
786        // refused without ever approaching its own per-session limit.
787        let budget = SharedKvBudget::new(entry_bytes(&u("n"), &u("v")));
788        let neighbour =
789            InMemorySessionKv::with_shared_budget(SessionKvLimits::default(), budget.clone());
790        neighbour.set(&u("n"), &u("v")).expect("neighbour fits");
791        let shared = InMemorySessionKv::with_shared_budget(SessionKvLimits::default(), budget);
792        let aggregate = shared
793            .set(&u("k"), &u("v"))
794            .expect_err("over the server-wide budget")
795            .to_string();
796
797        assert_ne!(own, aggregate);
798        assert!(own.contains("this session may hold"), "{own}");
799        assert!(
800            !own.contains("max-session-state-memory"),
801            "the per-session limit is not the operator's flag: {own}"
802        );
803        assert!(
804            aggregate.contains("across all live sessions"),
805            "{aggregate}"
806        );
807        assert!(
808            aggregate.contains("max-session-state-memory"),
809            "the aggregate refusal must name the flag that raises it: {aggregate}"
810        );
811        // Neither may carry a stored value.
812        for rendered in [&own, &aggregate] {
813            assert!(!rendered.contains("xxxx"), "{rendered}");
814        }
815    }
816}