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