dd_sds/scanner/regex_rule/
regex_store.rs

1use crate::stats::GLOBAL_STATS;
2use ahash::AHashMap;
3use lazy_static::lazy_static;
4use regex_automata::meta::{Cache, Regex as MetaRegex};
5use slotmap::{SlotMap, new_key_type};
6use std::ops::Deref;
7use std::sync::Weak;
8use std::sync::{Arc, Mutex};
9
10struct WeakSharedRegex {
11    regex: Weak<MetaRegex>,
12    // number of bytes used for the cache. Just used for metrics.
13    cache_size: usize,
14}
15
16#[derive(Debug, Clone)]
17pub struct SharedRegex {
18    pub regex: Arc<MetaRegex>,
19    pub cache_key: RegexCacheKey,
20}
21
22impl Deref for SharedRegex {
23    type Target = MetaRegex;
24
25    fn deref(&self) -> &Self::Target {
26        self.regex.deref()
27    }
28}
29
30pub fn get_memoized_regex<T>(
31    pattern: &str,
32    regex_factory: impl FnOnce(&str) -> Result<regex_automata::meta::Regex, T>,
33) -> Result<SharedRegex, T> {
34    get_memoized_regex_with_custom_store(pattern, regex_factory, &REGEX_STORE)
35}
36
37/// Drops store bookkeeping for regexes no longer referenced by any scanner (otherwise
38/// cleared only every `GC_FREQUENCY` inserts).
39pub fn gc_regex_store() {
40    REGEX_STORE.lock().unwrap().gc();
41}
42
43fn get_memoized_regex_with_custom_store<T>(
44    pattern: &str,
45    regex_factory: impl FnOnce(&str) -> Result<regex_automata::meta::Regex, T>,
46    store: &Mutex<RegexStore>,
47) -> Result<SharedRegex, T> {
48    {
49        let regex_store = store.lock().unwrap();
50        if let Some(exiting_regex) = regex_store.get(pattern) {
51            return Ok(exiting_regex);
52        }
53    }
54
55    // Create the new regex after the RegexStore lock is released, since this can be slow
56    let regex = regex_factory(pattern)?;
57
58    let mut regex_store = store.lock().unwrap();
59    Ok(regex_store.insert(pattern, regex))
60}
61
62// A GC of the regex store happens every N insertions
63// This is needed to occasionally clean out Weak references that have been dropped.
64const GC_FREQUENCY: u64 = 1_000;
65
66lazy_static! {
67    static ref REGEX_STORE: Arc<Mutex<RegexStore>> = Arc::new(Mutex::new(RegexStore::new()));
68}
69new_key_type! { pub struct RegexCacheKey; }
70
71struct RegexStore {
72    pattern_index: AHashMap<String, RegexCacheKey>,
73    key_map: SlotMap<RegexCacheKey, WeakSharedRegex>,
74    // used to decide when to GC. Counts up to `GC_FREQUENCY` and is reset to 0 when a GC happens
75    gc_counter: u64,
76}
77
78impl RegexStore {
79    pub fn new() -> Self {
80        Self {
81            pattern_index: AHashMap::new(),
82            key_map: SlotMap::with_key(),
83            gc_counter: 0,
84        }
85    }
86
87    /// Cleans up any configuration no longer used in Scanners. Should be called periodically.
88    fn gc(&mut self) {
89        self.gc_counter = 0;
90        self.pattern_index.retain(|_, cache_key| {
91            if self.key_map.get(*cache_key).unwrap().regex.strong_count() == 0 {
92                if let Some(old_regex) = self.key_map.remove(*cache_key) {
93                    GLOBAL_STATS.add_total_regex_cache(-(old_regex.cache_size as i64));
94                }
95                false
96            } else {
97                true
98            }
99        });
100        GLOBAL_STATS.set_total_regexes(self.key_map.len());
101    }
102
103    /// Check if a regex for this pattern already exists, and returns a copy if it does
104    pub fn get(&self, pattern: &str) -> Option<SharedRegex> {
105        self.pattern_index.get(pattern).and_then(|cache_key| {
106            self.key_map
107                .get(*cache_key)
108                .and_then(|x| x.regex.upgrade())
109                .map(|regex| SharedRegex {
110                    regex,
111                    cache_key: *cache_key,
112                })
113        })
114    }
115
116    #[cfg(test)]
117    fn len(&self) -> usize {
118        debug_assert_eq!(self.pattern_index.len(), self.key_map.len());
119        self.key_map.len()
120    }
121
122    /// Inserts a new rule into the cache. The "memoized" rule is returned and should be
123    /// used instead of the one passed in. This ensures that if there were duplicates of
124    /// a rule being created at the same time, only one is kept.
125    pub fn insert(&mut self, pattern: &str, regex: MetaRegex) -> SharedRegex {
126        self.gc_counter += 1;
127        if self.gc_counter >= GC_FREQUENCY {
128            self.gc();
129        }
130        match self.get(pattern) {
131            Some(existing_regex) => existing_regex,
132            _ => {
133                let shared_regex = Arc::new(regex);
134
135                let regex_cache = shared_regex.create_cache();
136                let cache_size = regex_cache.memory_usage() + std::mem::size_of::<Cache>();
137                let cache_key = self.key_map.insert(WeakSharedRegex {
138                    regex: Arc::downgrade(&shared_regex),
139                    cache_size,
140                });
141                GLOBAL_STATS.add_total_regex_cache(cache_size as i64);
142                if let Some(old_cache_key) =
143                    self.pattern_index.insert(pattern.to_owned(), cache_key)
144                {
145                    // cleanup old value (which must be a "dead" reference since `get` returned None)
146                    if let Some(weak_ref) = self.key_map.remove(old_cache_key) {
147                        GLOBAL_STATS.add_total_regex_cache(-(weak_ref.cache_size as i64));
148                        debug_assert!(weak_ref.regex.strong_count() == 0)
149                    }
150                }
151
152                GLOBAL_STATS.set_total_regexes(self.key_map.len());
153
154                SharedRegex {
155                    regex: shared_regex,
156                    cache_key,
157                }
158            }
159        }
160    }
161}
162
163#[cfg(test)]
164mod test {
165    use crate::scanner::regex_rule::regex_store::{
166        GC_FREQUENCY, RegexStore, get_memoized_regex_with_custom_store,
167    };
168    use regex_automata::meta::Regex;
169    use std::sync::Mutex;
170
171    #[test]
172    fn dropped_regexes_should_be_removed_from_global_store() {
173        let store = Mutex::new(RegexStore::new());
174
175        let regex = get_memoized_regex_with_custom_store("test", Regex::new, &store).unwrap();
176
177        assert_eq!(store.lock().unwrap().len(), 1);
178
179        drop(regex);
180
181        // force an early GC
182        store.lock().unwrap().gc();
183
184        assert_eq!(store.lock().unwrap().len(), 0);
185    }
186
187    #[test]
188    fn test_automatic_gc() {
189        let store = Mutex::new(RegexStore::new());
190
191        let regex = get_memoized_regex_with_custom_store("test", Regex::new, &store).unwrap();
192        drop(regex);
193
194        // insert enough new patterns to trigger a GC
195        for i in 0..(GC_FREQUENCY - 1) {
196            let regex =
197                get_memoized_regex_with_custom_store(&format!("test-{i}"), Regex::new, &store)
198                    .unwrap();
199            drop(regex)
200        }
201        // The insertion that triggered the GC is itself not cleaned up yet, but everything else is
202        assert_eq!(store.lock().unwrap().len(), 1);
203    }
204}