fix(executor-core): F-P-014 thread-local LRU regex cache (128 entries)
regex::is_match, find, find_all, replace, replace_all, split, captures
all called Regex::new(pattern) per invocation. A script doing
`regex::is_match("\\d+", x)` in a tight loop paid the compile every
iteration. Regex compile dominates is_match on short strings.
Add a thread-local LruCache<String, Arc<Regex>> (cap 128). The
compile helper now memoises by pattern string, returning Arc<Regex>
on hit. Cap is well above what any sensible script uses but bounded
enough to keep memory predictable on long-running threads.
AUDIT.md anchor: F-P-014.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,29 @@
|
||||
//! `regex::` — non-backtracking regular expressions (Rust `regex` crate).
|
||||
//!
|
||||
//! Patterns compile per call. No cache: premature for v1.1.0, and the
|
||||
//! `regex` crate's linear-time guarantees keep per-call cost bounded.
|
||||
//! Catastrophic patterns are rejected at compile time by the crate
|
||||
//! itself; no extra defense needed.
|
||||
//! F-P-014: compiled patterns are cached in a thread-local LRU so a
|
||||
//! tight script loop like `regex::is_match("\\d+", x)` doesn't recompile
|
||||
//! every iteration. Compile dominates `is_match` on short strings;
|
||||
//! caching shrinks per-call cost to a HashMap probe + `Arc::clone`.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use lru::LruCache;
|
||||
use regex::Regex;
|
||||
use rhai::{Array, Dynamic, Engine as RhaiEngine, EvalAltResult, Module};
|
||||
|
||||
/// Per-thread cap on compiled regex patterns. 128 is well above what
|
||||
/// any sensible script uses but bounded enough to keep memory in
|
||||
/// check on a fleet of long-running threads.
|
||||
const REGEX_CACHE_SIZE: usize = 128;
|
||||
|
||||
thread_local! {
|
||||
static REGEX_CACHE: RefCell<LruCache<String, Arc<Regex>>> = RefCell::new(
|
||||
LruCache::new(NonZeroUsize::new(REGEX_CACHE_SIZE).expect("non-zero"))
|
||||
);
|
||||
}
|
||||
|
||||
pub fn register(engine: &mut RhaiEngine) {
|
||||
let mut module = Module::new();
|
||||
register_is_match(&mut module);
|
||||
@@ -20,8 +36,18 @@ pub fn register(engine: &mut RhaiEngine) {
|
||||
engine.register_static_module("regex", module.into());
|
||||
}
|
||||
|
||||
fn compile(pattern: &str) -> Result<Regex, Box<EvalAltResult>> {
|
||||
Regex::new(pattern).map_err(|e| format!("invalid regex: {e}").into())
|
||||
fn compile(pattern: &str) -> Result<Arc<Regex>, Box<EvalAltResult>> {
|
||||
REGEX_CACHE.with(|cell| {
|
||||
if let Some(cached) = cell.borrow_mut().get(pattern) {
|
||||
return Ok(cached.clone());
|
||||
}
|
||||
let re = Arc::new(
|
||||
Regex::new(pattern)
|
||||
.map_err(|e| -> Box<EvalAltResult> { format!("invalid regex: {e}").into() })?,
|
||||
);
|
||||
cell.borrow_mut().put(pattern.to_string(), re.clone());
|
||||
Ok(re)
|
||||
})
|
||||
}
|
||||
|
||||
fn register_is_match(module: &mut Module) {
|
||||
|
||||
Reference in New Issue
Block a user