From feec06bc41ac5eea631a85388ad4bc3c9739cde4 Mon Sep 17 00:00:00 2001 From: karl Date: Fri, 23 Oct 2026 09:49:17 +0200 Subject: [PATCH] Handle native Mac extraction worker exit races --- crates/searchi-core/src/extraction_process.rs | 39 ++++++++++--------- .../tests/parent_roundtrip.rs | 27 ++++++++----- docs/MACOS-RUNTIME.md | 5 +++ 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/crates/searchi-core/src/extraction_process.rs b/crates/searchi-core/src/extraction_process.rs index 3d83abd..3c66b25 100644 --- a/crates/searchi-core/src/extraction_process.rs +++ b/crates/searchi-core/src/extraction_process.rs @@ -50,29 +50,32 @@ enum Event { #[cfg(target_os = "macos")] fn check_resident_memory(child: &mut Child, maximum: u64) -> Result<(), ProcessError> { - let mut info = std::mem::MaybeUninit::::uninit(); let size = std::mem::size_of::(); - // 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 { + // Task info can disappear just before an exiting child becomes waitable. + // Retry that transition briefly; an alive uninspectable child fails closed. + for _ in 0..10 { + let mut info = std::mem::MaybeUninit::::uninit(); + 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 unsafe { info.assume_init() }.pti_resident_size > maximum { + return Err(failed("Extraction worker resident memory exceeds budget")); + } + return Ok(()); + } if child.try_wait().map_err(failed)?.is_some() { return Ok(()); } - return Err(failed("Cannot inspect extraction memory usage")); + thread::sleep(Duration::from_millis(1)); } - if unsafe { info.assume_init() }.pti_resident_size > maximum { - return Err(failed("Extraction worker resident memory exceeds budget")); - } - Ok(()) + Err(failed("Cannot inspect extraction memory usage")) } /// Includes pipe writes, response EOF and process exit in the same deadline. diff --git a/crates/searchi-extract-worker/tests/parent_roundtrip.rs b/crates/searchi-extract-worker/tests/parent_roundtrip.rs index aa3f090..7e7c830 100644 --- a/crates/searchi-extract-worker/tests/parent_roundtrip.rs +++ b/crates/searchi-extract-worker/tests/parent_roundtrip.rs @@ -30,15 +30,24 @@ fn actual_parent_and_worker_return_original_docx_passage_and_digest() { let input = document.finish().unwrap().into_inner(); let request = RequestHeader::from_bytes(DocumentFormat::Docx, &input).unwrap(); let expected_hash = request.input_sha256.clone(); - let response = run(&config(), request, input, &AtomicBool::new(false)).unwrap(); - assert_eq!(response.input_sha256, expected_hash); - let ExtractionOutcome::Extracted { passages } = response.outcome else { - panic!("Valid document rejected"); - }; - assert_eq!(passages.len(), 1); - assert_eq!(passages[0].text, "Cancel with sixty days written notice."); - assert_eq!(passages[0].source.kind, "paragraph"); - assert_eq!(passages[0].source.number, 1); + // Exercise short-lived native worker exit/task-inspection races too. + for _ in 0..10 { + let response = run( + &config(), + request.clone(), + input.clone(), + &AtomicBool::new(false), + ) + .unwrap(); + assert_eq!(response.input_sha256, expected_hash); + let ExtractionOutcome::Extracted { passages } = response.outcome else { + panic!("Valid document rejected"); + }; + assert_eq!(passages.len(), 1); + assert_eq!(passages[0].text, "Cancel with sixty days written notice."); + assert_eq!(passages[0].source.kind, "paragraph"); + assert_eq!(passages[0].source.number, 1); + } } #[test] diff --git a/docs/MACOS-RUNTIME.md b/docs/MACOS-RUNTIME.md index 8bc5762..bd489a7 100644 --- a/docs/MACOS-RUNTIME.md +++ b/docs/MACOS-RUNTIME.md @@ -114,3 +114,8 @@ 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. + +Resident-memory inspection also retries the short exit-to-waitable transition +before treating a live child as uninspectable. Ten fresh installed CLI/helper +DOCX runs returned the expected passage; the native roundtrip test repeats that +short-lived worker path ten times to cover the race.