Skip to main content

dualcache_ff/
cache.rs

1extern crate alloc;
2#[cfg(not(feature = "std"))]
3use alloc::{vec, vec::Vec, boxed::Box};
4
5use crate::cache_padded::CachePadded;
6use crate::daemon::{Command, Daemon};
7use crate::lossy_queue::{LossyQueue, OneshotAck};
8use crate::unsafe_core::{Cache, T1, T2, WorkerSlot};
9use ahash::RandomState;
10use core::hash::{BuildHasher, Hash};
11use crate::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
12use crate::sync::index_types::{AtomicTick, TickType};
13use crate::sync::{Arc, ArcSlice, new_arc_slice};
14use crate::config::Config;
15
16// ── QSBR global epoch ─────────────────────────────────────────────────────
17
18// Global QSBR epoch. Daemon increments this every maintenance cycle.
19// Workers store their local epoch on `get()` entry and reset to 0 on exit,
20// allowing Daemon to safely reclaim stale pointers.
21#[cfg(any(feature = "loom", loom))]
22loom::lazy_static! {
23    pub static ref GLOBAL_EPOCH: loom::sync::atomic::AtomicUsize = loom::sync::atomic::AtomicUsize::new(1);
24}
25
26#[cfg(not(any(feature = "loom", loom)))]
27pub static GLOBAL_EPOCH: AtomicUsize = AtomicUsize::new(1);
28
29/// Per-worker QSBR state — cache-line padded to prevent false sharing
30/// between workers checking in/out simultaneously.
31pub struct WorkerState {
32    pub local_epoch: CachePadded<AtomicUsize>,
33}
34
35impl WorkerState {
36    pub fn new() -> Self {
37        Self {
38            local_epoch: CachePadded::new(AtomicUsize::new(0)),
39        }
40    }
41}
42
43use crate::spawner::{DaemonSpawner, DefaultSpawner};
44use crate::tls::{TlsProvider, DefaultTls};
45
46// ── DualCacheFF ───────────────────────────────────────────────────────────
47
48pub struct DualCacheFF<K, V, S = RandomState, Tls: TlsProvider = DefaultTls> {
49    pub hasher: S,
50    pub t1: Arc<T1<K, V>>,
51    pub t2: Arc<T2<K, V>>,
52    pub cache: Arc<Cache<K, V>>,
53    pub cmd_tx: Arc<LossyQueue<Command<K, V>>>,
54    pub hit_tx: Arc<LossyQueue<[usize; 64]>>,
55    pub epoch: Arc<AtomicU32>,
56    /// QSBR registry: one entry per thread slot.
57    pub worker_states: ArcSlice<WorkerState>,
58    /// Per-worker zero-lock batch buffers, indexed by WORKER_ID.
59    pub miss_buffers: ArcSlice<WorkerSlot<K, V>>,
60    /// Daemon tick counter — shared with the Daemon thread.
61    /// Workers read this (Relaxed) to implement time-based TLS flush.
62    pub daemon_tick: Arc<AtomicTick>,
63    /// Number of daemon_tick advances that correspond to ≈1 ms of real time.
64    pub flush_tick_threshold: TickType,
65    /// Cold-start flag: Daemon sets this to false when capacity is reached.
66    pub is_cold_start: Arc<AtomicBool>,
67    /// The thread-local storage provider, injected at compile-time for zero overhead.
68    pub tls: Tls,
69}
70
71impl<K, V, S: Clone, Tls: TlsProvider + Clone> Clone for DualCacheFF<K, V, S, Tls> {
72    fn clone(&self) -> Self {
73        Self {
74            hasher: self.hasher.clone(),
75            t1: self.t1.clone(),
76            t2: self.t2.clone(),
77            cache: self.cache.clone(),
78            cmd_tx: self.cmd_tx.clone(),
79            hit_tx: self.hit_tx.clone(),
80            epoch: self.epoch.clone(),
81            worker_states: self.worker_states.clone(),
82            miss_buffers: self.miss_buffers.clone(),
83            daemon_tick: self.daemon_tick.clone(),
84            flush_tick_threshold: self.flush_tick_threshold,
85            is_cold_start: self.is_cold_start.clone(),
86            tls: self.tls.clone(),
87        }
88    }
89}
90
91// ── Constructor (std mode — auto-spawns Daemon thread) ────────────────────
92
93#[cfg(any(feature = "std", feature = "loom", loom))]
94impl<K, V> DualCacheFF<K, V, RandomState, DefaultTls>
95where
96    K: Hash + Eq + Send + Sync + Clone + 'static,
97    V: Send + Sync + Clone + 'static,
98{
99    /// Create a new `DualCacheFF` and automatically spawn the background Daemon.
100    ///
101    /// Use this in `std` environments (servers, desktops).
102    #[inline]
103    pub fn new(config: Config) -> Self {
104        Self::new_with_spawner(config, DefaultSpawner)
105    }
106}
107
108#[cfg(any(feature = "std", feature = "loom", loom))]
109impl<K, V, Tls> DualCacheFF<K, V, RandomState, Tls>
110where
111    K: Hash + Eq + Send + Sync + Clone + 'static,
112    V: Send + Sync + Clone + 'static,
113    Tls: TlsProvider + 'static,
114{
115    /// Create a new `DualCacheFF` and automatically spawn the background Daemon using a custom thread-local storage provider.
116    #[inline]
117    pub fn new_with_tls(config: Config, tls: Tls) -> Self {
118        Self::new_with_tls_and_spawner(config, tls, DefaultSpawner)
119    }
120}
121
122// ── Constructor (universal — returns Daemon for manual scheduling) ─────────
123
124impl<K, V> DualCacheFF<K, V, RandomState, DefaultTls>
125where
126    K: Hash + Eq + Send + Sync + Clone + 'static,
127    V: Send + Sync + Clone + 'static,
128{
129    /// Create a new `DualCacheFF` and automatically spawn the background Daemon using a custom spawner.
130    pub fn new_with_spawner<Sp: DaemonSpawner + 'static>(config: Config, spawner: Sp) -> Self {
131        let (cache, daemon) = Self::new_headless(config);
132        spawner.spawn(alloc::boxed::Box::new(move || daemon.run()));
133        cache
134    }
135
136    /// Create a `DualCacheFF` and its `Daemon` without spawning any thread.
137    ///
138    /// # std mode
139    /// Prefer `DualCacheFF::new()` which spawns the daemon automatically.
140    ///
141    /// # no_std / RTOS mode
142    /// Use `new_headless()` to obtain both the cache handle and the daemon.
143    /// Schedule `daemon.run()` on a dedicated RTOS task:
144    /// ```ignore
145    /// let (cache, daemon) = DualCacheFF::new_headless(config);
146    /// rtos::spawn_task(|| daemon.run()); // RTOS-specific API
147    /// ```
148    pub fn new_headless(config: Config) -> (Self, Daemon<K, V, RandomState>) {
149        Self::new_headless_with_tls(config, DefaultTls)
150    }
151}
152
153impl<K, V, Tls> DualCacheFF<K, V, RandomState, Tls>
154where
155    K: Hash + Eq + Send + Sync + Clone + 'static,
156    V: Send + Sync + Clone + 'static,
157    Tls: TlsProvider + 'static,
158{
159    /// Create a `DualCacheFF` and its `Daemon` with a custom thread-local storage provider.
160    pub fn new_headless_with_tls(
161        config: Config,
162        tls: Tls,
163    ) -> (Self, Daemon<K, V, RandomState>) {
164        let hasher = RandomState::new();
165        let t1 = Arc::new(T1::new(config.t1_slots));
166        let t2 = Arc::new(T2::new(config.t2_slots));
167        let cache = Arc::new(Cache::new(config.capacity));
168        let cmd_q: Arc<LossyQueue<Command<K, V>>> = Arc::new(LossyQueue::new(8192));
169        let hit_q: Arc<LossyQueue<[usize; 64]>> = Arc::new(LossyQueue::new(1024));
170        let epoch = Arc::new(AtomicU32::new(0));
171        let daemon_tick = Arc::new(AtomicTick::new(0));
172        let is_cold_start = Arc::new(AtomicBool::new(true));
173
174        let mut buffers = Vec::with_capacity(config.threads);
175        let mut states = Vec::with_capacity(config.threads);
176        for _ in 0..config.threads {
177            buffers.push(WorkerSlot::new());
178            states.push(WorkerState::new());
179        }
180        let miss_buffers = new_arc_slice(buffers);
181        let worker_states = new_arc_slice(states);
182
183        let daemon = Daemon::new(
184            hasher.clone(),
185            config.capacity,
186            t1.clone(),
187            t2.clone(),
188            cache.clone(),
189            cmd_q.clone(),
190            hit_q.clone(),
191            epoch.clone(),
192            config.duration,
193            config.poll_us,
194            worker_states.clone(),
195            daemon_tick.clone(),
196            is_cold_start.clone(),
197        );
198
199        let this = Self {
200            hasher,
201            t1,
202            t2,
203            cache,
204            cmd_tx: cmd_q,
205            hit_tx: hit_q,
206            epoch,
207            worker_states,
208            miss_buffers,
209            daemon_tick,
210            flush_tick_threshold: (config.poll_us as TickType).max(1),
211            is_cold_start,
212            tls,
213        };
214        
215        (this, daemon)
216    }
217
218    /// Create a `DualCacheFF` and spawn its `Daemon` using both a custom TLS provider and custom spawner.
219    pub fn new_with_tls_and_spawner<Sp: DaemonSpawner + 'static>(config: Config, tls: Tls, spawner: Sp) -> Self
220    {
221        let (cache, daemon) = Self::new_headless_with_tls(config, tls);
222        spawner.spawn(alloc::boxed::Box::new(move || daemon.run()));
223        cache
224    }
225
226}
227
228// ── Public API (std + no_std) ─────────────────────────────────────────────
229
230impl<K, V, S, Tls: TlsProvider> DualCacheFF<K, V, S, Tls>
231where
232    K: Hash + Eq + Send + Sync + Clone + 'static,
233    V: Send + Sync + Clone + 'static,
234    S: BuildHasher + Clone + Send + 'static,
235{
236    /// Flush all pending TLS buffers and wait for the Daemon to process them.
237    ///
238    /// Blocks via `OneshotAck::wait()` (spin-wait, safe in both std and no_std).
239    pub fn sync(&self) {
240        // ── flush TLS hit buffer ─────────────────────────────────────
241        self.with_hit_buf(|state| {
242            if state.1 > 0_usize {
243                let _ = self.hit_tx.try_send(state.0);
244                state.1 = 0;
245            }
246        });
247
248        // ── flush all worker slots ───────────────────────────────────
249        for slot in self.miss_buffers.iter() {
250            let buf: &mut crate::unsafe_core::BatchBuf<K, V> = slot.get_mut_safe();
251            if buf.len() > 0 {
252                let batch = buf.drain_to_vec();
253                let _ = self.cmd_tx.try_send(Command::BatchInsert(batch));
254            }
255        }
256
257        // Send a Sync command and spin-wait for acknowledgment
258        let ack = OneshotAck::new();
259        self.cmd_tx.send_blocking(Command::Sync(ack.clone()));
260        ack.wait();
261    }
262
263    /// Look up a key.
264    ///
265    /// Hot-path order: T1 (L1 direct-map) → T2 (L2 direct-map) → Cache (L3).
266    /// Records a hit signal into the TLS buffer for Daemon processing.
267    pub fn get(&self, key: &K) -> Option<V> {
268        let hash = self.hash(key);
269        let current_epoch_cache = self.epoch.load(Ordering::Relaxed);
270
271        // ── QSBR Check-in ───────────────────────
272        let mut id_opt = None;
273        let global_epoch = GLOBAL_EPOCH.load(Ordering::Relaxed);
274        self.with_worker_id(|id| {
275            if id < self.worker_states.len() {
276                self.worker_states[id]
277                    .local_epoch
278                    .store(global_epoch, Ordering::Relaxed);
279                id_opt = Some(id);
280            }
281        });
282
283        let has_epoch = id_opt.is_some() || {
284            #[cfg(not(feature = "std"))]
285            { true }
286            #[cfg(feature = "std")]
287            { false }
288        };
289
290        let mut res: Option<V> = None;
291        let mut hit_g_idx: Option<u32> = None;
292
293        if has_epoch {
294            // ── T1 check ──────────────────────────────────────────────────────
295            if let Some(node) = self.t1.get_node(hash) {
296                if node.key == *key
297                    && (node.expire_at == 0 || node.expire_at >= current_epoch_cache)
298                {
299                    res = Some(node.value.clone());
300                    hit_g_idx = Some(node.g_idx);
301                    self.tls.with_warmup_state(&mut |s| *s = s.saturating_add(10));
302                }
303            }
304
305            // ── T2 check ──────────────────────────────────────────────────────
306            if res.is_none() {
307                if let Some(node) = self.t2.get_node(hash) {
308                    if node.key == *key
309                        && (node.expire_at == 0 || node.expire_at >= current_epoch_cache)
310                    {
311                        res = Some(node.value.clone());
312                        hit_g_idx = Some(node.g_idx);
313                        self.tls.with_warmup_state(&mut |s| *s = s.saturating_sub(5));
314                    }
315                }
316            }
317
318            // ── Cache (L3) check ──────────────────────────────────────────────
319            if res.is_none() {
320                let tag = (hash >> 48) as u16;
321                if let Some(global_idx) = self.cache.index_probe(hash, tag) {
322                    if let Some(v) = self
323                        .cache
324                        .node_get_full(global_idx, key, current_epoch_cache)
325                    {
326                        res = Some(v);
327                        hit_g_idx = Some(global_idx as u32);
328                        self.tls.with_warmup_state(&mut |s| *s = s.saturating_sub(10));
329                    }
330                }
331            }
332        }
333
334        // ── QSBR Check-out ─────────────────────────────────────
335        if let Some(id) = id_opt {
336            self.worker_states[id]
337                .local_epoch
338                .store(0, Ordering::Relaxed);
339        }
340
341        if let Some(g_idx) = hit_g_idx {
342            self.record_hit(g_idx as usize);
343        }
344
345        res
346    }
347
348    /// Insert a key-value pair.
349    ///
350    /// # L1 Probation Filter (std only)
351    /// Items that appear only once in a TLS epoch are silently dropped.
352    /// This prevents cache pollution from scan traffic.
353    /// In no_std mode the filter is skipped and all items are forwarded.
354    ///
355    /// # Task 6 — Time-based TLS Flush (std only)
356    /// The TLS batch buffer normally flushes when it reaches 32 items.
357    /// Additionally, if the Daemon tick counter has advanced by at least
358    /// `flush_tick_threshold` since the last flush, the buffer is force-drained
359    /// even if nearly empty. This prevents hot items from being invisible to
360    /// the Daemon for too long (the "split-brain eviction" bug).
361    pub fn insert(&self, key: K, value: V) {
362        let hash = self.hash(&key);
363
364        let mut id_opt = None;
365        let is_cold = self.is_cold_start.load(Ordering::Relaxed);
366        let mut bypass = is_cold;
367
368        if !bypass {
369            // Perform thread-safe fast lookup to see if key exists
370            // ── QSBR Check-in ───────────────────────
371            let global_epoch = GLOBAL_EPOCH.load(Ordering::Relaxed);
372            self.with_worker_id(|id| {
373                if id < self.worker_states.len() {
374                    self.worker_states[id]
375                        .local_epoch
376                        .store(global_epoch, Ordering::Relaxed);
377                    id_opt = Some(id);
378                }
379            });
380
381            if id_opt.is_some() {
382                // T1 check
383                if let Some(node) = self.t1.get_node(hash) {
384                    if node.key == key {
385                        bypass = true;
386                    }
387                }
388
389                // T2 check
390                if !bypass {
391                    if let Some(node) = self.t2.get_node(hash) {
392                        if node.key == key {
393                            bypass = true;
394                        }
395                    }
396                }
397
398                // Cache (L3) check
399                if !bypass {
400                    let tag = (hash >> 48) as u16;
401                    if let Some(global_idx) = self.cache.index_probe(hash, tag) {
402                        if let Some(node) = self.cache.get_node(global_idx) {
403                            if node.key == key {
404                                bypass = true;
405                            }
406                        }
407                    }
408                }
409            }
410
411            // ── QSBR Check-out ─────────────────────────────────────
412            if let Some(id) = id_opt {
413                self.worker_states[id]
414                    .local_epoch
415                    .store(0, Ordering::Relaxed);
416            }
417        }
418
419        let pass = if bypass {
420            true
421        } else {
422            // L1 Probation Filter
423            self.with_l1_filter(|state| {
424                let idx = (hash as usize) & 4095_usize;
425                let val = state.0[idx];
426
427                state.1 += 1;
428                if state.1 >= 4096_usize {
429                    for x in state.0.iter_mut() {
430                        *x >>= 1;
431                    }
432                    state.1 = 0;
433                }
434
435                if val < 1_u8 {
436                    state.0[idx] = 1;
437                    false
438                } else {
439                    if val < 2_u8 {
440                        state.0[idx] = 2;
441                    }
442                    true
443                }
444            }).unwrap_or(true) // no TLS -> pass-through
445        };
446
447        if !pass {
448            return;
449        }
450
451        let mut warmup_state = 255;
452        self.tls.with_warmup_state(&mut |s| warmup_state = *s);
453        let is_t1 = warmup_state < 100;
454
455        // Task 6: Time-based flush detection
456        let current_tick = self.daemon_tick.load(Ordering::Relaxed);
457        let mut should_time_flush = false;
458        self.with_last_flush_tick(|tick| {
459            should_time_flush = current_tick.wrapping_sub(*tick) >= self.flush_tick_threshold;
460        });
461
462        // Worker TLS batch buffer
463        let mut option_kv = Some((key, value));
464        let pushed_to_buf = self.with_worker_id(|id| {
465            let (k, v) = option_kv.take().unwrap();
466            if id >= self.miss_buffers.len() {
467                // Worker overflow: gracefully degrade to direct send
468                let _ = self.cmd_tx.try_send(Command::Insert(k, v, hash, is_t1));
469                return;
470            }
471
472            // Safety: id is unique per thread → exclusive slot access
473            let buf: &mut crate::unsafe_core::BatchBuf<K, V> = self.miss_buffers[id].get_mut_safe();
474            let capacity_flush = buf.push((k, v, hash, is_t1));
475
476            if capacity_flush || (should_time_flush && !buf.is_empty()) {
477                let batch = buf.drain_to_vec();
478                let _ = self.cmd_tx.try_send(Command::BatchInsert(batch));
479                self.with_last_flush_tick(|tick| {
480                    *tick = current_tick;
481                });
482            }
483        });
484
485        if pushed_to_buf.is_none() {
486            if let Some((k, v)) = option_kv {
487                let _ = self.cmd_tx.try_send(Command::Insert(k, v, hash, is_t1));
488            }
489        }
490    }
491
492    /// Start a Cold Start Session to inject items directly to T1.
493    pub fn begin_cold_start_session(&self) -> ColdStartSession<'_, K, V, S, Tls> {
494        ColdStartSession { cache: self }
495    }
496
497    /// Remove a key from the cache.
498    pub fn remove(&self, key: &K) {
499        let hash = self.hash(key);
500
501        // ── flush this thread's buffer first for causal ordering ─────
502        self.with_worker_id(|id| {
503            if id < self.miss_buffers.len() {
504                let buf: &mut crate::unsafe_core::BatchBuf<K, V> = self.miss_buffers[id].get_mut_safe();
505                if buf.len() > 0 {
506                    let batch = buf.drain_to_vec();
507                    let _ = self.cmd_tx.try_send(Command::BatchInsert(batch));
508                    let tick = self.daemon_tick.load(Ordering::Relaxed);
509                    self.with_last_flush_tick(|c| *c = tick);
510                }
511            }
512        });
513
514        self.cmd_tx.send_blocking(Command::Remove(key.clone(), hash));
515    }
516
517    /// Clear all cached data.
518    pub fn clear(&self) {
519        let ack = OneshotAck::new();
520        self.cmd_tx.send_blocking(Command::Clear(ack.clone()));
521        ack.wait();
522    }
523
524    // ── Internals ─────────────────────────────────────────────────────────
525
526    #[inline(always)]
527    fn with_worker_id<F, R>(&self, f: F) -> Option<R>
528    where
529        F: FnOnce(usize) -> R,
530    {
531        self.tls.get_worker_id().map(f)
532    }
533
534    #[inline(always)]
535    fn with_hit_buf<F, R>(&self, mut f: F) -> Option<R>
536    where
537        F: FnMut(&mut ([usize; 64], usize)) -> R,
538    {
539        let mut res = None;
540        self.tls.with_hit_buf(&mut |buf| {
541            res = Some(f(buf));
542        });
543        res
544    }
545
546    #[inline(always)]
547    fn with_l1_filter<F, R>(&self, mut f: F) -> Option<R>
548    where
549        F: FnMut(&mut ([u8; 4096], usize)) -> R,
550    {
551        let mut res = None;
552        self.tls.with_l1_filter(&mut |filter| {
553            res = Some(f(filter));
554        });
555        res
556    }
557
558    #[inline(always)]
559    fn with_last_flush_tick<F, R>(&self, mut f: F) -> Option<R>
560    where
561        F: FnMut(&mut TickType) -> R,
562    {
563        let mut res = None;
564        self.tls.with_last_flush_tick(&mut |tick| {
565            res = Some(f(tick));
566        });
567        res
568    }
569
570    #[inline(always)]
571    fn hash(&self, key: &K) -> u64 {
572        self.hasher.hash_one(key)
573    }
574
575    /// Buffer a Cache-hit global index for Daemon processing.
576    ///
577    /// std: fills the 64-element TLS array and ships it to `hit_tx` when full.
578    /// no_std: sends directly (no TLS batch buffering available).
579    #[inline(always)]
580    fn record_hit(&self, global_idx: usize) {
581        let opt = self.with_hit_buf(|state| {
582            let idx = state.1;
583            state.0[idx] = global_idx;
584            state.1 += 1;
585            if state.1 == 64_usize {
586                let _ = self.hit_tx.try_send(state.0);
587                state.0 = [usize::MAX; 64];
588                state.1 = 0;
589            }
590        });
591
592        if opt.is_none() {
593            let mut batch = [usize::MAX; 64];
594            batch[0] = global_idx;
595            let _ = self.hit_tx.try_send(batch);
596        }
597    }
598}
599
600impl<K, V, S, Tls: TlsProvider> Drop for DualCacheFF<K, V, S, Tls> {
601    fn drop(&mut self) {
602        if Arc::strong_count(&self.cmd_tx) <= 2 {
603            let _ = self.cmd_tx.try_send(Command::Shutdown);
604        }
605    }
606}
607
608
609/// A session object for injecting items directly into T1 during cold starts.
610/// By requiring a session object, we prevent accidental use of `insert_t1`
611/// in normal hot paths which would bypass TLS batching and overload the daemon.
612pub struct ColdStartSession<'a, K, V, S, Tls: TlsProvider> {
613    cache: &'a DualCacheFF<K, V, S, Tls>,
614}
615
616impl<'a, K, V, S, Tls: TlsProvider> ColdStartSession<'a, K, V, S, Tls>
617where
618    K: Hash + Eq + Send + Sync + Clone + 'static,
619    V: Send + Sync + Clone + 'static,
620    S: BuildHasher + Clone + Send + 'static,
621{
622    /// Insert a key-value pair directly as a high-priority "genius" item.
623    /// This bypasses the L1 probation filter, doesn't use the thread-local batch buffer,
624    /// and assigns the item the maximum survival rank (e.g. 255) and promotes it to T1 immediately.
625    pub fn warmup(&self, key: K, value: V) {
626        let hash = self.cache.hash(&key);
627        let _ = self.cache.cmd_tx.try_send(Command::InsertT1(key, value, hash));
628    }
629}