dd_sds/scanner/regex_rule/
regex_store.rs1use 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 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, ®EX_STORE)
35}
36
37pub 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 let regex = regex_factory(pattern)?;
57
58 let mut regex_store = store.lock().unwrap();
59 Ok(regex_store.insert(pattern, regex))
60}
61
62const 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 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 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 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 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 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 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 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 assert_eq!(store.lock().unwrap().len(), 1);
203 }
204}