1use crate::{get_current_epoch, LuaConfig, LuaConfigInner};
2use kumo_prometheus::declare_metric;
3use parking_lot::FairMutex as Mutex;
4use std::collections::VecDeque;
5use std::sync::atomic::{AtomicUsize, Ordering};
6use std::sync::mpsc::{channel, Sender};
7use std::sync::LazyLock;
8use std::time::Duration;
9
10static POOL: LazyLock<Mutex<Pool>> = LazyLock::new(|| Mutex::new(Pool::new()));
11
12static DROPPER: LazyLock<Sender<LuaConfigInner>> = LazyLock::new(|| {
13 let (tx, rx) = channel::<LuaConfigInner>();
14 std::thread::Builder::new()
15 .name("lua context dropper".to_string())
16 .spawn(move || {
17 for context in rx {
18 drop(context);
19 }
20 })
21 .expect("create lua context dropper thread");
22 tx
23});
24
25declare_metric! {
26static LUA_SPARE_COUNT: IntGauge("lua_spare_count");
28}
29
30static MAX_AGE: AtomicUsize = AtomicUsize::new(300);
32static MAX_USE: AtomicUsize = AtomicUsize::new(1024);
34static MAX_SPARE: AtomicUsize = AtomicUsize::new(8192);
36static GC_ON_PUT: AtomicUsize = AtomicUsize::new(0);
37
38pub fn set_max_use(max_use: usize) {
39 MAX_USE.store(max_use, Ordering::Relaxed);
40}
41
42pub fn set_max_spare(max_spare: usize) {
43 MAX_SPARE.store(max_spare, Ordering::Relaxed);
44}
45
46pub fn set_max_age(max_age: usize) {
47 MAX_AGE.store(max_age, Ordering::Relaxed);
48}
49
50pub fn set_gc_on_put(v: u8) {
52 GC_ON_PUT.store(v as usize, Ordering::Relaxed);
53}
54
55fn max_age() -> Duration {
56 Duration::from_secs(MAX_AGE.load(Ordering::Relaxed) as u64)
57}
58
59fn discard(config: LuaConfigInner) {
62 if let Err(err) = DROPPER.send(config) {
63 drop(err.0);
64 }
65}
66
67fn discard_all(contexts: Vec<LuaConfigInner>) {
68 for context in contexts {
69 discard(context);
70 }
71}
72
73#[derive(Default)]
74pub(crate) struct Pool {
75 pool: VecDeque<LuaConfigInner>,
76}
77
78impl Pool {
79 pub fn new() -> Self {
80 std::thread::Builder::new()
81 .name("config idler".to_string())
82 .spawn(|| loop {
83 std::thread::sleep(Duration::from_secs(30));
84 let expired = POOL.lock().expire();
85 discard_all(expired);
86 })
87 .expect("create config idler thread");
88 Self::default()
89 }
90
91 pub fn expire(&mut self) -> Vec<LuaConfigInner> {
95 let epoch = get_current_epoch();
96 let max_age = max_age();
97 let (keep, expired): (Vec<_>, Vec<_>) = std::mem::take(&mut self.pool)
98 .into_iter()
99 .partition(|inner| inner.created.elapsed() < max_age && inner.epoch == epoch);
100 self.pool = VecDeque::from(keep);
101 if !expired.is_empty() {
102 LUA_SPARE_COUNT.sub(expired.len() as i64);
103 }
104 expired
105 }
106
107 pub fn get(&mut self) -> (Option<LuaConfigInner>, Vec<LuaConfigInner>) {
111 let max_age = max_age();
112 let mut stale = vec![];
113 while let Some(mut item) = self.pool.pop_front() {
114 LUA_SPARE_COUNT.dec();
115 if item.created.elapsed() > max_age || item.epoch != get_current_epoch() {
116 stale.push(item);
117 continue;
118 }
119 item.use_count += 1;
120 return (Some(item), stale);
121 }
122 (None, stale)
123 }
124
125 pub fn put(&mut self, config: LuaConfigInner) -> Option<LuaConfigInner> {
129 if self.pool.len() + 1 > MAX_SPARE.load(Ordering::Relaxed) {
130 return Some(config);
131 }
132 self.pool.push_back(config);
133 LUA_SPARE_COUNT.inc();
134 None
135 }
136}
137
138pub(crate) fn pool_get() -> Option<LuaConfig> {
139 let (item, stale) = POOL.lock().get();
140 discard_all(stale);
141 item.map(|inner| LuaConfig { inner: Some(inner) })
142}
143
144pub(crate) fn pool_put(config: LuaConfigInner) {
145 let reusable = config.created.elapsed() <= max_age()
146 && config.use_count + 1 <= MAX_USE.load(Ordering::Relaxed);
147 if !reusable {
148 discard(config);
149 return;
150 }
151
152 let prob = GC_ON_PUT.load(Ordering::Relaxed);
154 if prob != 0 {
155 let chance = (rand::random::<f32>() * 100.) as usize;
156 if chance <= prob {
157 if let Err(err) = config.lua.gc_collect() {
158 tracing::error!("Error during gc: {err:#}");
159 discard(config);
160 return;
161 }
162 }
163 }
164
165 let mut pool = POOL.lock();
166 if config.epoch != get_current_epoch() {
170 drop(pool);
171 discard(config);
172 return;
173 }
174 if let Some(config) = pool.put(config) {
175 drop(pool);
176 discard(config);
177 }
178}
179
180#[cfg(test)]
181mod test {
182 use super::*;
183 use crate::epoch::bump_current_epoch;
184 use mlua::Lua;
185 use std::time::Instant;
186
187 fn new_inner() -> LuaConfigInner {
188 LuaConfigInner {
189 lua: Lua::new(),
190 created: Instant::now(),
191 use_count: 1,
192 epoch: get_current_epoch(),
193 }
194 }
195
196 #[test]
197 fn get_hands_back_contexts_from_an_earlier_epoch() {
198 let mut pool = Pool::default();
199 assert!(pool.put(new_inner()).is_none());
200 assert!(pool.put(new_inner()).is_none());
201
202 bump_current_epoch();
203
204 let (item, stale) = pool.get();
205 assert!(item.is_none());
206 k9::assert_equal!(stale.len(), 2);
207 k9::assert_equal!(pool.pool.len(), 0);
208 }
209
210 #[test]
211 fn expire_hands_back_contexts_from_an_earlier_epoch() {
212 let mut pool = Pool::default();
213 assert!(pool.put(new_inner()).is_none());
214
215 bump_current_epoch();
216 assert!(pool.put(new_inner()).is_none());
217
218 let expired = pool.expire();
219 k9::assert_equal!(expired.len(), 1);
220 k9::assert_equal!(pool.pool.len(), 1);
221 }
222
223 struct RestoreMaxSpare(usize);
227
228 impl Drop for RestoreMaxSpare {
229 fn drop(&mut self) {
230 set_max_spare(self.0);
231 }
232 }
233
234 #[test]
235 fn put_hands_back_the_context_when_the_pool_is_full() {
236 let _restore = RestoreMaxSpare(MAX_SPARE.load(Ordering::Relaxed));
237 set_max_spare(1);
238 let mut pool = Pool::default();
239 assert!(pool.put(new_inner()).is_none());
240 assert!(pool.put(new_inner()).is_some());
241 }
242}