use super::HsScanner;
use hyperscan::{Matching, Scratch};
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::{Arc, Weak};
struct CachedScratch {
owner: Weak<()>,
scratch: Scratch,
}
const SCRATCH_TLS_PRUNE_THRESHOLD: usize = 32;
thread_local! {
static SCRATCH_TLS: RefCell<HashMap<(u64, usize), CachedScratch>> =
RefCell::new(HashMap::new());
}
fn take_scratch(
scanner_id: u64,
shard_idx: usize,
shard: &super::Shard,
owner: &Arc<()>,
) -> Result<Scratch, String> {
let key = (scanner_id, shard_idx);
if let Some(scratch) =
SCRATCH_TLS.with(|tls| tls.borrow_mut().remove(&key).map(|cached| cached.scratch))
{
return Ok(scratch);
}
debug_assert!(Arc::strong_count(owner) > 0);
SCRATCH_TLS.with(|tls| prune_dead_scanner_scratch(&mut tls.borrow_mut()));
if let Some(scratch) = shard.scratch_pool.lock().pop() {
return Ok(scratch);
}
shard.db.alloc_scratch().map_err(|error| {
format!(
"hyperscan scratch on-demand growth failed for scanner {scanner_id} \
shard {shard_idx}: {error}"
)
})
}
fn put_scratch(scanner_id: u64, shard_idx: usize, owner: &Arc<()>, scratch: Scratch) {
let key = (scanner_id, shard_idx);
SCRATCH_TLS.with(|tls| {
let mut tls = tls.borrow_mut();
if tls.len() >= SCRATCH_TLS_PRUNE_THRESHOLD {
prune_dead_scanner_scratch(&mut tls);
}
tls.insert(
key,
CachedScratch {
owner: Arc::downgrade(owner),
scratch,
},
);
});
}
fn prune_dead_scanner_scratch(tls: &mut HashMap<(u64, usize), CachedScratch>) {
tls.retain(|_, cached| cached.owner.strong_count() > 0);
}
pub(super) fn purge_scanner_scratch(scanner_id: u64) {
SCRATCH_TLS.with(|tls| {
tls.borrow_mut()
.retain(|(cached_scanner_id, _), _| *cached_scanner_id != scanner_id);
});
}
#[cfg(test)]
fn current_thread_scratch_count_for_test(scanner_id: u64) -> usize {
SCRATCH_TLS.with(|tls| {
tls.borrow()
.keys()
.filter(|(cached_scanner_id, _)| *cached_scanner_id == scanner_id)
.count()
})
}
impl HsScanner {
pub(crate) fn scan_matches_result(
&self,
text: &[u8],
mut on_match: impl FnMut(usize, usize, usize),
) -> Result<(), String> {
for (shard_idx, shard) in self.shards.iter().enumerate() {
let scratch = take_scratch(self.scanner_id, shard_idx, shard, &self.scratch_owner)?;
if let Err(error) = shard.db.scan(text, &scratch, |id, from, to, _flags| {
on_match(id as usize, from as usize, to as usize);
Matching::Continue
}) {
put_scratch(self.scanner_id, shard_idx, &self.scratch_owner, scratch);
return Err(format!(
"hyperscan scan failed for shard {shard_idx}: {error}"
));
}
put_scratch(self.scanner_id, shard_idx, &self.scratch_owner, scratch);
}
Ok(())
}
pub(crate) fn scan_each_result(
&self,
text: &[u8],
mut on_match: impl FnMut(usize),
) -> Result<(), String> {
for (shard_idx, shard) in self.shards.iter().enumerate() {
let scratch = take_scratch(self.scanner_id, shard_idx, shard, &self.scratch_owner)?;
if let Err(error) = shard.db.scan(text, &scratch, |id, _from, _to, _flags| {
on_match(id as usize);
Matching::Continue
}) {
put_scratch(self.scanner_id, shard_idx, &self.scratch_owner, scratch);
return Err(format!(
"hyperscan scan_each failed for shard {shard_idx}: {error}"
));
}
put_scratch(self.scanner_id, shard_idx, &self.scratch_owner, scratch);
}
Ok(())
}
pub(crate) fn any_match_result(&self, text: &[u8]) -> Result<bool, String> {
for (shard_idx, shard) in self.shards.iter().enumerate() {
let scratch = take_scratch(self.scanner_id, shard_idx, shard, &self.scratch_owner)?;
let mut hit = false;
if let Err(error) = shard.db.scan(text, &scratch, |_id, _from, _to, _flags| {
hit = true;
Matching::Terminate
}) {
if !hit {
put_scratch(self.scanner_id, shard_idx, &self.scratch_owner, scratch);
return Err(format!(
"hyperscan any_match failed before a match was observed for shard {shard_idx}: {error}"
));
}
}
put_scratch(self.scanner_id, shard_idx, &self.scratch_owner, scratch);
if hit {
return Ok(true);
}
}
Ok(false)
}
pub(crate) fn pattern_info(&self, hs_id: usize) -> Option<(usize, usize, bool)> {
self.pattern_map
.get(hs_id)
.map(|&(_, det_idx, pat_idx, has_group)| (det_idx, pat_idx, has_group))
}
pub(crate) fn pattern_count(&self) -> usize {
self.pattern_map.len()
}
}
#[cfg(test)]
mod scratch_lifetime {
use super::super::HsScanner;
#[test]
fn dropping_scanner_purges_current_thread_tls_scratch() {
let patterns = [(0usize, 0usize, "KHDROP_[A-Z0-9]{8}", false)];
let (scanner, unsupported) = HsScanner::compile(&patterns).expect("probe pattern compiles");
assert!(
unsupported.is_empty(),
"probe pattern must be Hyperscan-supported, got unsupported={unsupported:?}"
);
let scanner_id = scanner.scanner_id;
let mut ids = Vec::new();
scanner
.scan_matches_result(b"KHDROP_AB12CD34", |id, _start, _end| ids.push(id))
.expect("scan succeeds and retains scratch in this thread");
assert_eq!(ids, vec![0]);
assert!(
super::current_thread_scratch_count_for_test(scanner_id) > 0,
"scan should retain at least one scratch for the live scanner"
);
drop(scanner);
assert_eq!(
super::current_thread_scratch_count_for_test(scanner_id),
0,
"dropping a scanner must evict its thread-local Hyperscan scratches"
);
}
#[test]
fn interleaved_live_scanners_keep_thread_local_scratches() {
let patterns_a = [(0usize, 0usize, "KHA_[A-Z0-9]{8}", false)];
let patterns_b = [(0usize, 0usize, "KHB_[A-Z0-9]{8}", false)];
let (scanner_a, unsupported_a) =
HsScanner::compile(&patterns_a).expect("scanner A pattern compiles");
let (scanner_b, unsupported_b) =
HsScanner::compile(&patterns_b).expect("scanner B pattern compiles");
assert!(
unsupported_a.is_empty() && unsupported_b.is_empty(),
"probe patterns must be Hyperscan-supported"
);
scanner_a
.scan_matches_result(b"KHA_AB12CD34", |_, _, _| {})
.expect("scanner A scan succeeds");
assert!(
super::current_thread_scratch_count_for_test(scanner_a.scanner_id) > 0,
"scanner A should retain its current-thread scratch"
);
scanner_b
.scan_matches_result(b"KHB_AB12CD34", |_, _, _| {})
.expect("scanner B scan succeeds");
assert!(
super::current_thread_scratch_count_for_test(scanner_a.scanner_id) > 0,
"interleaving scanner B must not evict live scanner A scratch"
);
assert!(
super::current_thread_scratch_count_for_test(scanner_b.scanner_id) > 0,
"scanner B should retain its own current-thread scratch"
);
}
#[test]
fn dead_scanner_scratch_is_pruned_on_worker_next_cache_touch() {
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
let (continue_tx, continue_rx) = std::sync::mpsc::channel();
let worker = std::thread::spawn(move || {
let patterns_a = [(0usize, 0usize, "KHSTALE_[A-Z0-9]{8}", false)];
let (scanner_a, unsupported_a) =
HsScanner::compile(&patterns_a).expect("scanner A pattern compiles");
assert!(
unsupported_a.is_empty(),
"scanner A pattern must be Hyperscan-supported"
);
let scanner_a_id = scanner_a.scanner_id;
scanner_a
.scan_matches_result(b"KHSTALE_AB12CD34", |_, _, _| {})
.expect("scanner A scan succeeds");
ready_tx
.send((
scanner_a_id,
super::current_thread_scratch_count_for_test(scanner_a_id),
scanner_a,
))
.expect("send scanner A cache count");
continue_rx.recv().expect("wait for prune command");
let patterns_b = [(0usize, 0usize, "KHFRESH_[A-Z0-9]{8}", false)];
let (scanner_b, unsupported_b) =
HsScanner::compile(&patterns_b).expect("scanner B pattern compiles");
assert!(
unsupported_b.is_empty(),
"scanner B pattern must be Hyperscan-supported"
);
scanner_b
.scan_matches_result(b"KHFRESH_AB12CD34", |_, _, _| {})
.expect("scanner B scan succeeds and prunes stale entries on miss");
(
super::current_thread_scratch_count_for_test(scanner_a_id),
super::current_thread_scratch_count_for_test(scanner_b.scanner_id),
)
});
let (_scanner_a_id, cached_before_drop, scanner_a) =
ready_rx.recv().expect("receive scanner A cache count");
assert!(
cached_before_drop > 0,
"worker should retain scanner A scratch before scanner A is dropped"
);
drop(scanner_a);
continue_tx.send(()).expect("release worker");
let (stale_after_touch, fresh_after_touch) = worker.join().expect("worker joins");
assert_eq!(
stale_after_touch, 0,
"dead scanner A scratch must be pruned on the worker's next cache touch"
);
assert!(
fresh_after_touch > 0,
"worker should retain scanner B scratch after pruning stale scanner A"
);
}
}