foyer_memory/eviction/
lru.rs

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