Skip to content

Commit 08cd543

Browse files
Test Userclaude
andcommitted
perf(spawner): compile redaction regexes once via OnceLock
redact() and verify_redacted() recompiled all 7 DEFAULT_REDACTION_PATTERNS on every call via Regex::new(pattern). With MAX_CAPTURED_EVENTS = 4096 the unit test test_captured_events_bounded calls redact() 4,106 times -> 28,742 regex compilations and a >60s test warning. In production every spawned-agent output line flows through redact(), so this is a hot path. Cache the compiled regexes in a static OnceLock<Vec<Regex>>, initialised once on first use via compiled_patterns(). Both functions now iterate the cached slice. Replacement-closure logic (preserve prefix groups, redact last group) and the silent-drop-on-compile-error behaviour are preserved verbatim, so external behaviour is identical. std::sync::OnceLock is stable since Rust 1.70; workspace MSRV is 1.91, so no new dependency is required. Single-file change to redaction.rs only. This is a clean re-PR of the validated OnceLock core from #2967, which was blocked for branch contamination only (unrelated #2884 report artefact, #2941 AgentValidator tests, unused proptest lockfile churn, unsafe async env-var mutation). The core OnceLock fix was unanimously approved by pr-validator/pr-reviewer/pr-verifier. This branch carries only that fix, cut from pristine origin/main. Verified: - cargo test -p terraphim_spawner --lib redaction:: -> 11 passed, 0 failed - cargo clippy -p terraphim_spawner --lib -- -D warnings -> clean - cargo fmt -p terraphim_spawner -- --check -> clean - diff vs origin/main: 1 file, +49/-27, no Cargo.lock/Cargo.toml change Refs #2961 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 236d747 commit 08cd543

1 file changed

Lines changed: 49 additions & 27 deletions

File tree

crates/terraphim_spawner/src/redaction.rs

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
//! secrets, passwords) from agent stdout/stderr before storage or remote
55
//! posting.
66
7+
use std::sync::OnceLock;
8+
79
use regex::Regex;
810

911
/// Default patterns that indicate sensitive content in agent output.
@@ -28,36 +30,58 @@ pub const DEFAULT_REDACTION_PATTERNS: &[&str] = &[
2830
r"(?i)(bearer\s+)([a-zA-Z0-9_\-]{20,})",
2931
];
3032

33+
/// Lazily-compiled redaction regexes.
34+
///
35+
/// Compiling these patterns on every `redact()`/`verify_redacted()` call is
36+
/// expensive (issue #2961): the unit test `test_captured_events_bounded` calls
37+
/// `redact()` 4,106 times, recompiling 7 patterns each time (>28k compiles).
38+
/// In production every spawned-agent output line flows through `redact()`, so
39+
/// this is a hot path. The patterns are static constants, so we compile them
40+
/// exactly once via `OnceLock`.
41+
static REDACTION_REGEXES: OnceLock<Vec<Regex>> = OnceLock::new();
42+
43+
/// Return the compiled redaction regexes, initialising the cache on first use.
44+
///
45+
/// Mirrors the previous `if let Ok(re) = Regex::new(pattern)` behaviour: a
46+
/// pattern that fails to compile is silently omitted (all current patterns are
47+
/// valid literals, so this never drops anything in practice).
48+
fn compiled_patterns() -> &'static [Regex] {
49+
REDACTION_REGEXES.get_or_init(|| {
50+
DEFAULT_REDACTION_PATTERNS
51+
.iter()
52+
.filter_map(|p| Regex::new(p).ok())
53+
.collect()
54+
})
55+
}
56+
3157
/// Redact sensitive patterns from a string, replacing matches with `***REDACTED***`.
3258
///
3359
/// Preserves prefix capture groups (if any) and replaces only the last
3460
/// capture group (the secret value).
3561
pub fn redact(input: &str) -> String {
3662
let mut result = input.to_string();
3763

38-
for pattern in DEFAULT_REDACTION_PATTERNS {
39-
if let Ok(re) = Regex::new(pattern) {
40-
result = re
41-
.replace_all(&result, |caps: &regex::Captures| {
42-
let group_count = caps.len();
43-
44-
// No capture groups beyond the full match -> redact entire match
45-
if group_count <= 2 {
46-
return "***REDACTED***".to_string();
47-
}
48-
49-
// Preserve all capture groups except the last one
50-
let mut replacement = String::new();
51-
for i in 1..group_count - 1 {
52-
if let Some(m) = caps.get(i) {
53-
replacement.push_str(m.as_str());
54-
}
64+
for re in compiled_patterns() {
65+
result = re
66+
.replace_all(&result, |caps: &regex::Captures| {
67+
let group_count = caps.len();
68+
69+
// No capture groups beyond the full match -> redact entire match
70+
if group_count <= 2 {
71+
return "***REDACTED***".to_string();
72+
}
73+
74+
// Preserve all capture groups except the last one
75+
let mut replacement = String::new();
76+
for i in 1..group_count - 1 {
77+
if let Some(m) = caps.get(i) {
78+
replacement.push_str(m.as_str());
5579
}
56-
replacement.push_str("***REDACTED***");
57-
replacement
58-
})
59-
.to_string();
60-
}
80+
}
81+
replacement.push_str("***REDACTED***");
82+
replacement
83+
})
84+
.to_string();
6185
}
6286

6387
result
@@ -74,11 +98,9 @@ pub fn verify_redacted(input: &str) -> bool {
7498
return true;
7599
}
76100

77-
for pattern in DEFAULT_REDACTION_PATTERNS {
78-
if let Ok(re) = Regex::new(pattern) {
79-
if re.is_match(input) {
80-
return false;
81-
}
101+
for re in compiled_patterns() {
102+
if re.is_match(input) {
103+
return false;
82104
}
83105
}
84106
true

0 commit comments

Comments
 (0)