Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions libdd-shared-runtime/src/shared_runtime/fork_safe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,12 @@ impl ForkSafeRuntime {
let mut workers_lock = self.workers.lock_or_panic();

for worker_entry in workers_lock.iter_mut() {
worker_entry.worker.start(tokio_spawn_fn(&handle))?;
if let Err(e) = worker_entry.worker.start(tokio_spawn_fn(&handle)) {
error!(
worker_id = worker_entry.id,
"Worker failed to restart after fork in parent: {:?}", e
)
Comment on lines +97 to +101

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return restart failures after finishing the loop

When a worker is already in InvalidState, this branch logs the failed restart but still lets after_fork_parent return Ok(()); after_fork_child now follows the same pattern. In that scenario the Rust/FFI caller is told the fork hook succeeded even though at least one registered worker is no longer running, so please keep restarting the remaining workers but remember and return a SharedRuntimeError::WorkerError afterward.

AGENTS.md reference: AGENTS.md:L70-L73

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On one hand, this kinda the point of the PR. On another hand, I agree that we now hide information from the caller. Should we keep the first error indeed and return it at the end? Have Result<Vec<(WorkerId, Error)>, _> where we return the non-fatal errors encountered for some workers? Or something else?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of the silent failure either although there's almost nothing the tracer can do about this issue (worker id isn't meaningful outside of the runtime and you can't know which worker failed). Also It only happens if there's a bug in libdatadog so I don't want to make it to complex to handle on tracer side for this.

}
}

Ok(())
Expand All @@ -121,7 +126,12 @@ impl ForkSafeRuntime {

for worker_entry in workers_lock.iter_mut() {
worker_entry.worker.reset();
worker_entry.worker.start(tokio_spawn_fn(&handle))?;
if let Err(e) = worker_entry.worker.start(tokio_spawn_fn(&handle)) {
error!(
worker_id = worker_entry.id,
"Worker failed to restart after fork in parent: {:?}", e
)
}
}

Ok(())
Expand Down Expand Up @@ -421,4 +431,44 @@ mod tests {
"worker should not run or shut down after fork in child when restart_on_fork is false"
);
}

/// A single `PausableWorker` in `InvalidState` must
/// not abort the whole restart loop in `after_fork_parent`
#[test]
fn after_fork_parent_skips_invalid_state_workers() {
let runtime = ForkSafeRuntime::new().unwrap();

let (good, good_rx) = make_test_worker();
let _ = runtime.spawn_worker(good, true).unwrap();

// Second worker — we'll corrupt its entry into InvalidState below,
// simulating a previously-aborted task.
let (bad, _bad_rx) = make_test_worker();
let _ = runtime.spawn_worker(bad, true).unwrap();

good_rx
.recv_timeout(Duration::from_secs(1))
.expect("good worker did not run before fork");

{
let mut workers = runtime.workers.lock_or_panic();
workers[1].worker = PausableWorker::InvalidState;
}

runtime.before_fork();

// Drain good worker queue
while good_rx.try_recv().is_ok() {}

let result = runtime.after_fork_parent();

assert!(
result.is_ok(),
"after_fork_parent should not bail on a single InvalidState worker"
);
assert!(
good_rx.recv_timeout(Duration::from_secs(1)).is_ok(),
"good worker should resume after fork even if a peer is InvalidState"
);
}
}
Loading