Skip to main content

foyer_memory/eviction/
sieve.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::{
16    mem::offset_of,
17    sync::{
18        Arc,
19        atomic::{AtomicBool, Ordering},
20    },
21};
22
23use foyer_common::{
24    code::{Key, Value},
25    error::Result,
26    properties::Properties,
27};
28use intrusive_collections::{LinkedList, LinkedListAtomicLink, intrusive_adapter};
29use serde::{Deserialize, Serialize};
30
31use super::{Eviction, Op};
32use crate::record::Record;
33
34/// Sieve eviction algorithm config.
35#[derive(Debug, Clone, Default, Serialize, Deserialize)]
36pub struct SieveConfig;
37
38#[derive(Debug, Default)]
39pub struct SieveState {
40    link: LinkedListAtomicLink,
41    visited: AtomicBool,
42}
43
44impl SieveState {
45    #[inline]
46    fn is_visited(&self) -> bool {
47        self.visited.load(Ordering::Relaxed)
48    }
49
50    fn set_visited(&self, visited: bool) {
51        self.visited.store(visited, Ordering::Relaxed);
52    }
53}
54
55intrusive_adapter! {
56    Adapter<K, V, P> = Arc<Record<Sieve<K, V, P>>>: Record<Sieve<K, V, P>> {
57        ?offset = Record::<Sieve<K, V, P>>::STATE_OFFSET + offset_of!(SieveState, link) => LinkedListAtomicLink
58    }
59    where K: Key, V: Value, P: Properties
60}
61
62/// Sieve eviction algorithm implementation based on the paper:
63/// "SIEVE is Simpler than LRU: an Efficient Turn-Key Eviction Algorithm for Web Caches"
64/// (https://junchengyang.com/publication/nsdi24-SIEVE.pdf).
65///
66/// The Sieve algorithm is specifically designed for web cache workloads, providing
67/// a simple and efficient alternative to traditional LRU eviction policies. It leverages
68/// a visited bit to distinguish between recently accessed and unaccessed items, enabling
69/// efficient eviction decisions with minimal overhead.
70///
71/// **Note:** Due to its lack of scan resistance, Sieve is not recommended for use in
72/// block cache workloads or environments where scan-resistant eviction is required.
73/// It is best suited for web workloads where access patterns align with the algorithm's
74/// design assumptions.
75pub struct Sieve<K, V, P>
76where
77    K: Key,
78    V: Value,
79    P: Properties,
80{
81    queue: LinkedList<Adapter<K, V, P>>,
82    /// Hand pointer for eviction scanning, points to the next candidate to examine
83    hand: Option<Arc<Record<Sieve<K, V, P>>>>,
84}
85
86impl<K, V, P> Eviction for Sieve<K, V, P>
87where
88    K: Key,
89    V: Value,
90    P: Properties,
91{
92    type Config = SieveConfig;
93    type Key = K;
94    type Value = V;
95    type Properties = P;
96    type State = SieveState;
97
98    fn new(_capacity: usize, _config: &Self::Config) -> Self
99    where
100        Self: Sized,
101    {
102        Self {
103            queue: LinkedList::new(Adapter::new()),
104            hand: None,
105        }
106    }
107
108    fn update(&mut self, _: usize, _: Option<&Self::Config>) -> Result<()> {
109        Ok(())
110    }
111
112    fn push(&mut self, record: Arc<Record<Self>>) {
113        record.set_in_eviction(true);
114        self.queue.push_back(record);
115    }
116
117    fn pop(&mut self) -> Option<Arc<Record<Self>>> {
118        let mut candidate = if let Some(ref hand_ptr) = self.hand {
119            unsafe { self.queue.cursor_mut_from_ptr(Arc::as_ptr(hand_ptr)) }
120        } else {
121            self.queue.front_mut()
122        };
123
124        loop {
125            {
126                let record = candidate.get()?;
127                let state = unsafe { &*record.state().get() };
128                if !state.is_visited() {
129                    break;
130                } else {
131                    state.set_visited(false);
132                    if candidate.peek_next().is_null() {
133                        candidate = self.queue.front_mut();
134                    } else {
135                        candidate.move_next();
136                    }
137                }
138            }
139        }
140
141        self.hand = candidate.peek_next().clone_pointer();
142        candidate.remove().inspect(|record| record.set_in_eviction(false))
143    }
144
145    fn remove(&mut self, record: &Arc<Record<Self>>) {
146        if let Some(ref hand_ptr) = self.hand
147            && Arc::ptr_eq(hand_ptr, record)
148        {
149            // Reset hand if we are removing the current hand pointer
150            self.hand = None;
151        }
152
153        unsafe { self.queue.remove_from_ptr(Arc::as_ptr(record)) };
154        record.set_in_eviction(false);
155    }
156
157    fn acquire() -> Op<Self> {
158        Op::immutable(|_: &Self, record| {
159            let state = unsafe { &*record.state().get() };
160            state.set_visited(true);
161        })
162    }
163
164    fn release() -> Op<Self> {
165        Op::noop()
166    }
167}
168
169#[cfg(test)]
170pub mod tests {
171    use itertools::Itertools;
172
173    use super::*;
174    use crate::{
175        eviction::test_utils::{Dump, OpExt, TestProperties, assert_ptr_eq, assert_ptr_vec_eq},
176        record::Data,
177    };
178
179    impl<K, V> Dump for Sieve<K, V, TestProperties>
180    where
181        K: Key + Clone,
182        V: Value + Clone,
183    {
184        type Output = Vec<Arc<Record<Self>>>;
185        fn dump(&self) -> Self::Output {
186            let mut res = vec![];
187            let mut cursor = self.queue.cursor();
188            loop {
189                cursor.move_next();
190                match cursor.clone_pointer() {
191                    Some(record) => res.push(record),
192                    None => break,
193                }
194            }
195            res
196        }
197    }
198
199    type TestSieve = Sieve<u64, u64, TestProperties>;
200
201    #[test]
202    fn test_sieve_basic() {
203        let rs = (0..8)
204            .map(|i| {
205                Arc::new(Record::new(Data {
206                    key: i,
207                    value: i,
208                    properties: TestProperties::default(),
209                    hash: i,
210                    weight: 1,
211                }))
212            })
213            .collect_vec();
214        let r = |i: usize| rs[i].clone();
215        let mut sieve = TestSieve::new(100, &SieveConfig {});
216
217        // 0, 1, 2, 3
218        sieve.push(r(0));
219        sieve.push(r(1));
220        sieve.push(r(2));
221        sieve.push(r(3));
222        assert_ptr_vec_eq(sieve.dump(), vec![r(0), r(1), r(2), r(3)]);
223
224        sieve.acquire_immutable(&r(1));
225        sieve.acquire_immutable(&r(3));
226
227        // 0 is oldest, and not visited, so it should be evicted first
228        let r0 = sieve.pop().unwrap();
229        assert_ptr_eq(&rs[0], &r0);
230
231        // 1 is visited, so it will not be evicted in this round
232        let r2 = sieve.pop().unwrap();
233        assert_ptr_eq(&rs[2], &r2);
234
235        // Now we only have 1 and 3 in the sieve, and 1 is unvisited, 3 is visited
236        // and hand points to 3
237        assert_ptr_vec_eq(sieve.dump(), vec![r(1), r(3)]);
238
239        // 1 is unvisited, so it will be evicted
240        let r1 = sieve.pop().unwrap();
241        assert_ptr_eq(&rs[1], &r1);
242
243        assert_ptr_vec_eq(sieve.dump(), vec![r(3)]);
244
245        sieve.remove(&r(3));
246        assert_ptr_vec_eq(sieve.dump(), vec![]);
247
248        // clear
249        sieve.push(r(4));
250        sieve.push(r(5));
251        sieve.push(r(6));
252        sieve.clear();
253        assert_ptr_vec_eq(sieve.dump(), vec![]);
254    }
255}