ferrox_core/expert_store.rs
1//! A bounded, lease-protected byte cache for routed-expert weights --
2//! the storage foundation for running MoE checkpoints whose experts do
3//! not all fit in RAM at once (stream cold experts from SSD, keep hot
4//! ones resident under one global byte budget).
5//!
6//! Status: wired into both decode paths as an opt-in
7//! (`Decoder::from_gguf_with_expert_cache` for GGUF,
8//! `load_kimi_checkpoint_with_expert_cache` for Kimi safetensors, both
9//! behind the server's `FERROX_EXPERT_CACHE_BYTES` or
10//! `FERROX_SSD_STREAMING=1` which defaults the cache to 2 GiB), each
11//! proven
12//! bit-identical to its resident path on the committed fixtures at
13//! both generous and smaller-than-one-expert budgets. Without the
14//! opt-in, experts still load resident (mmap) exactly as before.
15//! [`ExpertStore::prefetch`] warms keys for ds4-style SSD streaming
16//! overlap (caller supplies the hotlist). Never yet exercised against
17//! a real large checkpoint. Reference clone: `.scratch/ds4` (gitignored).
18//!
19//! Design:
20//!
21//! - [`ExpertSource`] abstracts where bytes come from (a file with
22//! positional reads, a shard set, an in-memory test source). The
23//! store never caches a partial expert: one `(layer, expert)` key
24//! maps to one complete byte buffer (gate+up+down and any auxiliary
25//! tensors, concatenated by the source in a layout the consumer
26//! defines).
27//! - [`ExpertStore::acquire`] returns an [`ExpertLease`] -- a cheap
28//! `Arc` handle that *pins* the entry: eviction skips any entry with
29//! an outstanding lease, so a slot can never be reused while
30//! CPU (or, later, GPU) work still reads it. This is the
31//! slot-reuse-corruption guard, enforced structurally (an `Arc` with
32//! `strong_count > 1` is simply not freeable), not by convention.
33//! - When an expert doesn't fit even after evicting every unleased
34//! entry (e.g. a cache configured smaller than one decode step's
35//! expert union), `acquire` still succeeds: the bytes are read and
36//! returned as an *uncached* pass-through lease. Capacity pressure
37//! degrades to more I/O, never to a wrong answer or a deadlock.
38//! - Reads happen outside the store lock, so concurrent misses on
39//! different experts overlap their I/O. Two concurrent misses on the
40//! *same* expert may both read it; the first to insert wins and the
41//! loser's buffer becomes that caller's private pass-through copy --
42//! duplicated work under a rare race, never wrong bytes.
43
44use std::collections::HashMap;
45use std::io;
46use std::sync::atomic::{AtomicU64, Ordering};
47use std::sync::{Arc, Mutex};
48
49/// Stable identity of one routed expert within a checkpoint.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
51pub struct ExpertKey {
52 pub layer: u32,
53 pub expert: u32,
54}
55
56/// Where expert bytes come from. Implementations must be cheap to call
57/// concurrently (positional reads, not a shared seek cursor).
58pub trait ExpertSource: Send + Sync {
59 /// The exact byte length of this expert, or `None` if the key does
60 /// not exist. Used for budget accounting *before* reading.
61 fn expert_len(&self, key: ExpertKey) -> Option<usize>;
62
63 /// Reads this expert's complete bytes.
64 fn read_expert(&self, key: ExpertKey) -> io::Result<Vec<u8>>;
65}
66
67/// A pinned handle to one expert's bytes. While any lease for an entry
68/// is alive, the store cannot evict or reuse that entry's memory.
69pub struct ExpertLease {
70 data: Arc<Vec<u8>>,
71}
72
73impl ExpertLease {
74 pub fn bytes(&self) -> &[u8] {
75 &self.data
76 }
77
78 /// The shared buffer behind this lease, for building
79 /// `WeightBytes::Shared` sub-range views (one per matrix packed
80 /// into the expert's combined bytes). Every clone extends the
81 /// entry's pin: the store cannot evict while any of these views is
82 /// alive.
83 pub fn shared_buf(&self) -> Arc<Vec<u8>> {
84 Arc::clone(&self.data)
85 }
86}
87
88/// Monotonic counters, readable at any time without taking the store
89/// lock. `resident_bytes` is a gauge (current cache footprint).
90#[derive(Debug, Clone, Copy, Default)]
91pub struct ExpertStoreStats {
92 pub hits: u64,
93 pub misses: u64,
94 pub evictions: u64,
95 /// Acquires that could not be cached (entry larger than what the
96 /// budget could free) and were served as uncached pass-throughs.
97 pub pass_throughs: u64,
98 pub bytes_read: u64,
99 pub resident_bytes: u64,
100}
101
102struct Entry {
103 data: Arc<Vec<u8>>,
104 /// Monotonic recency stamp; smallest = least recently used. A
105 /// stamp per touch is simpler and cheaper than reshuffling a
106 /// dedicated LRU list under the same lock, at the cost of an O(n)
107 /// scan on eviction -- fine for the hundreds-to-low-thousands of
108 /// entries a real expert cache holds.
109 last_used: u64,
110}
111
112struct Inner {
113 entries: HashMap<ExpertKey, Entry>,
114 resident_bytes: usize,
115 clock: u64,
116}
117
118/// Bytes every live [`ExpertStore`] in this process has committed to
119/// holding expert weights.
120///
121/// This module is the SINGLE holder of the expert byte budget, because
122/// on unified memory two budgets are the same RAM counted twice. That
123/// rule only bites when something *else* also wants to retain weight
124/// bytes, and something else now does: `weight_matrix::repack_cache`
125/// keeps interleaved copies of matrices it has packed. This gauge is
126/// how that cache subtracts rather than restates -- see
127/// [`crate::host_memory::derived_copy_budget`].
128static COMMITTED_EXPERT_BYTES: AtomicU64 = AtomicU64::new(0);
129
130/// Bytes currently committed to expert caches across the process. See
131/// [`COMMITTED_EXPERT_BYTES`].
132pub fn committed_expert_bytes() -> u64 {
133 COMMITTED_EXPERT_BYTES.load(Ordering::Relaxed)
134}
135
136/// The bounded cache. See the module docs for the design contract.
137pub struct ExpertStore<S: ExpertSource> {
138 source: S,
139 budget_bytes: usize,
140 inner: Mutex<Inner>,
141 hits: AtomicU64,
142 misses: AtomicU64,
143 evictions: AtomicU64,
144 pass_throughs: AtomicU64,
145 bytes_read: AtomicU64,
146}
147
148impl<S: ExpertSource> Drop for ExpertStore<S> {
149 fn drop(&mut self) {
150 COMMITTED_EXPERT_BYTES.fetch_sub(self.budget_bytes as u64, Ordering::Relaxed);
151 }
152}
153
154impl<S: ExpertSource> ExpertStore<S> {
155 pub fn new(source: S, budget_bytes: usize) -> Self {
156 COMMITTED_EXPERT_BYTES.fetch_add(budget_bytes as u64, Ordering::Relaxed);
157 ExpertStore {
158 source,
159 budget_bytes,
160 inner: Mutex::new(Inner {
161 entries: HashMap::new(),
162 resident_bytes: 0,
163 clock: 0,
164 }),
165 hits: AtomicU64::new(0),
166 misses: AtomicU64::new(0),
167 evictions: AtomicU64::new(0),
168 pass_throughs: AtomicU64::new(0),
169 bytes_read: AtomicU64::new(0),
170 }
171 }
172
173 pub fn budget_bytes(&self) -> usize {
174 self.budget_bytes
175 }
176
177 pub fn stats(&self) -> ExpertStoreStats {
178 let resident = self
179 .inner
180 .lock()
181 .unwrap_or_else(|p| p.into_inner())
182 .resident_bytes as u64;
183 ExpertStoreStats {
184 hits: self.hits.load(Ordering::Relaxed),
185 misses: self.misses.load(Ordering::Relaxed),
186 evictions: self.evictions.load(Ordering::Relaxed),
187 pass_throughs: self.pass_throughs.load(Ordering::Relaxed),
188 bytes_read: self.bytes_read.load(Ordering::Relaxed),
189 resident_bytes: resident,
190 }
191 }
192
193 /// Best-effort warm of `keys` into the cache (ds4-style SSD
194 /// streaming prefetch): acquires each key and drops the lease so
195 /// the entry stays resident under the budget until eviction.
196 /// Concurrent acquires on the same key may duplicate I/O; never
197 /// returns wrong bytes. Failures on individual keys are skipped.
198 pub fn prefetch(&self, keys: &[ExpertKey]) {
199 for &key in keys {
200 let _ = self.acquire(key);
201 }
202 }
203
204 /// Returns a pinned lease on `key`'s bytes, reading them from the
205 /// source on a miss. Never blocks waiting for other leases to be
206 /// released: if the entry cannot fit in the budget right now, the
207 /// bytes are returned uncached (see module docs).
208 pub fn acquire(&self, key: ExpertKey) -> io::Result<ExpertLease> {
209 // Fast path: cache hit under the lock.
210 {
211 let mut inner = self.inner.lock().unwrap_or_else(|p| p.into_inner());
212 inner.clock += 1;
213 let clock = inner.clock;
214 if let Some(entry) = inner.entries.get_mut(&key) {
215 entry.last_used = clock;
216 self.hits.fetch_add(1, Ordering::Relaxed);
217 return Ok(ExpertLease {
218 data: Arc::clone(&entry.data),
219 });
220 }
221 }
222
223 // Miss: read outside the lock so concurrent misses overlap I/O.
224 self.misses.fetch_add(1, Ordering::Relaxed);
225 let data = self.source.read_expert(key)?;
226 self.bytes_read
227 .fetch_add(data.len() as u64, Ordering::Relaxed);
228 let size = data.len();
229 let data = Arc::new(data);
230
231 let mut inner = self.inner.lock().unwrap_or_else(|p| p.into_inner());
232 // Someone else may have inserted this key while we read -- use
233 // theirs (keeps exactly one cached copy), our read becomes the
234 // rare-race duplicate described in the module docs. Already
235 // counted as a miss above (each acquire increments exactly one
236 // of hits/misses), so no hit is recorded here.
237 if let Some(entry) = inner.entries.get_mut(&key) {
238 return Ok(ExpertLease {
239 data: Arc::clone(&entry.data),
240 });
241 }
242
243 if size > self.budget_bytes {
244 self.pass_throughs.fetch_add(1, Ordering::Relaxed);
245 return Ok(ExpertLease { data });
246 }
247
248 // Evict least-recently-used *unleased* entries until it fits.
249 while inner.resident_bytes + size > self.budget_bytes {
250 let victim = inner
251 .entries
252 .iter()
253 .filter(|(_, e)| Arc::strong_count(&e.data) == 1)
254 .min_by_key(|(_, e)| e.last_used)
255 .map(|(k, _)| *k);
256 match victim {
257 Some(k) => {
258 let e = inner.entries.remove(&k).expect("victim key just found");
259 inner.resident_bytes -= e.data.len();
260 self.evictions.fetch_add(1, Ordering::Relaxed);
261 }
262 None => {
263 // Every resident entry is pinned by a live lease --
264 // serve uncached rather than waiting (a wait here
265 // could deadlock against our own caller's leases).
266 self.pass_throughs.fetch_add(1, Ordering::Relaxed);
267 return Ok(ExpertLease { data });
268 }
269 }
270 }
271
272 inner.clock += 1;
273 let clock = inner.clock;
274 inner.resident_bytes += size;
275 inner.entries.insert(
276 key,
277 Entry {
278 data: Arc::clone(&data),
279 last_used: clock,
280 },
281 );
282 Ok(ExpertLease { data })
283 }
284}
285
286/// A file-backed [`ExpertSource`]: each expert is one contiguous
287/// `(offset, len)` byte range in a single file, read with positional
288/// reads (`pread`-style on unix, so concurrent misses never contend on
289/// a shared seek cursor). This is the portable buffered-I/O default
290/// the storage design starts from; `O_DIRECT`/`F_NOCACHE`/`io_uring`
291/// variants are future work behind the same trait and must produce
292/// identical bytes.
293pub struct FileRangeSource {
294 file: std::fs::File,
295 ranges: HashMap<ExpertKey, (u64, usize)>,
296 /// Non-unix fallback only: serializes seek+read pairs.
297 #[cfg(not(unix))]
298 seek_lock: Mutex<()>,
299}
300
301impl FileRangeSource {
302 pub fn new(file: std::fs::File, ranges: HashMap<ExpertKey, (u64, usize)>) -> Self {
303 FileRangeSource {
304 file,
305 ranges,
306 #[cfg(not(unix))]
307 seek_lock: Mutex::new(()),
308 }
309 }
310}
311
312impl ExpertSource for FileRangeSource {
313 fn expert_len(&self, key: ExpertKey) -> Option<usize> {
314 self.ranges.get(&key).map(|&(_, len)| len)
315 }
316
317 fn read_expert(&self, key: ExpertKey) -> io::Result<Vec<u8>> {
318 let &(offset, len) = self
319 .ranges
320 .get(&key)
321 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("{key:?}")))?;
322 let mut buf = vec![0u8; len];
323 #[cfg(unix)]
324 {
325 use std::os::unix::fs::FileExt;
326 self.file.read_exact_at(&mut buf, offset)?;
327 }
328 #[cfg(not(unix))]
329 {
330 use std::io::{Read, Seek, SeekFrom};
331 let _guard = self.seek_lock.lock().unwrap_or_else(|p| p.into_inner());
332 let mut f = &self.file;
333 f.seek(SeekFrom::Start(offset))?;
334 f.read_exact(&mut buf)?;
335 }
336 Ok(buf)
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 /// Deterministic per-key content so any slot-reuse/wrong-bytes bug
345 /// fails loudly: byte `i` of expert `(l, e)` is a function of all
346 /// three.
347 struct PatternSource {
348 len: usize,
349 n_layers: u32,
350 n_experts: u32,
351 }
352
353 fn expected_bytes(key: ExpertKey, len: usize) -> Vec<u8> {
354 (0..len)
355 .map(|i| (key.layer as usize * 31 + key.expert as usize * 7 + i * 13) as u8)
356 .collect()
357 }
358
359 impl ExpertSource for PatternSource {
360 fn expert_len(&self, key: ExpertKey) -> Option<usize> {
361 (key.layer < self.n_layers && key.expert < self.n_experts).then_some(self.len)
362 }
363 fn read_expert(&self, key: ExpertKey) -> io::Result<Vec<u8>> {
364 if self.expert_len(key).is_none() {
365 return Err(io::Error::new(io::ErrorKind::NotFound, "no such expert"));
366 }
367 Ok(expected_bytes(key, self.len))
368 }
369 }
370
371 fn key(layer: u32, expert: u32) -> ExpertKey {
372 ExpertKey { layer, expert }
373 }
374
375 fn store(len: usize, budget: usize) -> ExpertStore<PatternSource> {
376 ExpertStore::new(
377 PatternSource {
378 len,
379 n_layers: 8,
380 n_experts: 8,
381 },
382 budget,
383 )
384 }
385
386 /// The process-wide gauge tracks every live store's budget and
387 /// releases it on drop.
388 ///
389 /// It exists so `weight_matrix::repack_cache` can SUBTRACT the
390 /// expert budget from the same pool rather than declare a second
391 /// one: on unified memory an expert byte and a repacked byte are
392 /// the same RAM, and this module is the single holder of the expert
393 /// half. A gauge that did not fall on drop would starve the repack
394 /// cache for the rest of the process.
395 ///
396 /// The gauge is process-wide and the harness runs these tests in
397 /// parallel, so this asserts against a SENTINEL budget larger than
398 /// every other budget in this module put together rather than
399 /// against an exact total another test could move under it.
400 ///
401 /// Sabotage: delete the `fetch_add` in `new` or the `Drop` impl and
402 /// this goes red.
403 #[test]
404 fn the_committed_gauge_rises_with_a_store_and_falls_when_it_drops() {
405 const SENTINEL: usize = 1 << 40;
406 {
407 let _held = store(64, SENTINEL);
408 assert!(
409 committed_expert_bytes() >= SENTINEL as u64,
410 "a live store must have committed its budget"
411 );
412 }
413 assert!(
414 committed_expert_bytes() < SENTINEL as u64,
415 "a dropped store must return its budget to the pool, or the \
416 repack cache is starved of it for the rest of the process"
417 );
418 }
419
420 #[test]
421 fn hits_misses_and_lru_eviction_order() {
422 let s = store(100, 250); // fits 2 experts
423 assert_eq!(
424 s.acquire(key(0, 0)).unwrap().bytes(),
425 expected_bytes(key(0, 0), 100)
426 );
427 assert_eq!(
428 s.acquire(key(0, 1)).unwrap().bytes(),
429 expected_bytes(key(0, 1), 100)
430 );
431 // Touch (0,0) so (0,1) is now least recently used.
432 s.acquire(key(0, 0)).unwrap();
433 // Third expert evicts (0,1), not (0,0).
434 s.acquire(key(0, 2)).unwrap();
435 let before = s.stats();
436 s.acquire(key(0, 0)).unwrap(); // must still be a hit
437 let after = s.stats();
438 assert_eq!(after.hits, before.hits + 1);
439 assert_eq!(after.misses, before.misses);
440 assert_eq!(after.evictions, 1);
441 assert!(after.resident_bytes <= 250);
442 // (0,1) was the eviction victim: acquiring it again is a miss.
443 s.acquire(key(0, 1)).unwrap();
444 assert_eq!(s.stats().misses, after.misses + 1);
445 }
446
447 #[test]
448 fn a_live_lease_pins_its_entry_against_eviction() {
449 let s = store(100, 250); // fits 2 experts
450 let pinned = s.acquire(key(1, 0)).unwrap();
451 // Fill and churn the cache well past the budget: the pinned
452 // entry must never be evicted or corrupted while the lease
453 // lives...
454 for e in 1..6 {
455 s.acquire(key(1, e)).unwrap();
456 }
457 assert_eq!(pinned.bytes(), expected_bytes(key(1, 0), 100));
458 // ...and it is still resident (a re-acquire is a hit).
459 let hits_before = s.stats().hits;
460 let again = s.acquire(key(1, 0)).unwrap();
461 assert_eq!(s.stats().hits, hits_before + 1);
462 assert_eq!(again.bytes(), expected_bytes(key(1, 0), 100));
463
464 // Once every lease is dropped, pressure may evict it like any
465 // other entry.
466 drop(pinned);
467 drop(again);
468 for e in 1..6 {
469 s.acquire(key(2, e)).unwrap();
470 }
471 let misses_before = s.stats().misses;
472 s.acquire(key(1, 0)).unwrap();
473 assert_eq!(
474 s.stats().misses,
475 misses_before + 1,
476 "unpinned entry was evictable"
477 );
478 }
479
480 /// The plan's "cache smaller than one token's expert union" case:
481 /// a budget that can't hold even one expert still serves correct
482 /// bytes on every acquire, as uncached pass-throughs.
483 #[test]
484 fn budget_smaller_than_one_expert_degrades_to_pass_through_not_failure() {
485 let s = store(100, 50);
486 for e in 0..4 {
487 let lease = s.acquire(key(0, e)).unwrap();
488 assert_eq!(lease.bytes(), expected_bytes(key(0, e), 100));
489 }
490 let st = s.stats();
491 assert_eq!(st.pass_throughs, 4);
492 assert_eq!(st.resident_bytes, 0);
493 assert_eq!(st.evictions, 0);
494 }
495
496 /// If every resident entry is pinned, a new acquire must not
497 /// deadlock or evict a pinned entry -- it passes through.
498 #[test]
499 fn fully_pinned_cache_serves_new_experts_uncached() {
500 let s = store(100, 200);
501 let _a = s.acquire(key(0, 0)).unwrap();
502 let _b = s.acquire(key(0, 1)).unwrap();
503 let c = s.acquire(key(0, 2)).unwrap();
504 assert_eq!(c.bytes(), expected_bytes(key(0, 2), 100));
505 assert_eq!(s.stats().pass_throughs, 1);
506 assert_eq!(s.stats().evictions, 0);
507 // The two pinned entries are still hits.
508 let hits_before = s.stats().hits;
509 s.acquire(key(0, 0)).unwrap();
510 s.acquire(key(0, 1)).unwrap();
511 assert_eq!(s.stats().hits, hits_before + 2);
512 }
513
514 #[test]
515 fn missing_expert_is_a_clean_error() {
516 let s = store(100, 1000);
517 assert!(s.acquire(key(99, 0)).is_err());
518 }
519
520 /// Concurrent stress under a deliberately tiny budget: many
521 /// threads acquiring random keys while evictions churn constantly.
522 /// Every lease's bytes must match that key's expected pattern --
523 /// the direct test for slot-reuse corruption.
524 #[test]
525 fn concurrent_acquires_under_eviction_pressure_never_yield_wrong_bytes() {
526 let s = Arc::new(store(64, 200)); // fits 3 of 16 hot keys
527 let mut handles = Vec::new();
528 for t in 0..8u32 {
529 let s = Arc::clone(&s);
530 handles.push(std::thread::spawn(move || {
531 let mut state = t.wrapping_mul(2654435761).wrapping_add(12345);
532 for _ in 0..500 {
533 state = state.wrapping_mul(1664525).wrapping_add(1013904223);
534 let k = key((state >> 8) % 4, (state >> 16) % 4);
535 let lease = s.acquire(k).expect("in-range key must read");
536 assert_eq!(
537 lease.bytes(),
538 expected_bytes(k, 64),
539 "wrong bytes for {k:?} -- slot reuse corruption"
540 );
541 }
542 }));
543 }
544 for h in handles {
545 h.join().unwrap();
546 }
547 let st = s.stats();
548 assert_eq!(
549 st.hits + st.misses,
550 8 * 500,
551 "every acquire is a hit or a miss"
552 );
553 assert!(st.resident_bytes <= 200, "budget held under concurrency");
554 }
555
556 /// The bridge to the weight types: a quantized `WeightMatrix` built
557 /// over a lease's shared buffer must (a) compute exactly what the
558 /// same bytes compute as an owned buffer, and (b) keep the cache
559 /// entry pinned for as long as the matrix lives, even after the
560 /// original lease is dropped.
561 #[test]
562 fn weight_matrix_over_a_lease_computes_identically_and_extends_the_pin() {
563 use crate::weight_matrix::{QuantKind, WeightBytes, WeightMatrix};
564
565 // A source whose "expert" payload is real Q8_0 rows.
566 struct QuantSource {
567 rows: Vec<u8>,
568 }
569 impl ExpertSource for QuantSource {
570 fn expert_len(&self, _k: ExpertKey) -> Option<usize> {
571 Some(self.rows.len())
572 }
573 fn read_expert(&self, _k: ExpertKey) -> io::Result<Vec<u8>> {
574 Ok(self.rows.clone())
575 }
576 }
577
578 let cols = 64;
579 let rows = 2;
580 let values: Vec<f32> = (0..rows * cols)
581 .map(|i| ((i as f32) * 0.11).sin())
582 .collect();
583 let mut packed = Vec::new();
584 for r in 0..rows {
585 packed.extend(ferrox_quant::quantize_q8_0(
586 &values[r * cols..(r + 1) * cols],
587 ));
588 }
589 let store = ExpertStore::new(
590 QuantSource {
591 rows: packed.clone(),
592 },
593 packed.len(),
594 );
595
596 let lease = store.acquire(key(0, 0)).unwrap();
597 let matrix = WeightMatrix::Quantized {
598 data: WeightBytes::Shared {
599 buf: lease.shared_buf(),
600 range: 0..packed.len(),
601 },
602 rows,
603 cols,
604 kind: QuantKind::Q8_0,
605 };
606 drop(lease); // the matrix alone must keep the entry pinned
607
608 let owned = WeightMatrix::Quantized {
609 data: WeightBytes::Owned(packed),
610 rows,
611 cols,
612 kind: QuantKind::Q8_0,
613 };
614 let x: Vec<f32> = (0..cols).map(|i| ((i as f32) * 0.07).cos()).collect();
615 assert_eq!(
616 matrix.apply(&x),
617 owned.apply(&x),
618 "lease-backed == owned, bit for bit"
619 );
620
621 // Entry is still resident (pinned by the matrix): re-acquire is
622 // a hit even though the budget is exactly one expert.
623 let hits_before = store.stats().hits;
624 store.acquire(key(0, 0)).unwrap();
625 assert_eq!(store.stats().hits, hits_before + 1);
626 }
627
628 /// End-to-end through a real on-disk file: positional-range reads
629 /// through the bounded store return exactly the bytes written at
630 /// each expert's offset, including under concurrent access.
631 #[test]
632 fn file_range_source_reads_correct_ranges_through_the_store() {
633 let dir = std::env::temp_dir();
634 let path = dir.join(format!(
635 "ferrox_expert_store_test_{}.bin",
636 std::process::id()
637 ));
638 let mut file_bytes = Vec::new();
639 let mut ranges = HashMap::new();
640 for l in 0..3u32 {
641 for e in 0..4u32 {
642 let content = expected_bytes(key(l, e), 96);
643 ranges.insert(key(l, e), (file_bytes.len() as u64, content.len()));
644 file_bytes.extend_from_slice(&content);
645 }
646 }
647 std::fs::write(&path, &file_bytes).unwrap();
648 let source = FileRangeSource::new(std::fs::File::open(&path).unwrap(), ranges);
649 let store = Arc::new(ExpertStore::new(source, 300)); // fits 3 of 12
650
651 let mut handles = Vec::new();
652 for t in 0..4u32 {
653 let store = Arc::clone(&store);
654 handles.push(std::thread::spawn(move || {
655 for i in 0..200u32 {
656 let k = key((t + i) % 3, i % 4);
657 let lease = store.acquire(k).unwrap();
658 assert_eq!(lease.bytes(), expected_bytes(k, 96));
659 }
660 }));
661 }
662 for h in handles {
663 h.join().unwrap();
664 }
665 std::fs::remove_file(&path).ok();
666 }
667}