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