Sends one extract request and reads the response, killing the worker process and returning [`RoundTripOutcome::Timeout`] if the read takes longer than `timeout`. The worker's child handle is `kill()`ed in place to unblock the read; the caller is expected to `spawn_worker` a fresh subprocess after either a timeout or a crash.
(
worker: &mut WorkerHandle,
req: &ExtractRequest,
timeout: Duration,
)
| 405 | /// to unblock the read; the caller is expected to `spawn_worker` a fresh |
| 406 | /// subprocess after either a timeout or a crash. |
| 407 | fn round_trip_with_timeout( |
| 408 | worker: &mut WorkerHandle, |
| 409 | req: &ExtractRequest, |
| 410 | timeout: Duration, |
| 411 | ) -> RoundTripOutcome { |
| 412 | let Some(stdin) = worker.stdin.as_mut() else { |
| 413 | return RoundTripOutcome::Err(io::Error::other("worker stdin already closed")); |
| 414 | }; |
| 415 | if let Err(e) = write_message(stdin, req).and_then(|()| stdin.flush()) { |
| 416 | return RoundTripOutcome::Err(e); |
| 417 | } |
| 418 | |
| 419 | // Split-borrow `stdout` (owned by the read thread) and `child` (owned by |
| 420 | // the watchdog thread). Rust allows this because they're disjoint fields |
| 421 | // of `*worker`. |
| 422 | let WorkerHandle { |
| 423 | ref mut stdout, |
| 424 | ref mut child, |
| 425 | .. |
| 426 | } = *worker; |
| 427 | |
| 428 | let timed_out = AtomicBool::new(false); |
| 429 | let (cancel_tx, cancel_rx) = std::sync::mpsc::channel::<()>(); |
| 430 | |
| 431 | let read_result: io::Result<ExtractResponse> = std::thread::scope(|s| { |
| 432 | // `move` the Receiver into the watchdog so it owns it (Receiver |
| 433 | // is `Send` but not `Sync`). `&timed_out` and `&mut *child` are |
| 434 | // borrowed from the outer scope under `'scope`. |
| 435 | let timed_out = &timed_out; |
| 436 | s.spawn(move || { |
| 437 | // Watchdog: if the read doesn't finish in `timeout`, kill the |
| 438 | // child so the read returns EOF and unblocks. The kill failing |
| 439 | // (child already exited) is fine — we just won't have a clean |
| 440 | // way to distinguish "crashed at exactly the wrong moment" from |
| 441 | // "timed out", and that's OK; both cases get respawned. |
| 442 | if cancel_rx.recv_timeout(timeout).is_err() { |
| 443 | timed_out.store(true, Ordering::SeqCst); |
| 444 | let _ = child.kill(); |
| 445 | } |
| 446 | }); |
| 447 | let r = read_message(stdout); |
| 448 | let _ = cancel_tx.send(()); |
| 449 | r |
| 450 | }); |
| 451 | |
| 452 | if timed_out.load(Ordering::SeqCst) { |
| 453 | RoundTripOutcome::Timeout |
| 454 | } else { |
| 455 | match read_result { |
| 456 | Ok(resp) => RoundTripOutcome::Ok(resp), |
| 457 | Err(e) => RoundTripOutcome::Err(e), |
| 458 | } |
| 459 | } |
| 460 | } |
| 461 | |
| 462 | fn spawn_worker(self_path: &Path, token: &[u8; TOKEN_LEN]) -> io::Result<WorkerHandle> { |
| 463 | let token_hex = hex::encode(token); |
no test coverage detected