Keep isolated document extraction working on macOS
This commit is contained in:
1 parent
5b93dd67a7
commit
dd589e88ae
3 files changed
+60
No files matched your search
@@ -48,6 +48,33 @@ enum Event {
|
|||||||
Stderr,
|
Stderr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn check_resident_memory(child: &mut Child, maximum: u64) -> Result<(), ProcessError> {
|
||||||
|
let mut info = std::mem::MaybeUninit::<libc::proc_taskinfo>::uninit();
|
||||||
|
let size = std::mem::size_of::<libc::proc_taskinfo>();
|
||||||
|
// The child stays owned and unreaped until I/O completes. A failed query is
|
||||||
|
// accepted only when it has already exited; otherwise fail closed.
|
||||||
|
let read = unsafe {
|
||||||
|
libc::proc_pidinfo(
|
||||||
|
child.id() as i32,
|
||||||
|
libc::PROC_PIDTASKINFO,
|
||||||
|
0,
|
||||||
|
info.as_mut_ptr().cast(),
|
||||||
|
size as i32,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if read != size as i32 {
|
||||||
|
if child.try_wait().map_err(failed)?.is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
return Err(failed("Cannot inspect extraction memory usage"));
|
||||||
|
}
|
||||||
|
if unsafe { info.assume_init() }.pti_resident_size > maximum {
|
||||||
|
return Err(failed("Extraction worker resident memory exceeds budget"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Includes pipe writes, response EOF and process exit in the same deadline.
|
/// Includes pipe writes, response EOF and process exit in the same deadline.
|
||||||
/// No parser fallback occurs when limits, transport or worker execution fail.
|
/// No parser fallback occurs when limits, transport or worker execution fail.
|
||||||
pub fn run(
|
pub fn run(
|
||||||
@@ -161,6 +188,8 @@ pub fn run(
|
|||||||
if started.elapsed() >= config.timeout {
|
if started.elapsed() >= config.timeout {
|
||||||
return Err(ProcessError::TimedOut);
|
return Err(ProcessError::TimedOut);
|
||||||
}
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
check_resident_memory(&mut owned.child, 512 * 1024 * 1024)?;
|
||||||
match rx.recv_timeout(
|
match rx.recv_timeout(
|
||||||
Duration::from_millis(5).min(config.timeout.saturating_sub(started.elapsed())),
|
Duration::from_millis(5).min(config.timeout.saturating_sub(started.elapsed())),
|
||||||
) {
|
) {
|
||||||
@@ -288,6 +317,20 @@ mod tests {
|
|||||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[test]
|
||||||
|
fn mac_memory_budget_rejects_a_live_owned_child() {
|
||||||
|
let mut child = Command::new("/bin/sleep").arg("30").spawn().unwrap();
|
||||||
|
// Wait for exec before measuring, then impose a deliberately tiny budget.
|
||||||
|
thread::sleep(Duration::from_millis(50));
|
||||||
|
let result = check_resident_memory(&mut child, 1);
|
||||||
|
let _ = child.kill();
|
||||||
|
child.wait().unwrap();
|
||||||
|
assert!(
|
||||||
|
matches!(result, Err(ProcessError::Failed(message)) if message.contains("exceeds budget"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn fixtures() -> tempfile::TempDir {
|
fn fixtures() -> tempfile::TempDir {
|
||||||
let directory = tempfile::tempdir().unwrap();
|
let directory = tempfile::tempdir().unwrap();
|
||||||
let source = directory.path().join("fake.rs");
|
let source = directory.path().join("fake.rs");
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use anyhow::{ensure, Result};
|
use anyhow::{ensure, Result};
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
const MEMORY_BYTES: usize = 512 * 1024 * 1024;
|
const MEMORY_BYTES: usize = 512 * 1024 * 1024;
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@@ -37,7 +38,10 @@ pub fn apply() -> Result<()> {
|
|||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pub fn apply() -> Result<()> {
|
pub fn apply() -> Result<()> {
|
||||||
|
// Darwin rejects finite AS/DATA limits. The parent enforces a sampled RSS
|
||||||
|
// budget there; retain the CPU limit and bounded path-free transport here.
|
||||||
for (resource, maximum) in [
|
for (resource, maximum) in [
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
(libc::RLIMIT_AS, MEMORY_BYTES as libc::rlim_t),
|
(libc::RLIMIT_AS, MEMORY_BYTES as libc::rlim_t),
|
||||||
(libc::RLIMIT_CPU, 30 as libc::rlim_t),
|
(libc::RLIMIT_CPU, 30 as libc::rlim_t),
|
||||||
] {
|
] {
|
||||||
|
|||||||
@@ -101,3 +101,16 @@ The broader workspace run exposes an existing stale permission-denial rollback
|
|||||||
expectation in `targeted_reconciliation` (current engine skips unreadable inputs).
|
expectation in `targeted_reconciliation` (current engine skips unreadable inputs).
|
||||||
Strict current-toolchain Clippy also flags unchanged permission-literal and
|
Strict current-toolchain Clippy also flags unchanged permission-literal and
|
||||||
ANN-cache boolean expressions. These failures are not qualified as passing gates.
|
ANN-cache boolean expressions. These failures are not qualified as passing gates.
|
||||||
|
|
||||||
|
### Native document extraction policy
|
||||||
|
|
||||||
|
Mac parent/worker DOCX roundtrip and malformed-PDF rejection tests now pass.
|
||||||
|
Darwin rejects finite `RLIMIT_AS` and `RLIMIT_DATA` on the tested system. The
|
||||||
|
worker retains the30-second CPU limit; the parent checks resident memory through
|
||||||
|
`proc_pidinfo` during its existing5ms polling loop and rejects usage above512MiB,
|
||||||
|
killing/reaping its owned process group. Input/output bounds and the parent
|
||||||
|
wall deadline remain active. This sampled RSS guard may overshoot between polls;
|
||||||
|
it is not equivalent to a kernel-enforced hard address-space limit. Linux and
|
||||||
|
Windows retain their existing resource policies. An owned live-child test
|
||||||
|
verifies budget rejection; five process-owner tests and two actual parent/worker
|
||||||
|
roundtrip/rejection tests pass on the Mac.
|
||||||
Reference in new issue
Block a user