foyer_memory/eviction/
lru.rs

1// Copyright 2025 foyer Project Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{mem::offset_of, sync::Arc};
16
17use foyer_common::{
18    code::{Key, Value},
19    strict_assert,
20};
21use intrusive_collections::{intrusive_adapter, LinkedList, LinkedListAtomicLink};
22use serde::{Deserialize, Serialize};
23
24use super::{Eviction, Op};
25use crate::{
26    error::{Error, Result},
27    record::{CacheHint, Record},
28};
29
30/// Lru eviction algorithm config.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct LruConfig {
33    /// The ratio of the high priority pool occupied.
34    ///
35    /// `Lru` guarantees that the high priority weight are always as larger as
36    /// but no larger that the capacity * high priority pool ratio.
37    ///
38    /// # Panic
39    ///
40    /// Panics if the value is not in [0, 1.0].
41    pub high_priority_pool_ratio: f64,
42}
43
44impl Default for LruConfig {
45    fn default() -> Self {
46        Self {
47            high_priority_pool_ratio: 0.9,
48        }
49    }
50}
51
52/// Lru eviction algorithm hint.
53#[derive(Debug, Clone)]
54pub enum LruHint {
55    HighPriority,
56    LowPriority,
57}
58
59impl Default for LruHint {
60    fn default() -> Self {
61        Self::HighPriority
62    }
63}
64
65impl From<CacheHint> for LruHint {
66    fn from(hint: CacheHint) -> Self {
67        match hint {
68            CacheHint::Normal => LruHint::HighPriority,
69            CacheHint::Low => LruHint::LowPriority,
70        }
71    }
72}
73
74impl From<LruHint> for CacheHint {
75    fn from(hint: LruHint) -> Self {
76        match hint {
77            LruHint::HighPriority => CacheHint::Normal,
78            LruHint::LowPriority => CacheHint::Low,
79        }
80    }
81}
82
83/// Lru eviction algorithm state.
84#[derive(Debug, Default)]
85pub struct LruState {
86    link: LinkedListAtomicLink,
87    in_high_priority_pool: bool,
88    is_pinned: bool,
89}
90
91intrusive_adapter! { Adapter<K, V> = Arc<Record<Lru<K, V>>>: Record<Lru<K, V>> { ?offset = Record::<Lru<K, V>>::STATE_OFFSET + offset_of!(LruState, link) => LinkedListAtomicLink } where K: Key, V: Value }
92
93pub struct Lru<K, V>
94where
95    K: Key,
96    V: Value,
97{
98    high_priority_list: LinkedList<Adapter<K, V>>,
99    list: LinkedList<Adapter<K, V>>,
100    pin_list: LinkedList<Adapter<K, V>>,
101
102    high_priority_weight: usize,
103    high_priority_weight_capacity: usize,
104
105    config: LruConfig,
106}
107
108impl<K, V> Lru<K, V>
109where
110    K: Key,
111    V: Value,
112{
113    fn may_overflow_high_priority_pool(&mut self) {
114        while self.high_priority_weight > self.high_priority_weight_capacity {
115            strict_assert!(!self.high_priority_list.is_empty());
116
117            // overflow last entry in high priority pool to low priority pool
118            let record = self.high_priority_list.pop_front().unwrap();
119            let state = unsafe { &mut *record.state().get() };
120            strict_assert!(state.in_high_priority_pool);
121            state.in_high_priority_pool = false;
122            self.high_priority_weight -= record.weight();
123            self.list.push_back(record);
124        }
125    }
126}
127
128impl<K, V> Eviction for Lru<K, V>
129where
130    K: Key,
131    V: Value,
132{
133    type Config = LruConfig;
134    type Key = K;
135    type Value = V;
136    type Hint = LruHint;
137    type State = LruState;
138
139    fn new(capacity: usize, config: &Self::Config) -> Self
140    where
141        Self: Sized,
142    {
143        assert!(
144            (0.0..=1.0).contains(&config.high_priority_pool_ratio),
145            "high_priority_pool_ratio_percentage must be in 0.0..=1.0, given: {}",
146            config.high_priority_pool_ratio
147        );
148
149        let config = config.clone();
150
151        let high_priority_weight_capacity = (capacity as f64 * config.high_priority_pool_ratio) as usize;
152
153        Self {
154            high_priority_list: LinkedList::new(Adapter::new()),
155            list: LinkedList::new(Adapter::new()),
156            pin_list: LinkedList::new(Adapter::new()),
157            high_priority_weight: 0,
158            high_priority_weight_capacity,
159            config,
160        }
161    }
162
163    fn update(&mut self, capacity: usize, config: Option<&Self::Config>) -> Result<()> {
164        if let Some(config) = config {
165            if !(0.0..=1.0).contains(&config.high_priority_pool_ratio) {
166                return Err(Error::ConfigError(
167                    format!(
168                        "[lru]: high_priority_pool_ratio_percentage must be in 0.0..=1.0, given: {}, new configuration ignored",
169                        config.high_priority_pool_ratio
170                    )
171                ));
172            }
173            self.config = config.clone();
174        }
175
176        let high_priority_weight_capacity = (capacity as f64 * self.config.high_priority_pool_ratio) as usize;
177        self.high_priority_weight_capacity = high_priority_weight_capacity;
178
179        self.may_overflow_high_priority_pool();
180
181        Ok(())
182    }
183
184    fn push(&mut self, record: Arc<Record<Self>>) {
185        let state = unsafe { &mut *record.state().get() };
186
187        strict_assert!(!state.link.is_linked());
188
189        record.set_in_eviction(true);
190
191        match record.hint() {
192            LruHint::HighPriority => {
193                state.in_high_priority_pool = true;
194                self.high_priority_weight += record.weight();
195                self.high_priority_list.push_back(record);
196
197                self.may_overflow_high_priority_pool();
198            }
199            LruHint::LowPriority => {
200                state.in_high_priority_pool = false;
201                self.list.push_back(record);
202            }
203        }
204    }
205
206    fn pop(&mut self) -> Option<Arc<Record<Self>>> {
207        let record = self.list.pop_front().or_else(|| self.high_priority_list.pop_front())?;
208
209        let state = unsafe { &mut *record.state().get() };
210
211        strict_assert!(!state.link.is_linked());
212
213        if state.in_high_priority_pool {
214            self.high_priority_weight -= record.weight();
215            state.in_high_priority_pool = false;
216        }
217
218        record.set_in_eviction(false);
219
220        Some(record)
221    }
222
223    fn remove(&mut self, record: &Arc<Record<Self>>) {
224        let state = unsafe { &mut *record.state().get() };
225
226        strict_assert!(state.link.is_linked());
227
228        match (state.is_pinned, state.in_high_priority_pool) {
229            (true, false) => unsafe { self.pin_list.remove_from_ptr(Arc::as_ptr(record)) },
230            (true, true) => unsafe {
231                self.high_priority_weight -= record.weight();
232                state.in_high_priority_pool = false;
233                self.pin_list.remove_from_ptr(Arc::as_ptr(record))
234            },
235            (false, true) => {
236                self.high_priority_weight -= record.weight();
237                state.in_high_priority_pool = false;
238                unsafe { self.high_priority_list.remove_from_ptr(Arc::as_ptr(record)) }
239            }
240            (false, false) => unsafe { self.list.remove_from_ptr(Arc::as_ptr(record)) },
241        };
242
243        strict_assert!(!state.link.is_linked());
244
245        record.set_in_eviction(false);
246    }
247
248    fn clear(&mut self) {
249        while self.pop().is_some() {}
250
251        // Clear pin list to prevent from memory leak.
252        while let Some(record) = self.pin_list.pop_front() {
253            let state = unsafe { &mut *record.state().get() };
254            strict_assert!(!state.link.is_linked());
255
256            if state.in_high_priority_pool {
257                self.high_priority_weight -= record.weight();
258                state.in_high_priority_pool = false;
259            }
260
261            record.set_in_eviction(false);
262        }
263
264        assert!(self.list.is_empty());
265        assert!(self.high_priority_list.is_empty());
266        assert!(self.pin_list.is_empty());
267        assert_eq!(self.high_priority_weight, 0);
268    }
269
270    fn acquire() -> Op<Self> {
271        Op::mutable(|this: &mut Self, record| {
272            if !record.is_in_eviction() {
273                return;
274            }
275
276            let state = unsafe { &mut *record.state().get() };
277            assert!(state.link.is_linked());
278
279            if state.is_pinned {
280                return;
281            }
282
283            // Pin the record by moving it to the pin list.
284
285            let r = if state.in_high_priority_pool {
286                unsafe { this.high_priority_list.remove_from_ptr(Arc::as_ptr(record)) }
287            } else {
288                unsafe { this.list.remove_from_ptr(Arc::as_ptr(record)) }
289            };
290
291            this.pin_list.push_back(r);
292
293            state.is_pinned = true;
294        })
295    }
296
297    fn release() -> Op<Self> {
298        Op::mutable(|this: &mut Self, record| {
299            if !record.is_in_eviction() {
300                return;
301            }
302
303            let state = unsafe { &mut *record.state().get() };
304            assert!(state.link.is_linked());
305
306            if !state.is_pinned {
307                return;
308            }
309
310            // Unpin the record by moving it from the pin list.
311
312            unsafe { this.pin_list.remove_from_ptr(Arc::as_ptr(record)) };
313
314            if state.in_high_priority_pool {
315                this.high_priority_list.push_back(record.clone());
316            } else {
317                this.list.push_back(record.clone());
318            }
319
320            state.is_pinned = false;
321        })
322    }
323}
324
325#[cfg(test)]
326pub mod tests {
327
328    use itertools::Itertools;
329
330    use super::*;
331    use crate::{
332        eviction::test_utils::{assert_ptr_eq, assert_ptr_vec_vec_eq, Dump, OpExt},
333        record::Data,
334    };
335
336    impl<K, V> Dump for Lru<K, V>
337    where
338        K: Key + Clone,
339        V: Value + Clone,
340    {
341        type Output = Vec<Vec<Arc<Record<Self>>>>;
342        fn dump(&self) -> Self::Output {
343            let mut low = vec![];
344            let mut high = vec![];
345            let mut pin = vec![];
346
347            let mut cursor = self.list.cursor();
348            loop {
349                cursor.move_next();
350                match cursor.clone_pointer() {
351                    Some(record) => low.push(record),
352                    None => break,
353                }
354            }
355
356            let mut cursor = self.high_priority_list.cursor();
357            loop {
358                cursor.move_next();
359                match cursor.clone_pointer() {
360                    Some(record) => high.push(record),
361                    None => break,
362                }
363            }
364
365            let mut cursor = self.pin_list.cursor();
366            loop {
367                cursor.move_next();
368                match cursor.clone_pointer() {
369                    Some(record) => pin.push(record),
370                    None => break,
371                }
372            }
373
374            vec![low, high, pin]
375        }
376    }
377
378    type TestLru = Lru<u64, u64>;
379
380    #[test]
381    fn test_lru() {
382        let rs = (0..20)
383            .map(|i| {
384                Arc::new(Record::new(Data {
385                    key: i,
386                    value: i,
387                    hint: if i < 10 {
388                        LruHint::HighPriority
389                    } else {
390                        LruHint::LowPriority
391                    },
392                    hash: i,
393                    weight: 1,
394                }))
395            })
396            .collect_vec();
397        let r = |i: usize| rs[i].clone();
398
399        let config = LruConfig {
400            high_priority_pool_ratio: 0.5,
401        };
402        let mut lru = TestLru::new(8, &config);
403
404        assert_eq!(lru.high_priority_weight_capacity, 4);
405
406        // [0, 1, 2, 3]
407        lru.push(r(0));
408        lru.push(r(1));
409        lru.push(r(2));
410        lru.push(r(3));
411        assert_ptr_vec_vec_eq(lru.dump(), vec![vec![], vec![r(0), r(1), r(2), r(3)], vec![]]);
412
413        // 0, [1, 2, 3, 4]
414        lru.push(r(4));
415        assert_ptr_vec_vec_eq(lru.dump(), vec![vec![r(0)], vec![r(1), r(2), r(3), r(4)], vec![]]);
416
417        // 0, 10, [1, 2, 3, 4]
418        lru.push(r(10));
419        assert_ptr_vec_vec_eq(
420            lru.dump(),
421            vec![vec![r(0), r(10)], vec![r(1), r(2), r(3), r(4)], vec![]],
422        );
423
424        // 10, [1, 2, 3, 4]
425        let r0 = lru.pop().unwrap();
426        assert_ptr_eq(&r(0), &r0);
427        assert_ptr_vec_vec_eq(lru.dump(), vec![vec![r(10)], vec![r(1), r(2), r(3), r(4)], vec![]]);
428
429        // 10, [1, 3, 4]
430        lru.remove(&rs[2]);
431        assert_ptr_vec_vec_eq(lru.dump(), vec![vec![r(10)], vec![r(1), r(3), r(4)], vec![]]);
432
433        // 10, 11, [1, 3, 4]
434        lru.push(r(11));
435        assert_ptr_vec_vec_eq(lru.dump(), vec![vec![r(10), r(11)], vec![r(1), r(3), r(4)], vec![]]);
436
437        // 10, 11, 1, [3, 4, 5, 6]
438        lru.push(r(5));
439        lru.push(r(6));
440        assert_ptr_vec_vec_eq(
441            lru.dump(),
442            vec![vec![r(10), r(11), r(1)], vec![r(3), r(4), r(5), r(6)], vec![]],
443        );
444
445        // 10, 11, 1, 3, [4, 5, 6, 0]
446        lru.push(r(0));
447        assert_ptr_vec_vec_eq(
448            lru.dump(),
449            vec![vec![r(10), r(11), r(1), r(3)], vec![r(4), r(5), r(6), r(0)], vec![]],
450        );
451
452        lru.clear();
453        assert_ptr_vec_vec_eq(lru.dump(), vec![vec![], vec![], vec![]]);
454    }
455
456    #[test]
457    fn test_lru_pin() {
458        let rs = (0..20)
459            .map(|i| {
460                Arc::new(Record::new(Data {
461                    key: i,
462                    value: i,
463                    hint: if i < 10 {
464                        LruHint::HighPriority
465                    } else {
466                        LruHint::LowPriority
467                    },
468                    hash: i,
469                    weight: 1,
470                }))
471            })
472            .collect_vec();
473        let r = |i: usize| rs[i].clone();
474
475        let config = LruConfig {
476            high_priority_pool_ratio: 0.5,
477        };
478        let mut lru = TestLru::new(8, &config);
479
480        assert_eq!(lru.high_priority_weight_capacity, 4);
481
482        // 10, 11, [0, 1]
483        lru.push(r(0));
484        lru.push(r(1));
485        lru.push(r(10));
486        lru.push(r(11));
487        assert_ptr_vec_vec_eq(lru.dump(), vec![vec![r(10), r(11)], vec![r(0), r(1)], vec![]]);
488
489        // pin: [0], 10
490        // 11, [1]
491        lru.acquire_mutable(&rs[0]);
492        lru.acquire_mutable(&rs[10]);
493        assert_ptr_vec_vec_eq(lru.dump(), vec![vec![r(11)], vec![r(1)], vec![r(0), r(10)]]);
494
495        // 11, 10, [1, 0]
496        lru.release_mutable(&rs[0]);
497        lru.release_mutable(&rs[10]);
498        assert_ptr_vec_vec_eq(lru.dump(), vec![vec![r(11), r(10)], vec![r(1), r(0)], vec![]]);
499
500        // acquire pinned
501        // pin: [0], 11
502        // 10, [1]
503        lru.acquire_mutable(&rs[0]);
504        lru.acquire_mutable(&rs[11]);
505        lru.acquire_mutable(&rs[0]);
506        lru.acquire_mutable(&rs[11]);
507        assert_ptr_vec_vec_eq(lru.dump(), vec![vec![r(10)], vec![r(1)], vec![r(0), r(11)]]);
508
509        // remove pinned (low priority)
510        // pin: [0]
511        // 10, [1]
512        lru.remove(&rs[11]);
513        assert_ptr_vec_vec_eq(lru.dump(), vec![vec![r(10)], vec![r(1)], vec![r(0)]]);
514
515        // remove pinned (high priority)
516        // step 1:
517        //   pin: [0], [2]
518        //   10, [1]
519        lru.push(r(2));
520        lru.acquire_mutable(&rs[2]);
521        assert_ptr_vec_vec_eq(lru.dump(), vec![vec![r(10)], vec![r(1)], vec![r(0), r(2)]]);
522        // step 2:
523        //   pin: [0]
524        //   10, [1]
525        lru.remove(&rs[2]);
526        assert_ptr_vec_vec_eq(lru.dump(), vec![vec![r(10)], vec![r(1)], vec![r(0)]]);
527
528        // release removed
529        // pin: [0]
530        // 10, [1]
531        lru.release_mutable(&rs[11]);
532        assert_ptr_vec_vec_eq(lru.dump(), vec![vec![r(10)], vec![r(1)], vec![r(0)]]);
533
534        // release unpinned
535        // 10, [1, 0]
536        lru.release_mutable(&rs[0]);
537        lru.release_mutable(&rs[0]);
538        assert_ptr_vec_vec_eq(lru.dump(), vec![vec![r(10)], vec![r(1), r(0)], vec![]]);
539
540        // clear with pinned
541        // pin: [1]
542        // 10, [0]
543        lru.acquire_mutable(&rs[1]);
544        assert_ptr_vec_vec_eq(lru.dump(), vec![vec![r(10)], vec![r(0)], vec![r(1)]]);
545
546        lru.clear();
547        assert_ptr_vec_vec_eq(lru.dump(), vec![vec![], vec![], vec![]]);
548    }
549}