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