Skip to content

Commit eac1414

Browse files
committed
Trigger CI
1 parent dc76d1f commit eac1414

1 file changed

Lines changed: 54 additions & 28 deletions

File tree

libdd-crashtracker/src/receiver/ptrace_collector.rs

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,15 @@ fn attach_thread(tid: libc::pid_t, stop_deadline: Instant) -> Result<(), PtraceE
173173
// On older kernels, the register state may not be
174174
// immediately readable after waitpid reports the stop. Spin briefly
175175
// until PEEKUSER returns a non-zero IP, proving registers are committed.
176-
if !wait_for_registers(tid, stop_deadline) {
177-
let _ = detach_thread(tid);
178-
return Err(PtraceError::Attach(tid, libc::ETIMEDOUT));
179-
}
176+
//
177+
// If the deadline expires before we see a non-zero IP, proceed anyway:
178+
// libunwind uses PTRACE_GETREGSET which may succeed even when PEEKUSER
179+
// returns zero. If registers are truly uncommitted, unwind_remote_thread
180+
// will return 0 frames and capture_with_retry will retry without needing
181+
// a costly detach/re-attach cycle (which can fail with EPERM under CPU
182+
// pressure because the kernel hasn't fully released the prior ptrace
183+
// state).
184+
let _ = wait_for_registers(tid, stop_deadline);
180185

181186
Ok(())
182187
}
@@ -245,6 +250,14 @@ fn detach_thread(tid: libc::pid_t) -> Result<(), PtraceError> {
245250
return Err(PtraceError::Detach(tid, errno));
246251
}
247252
}
253+
254+
// Drain any pending waitpid event so the kernel fully releases the thread.
255+
// Without this, a rapid re-attach (PTRACE_SEIZE) can fail with EPERM under
256+
// CPU pressure because the kernel hasn't finished processing the detach.
257+
unsafe {
258+
libc::waitpid(tid, ptr::null_mut(), libc::__WALL | libc::WNOHANG);
259+
}
260+
248261
Ok(())
249262
}
250263

@@ -390,58 +403,71 @@ pub fn capture_thread_context(
390403
}
391404

392405
/// Maximum time to wait for a single thread to enter ptrace-stop.
393-
const STOP_TIMEOUT_PER_THREAD: Duration = Duration::from_millis(50);
406+
///
407+
/// 200ms accommodates CI environments with heavy CPU contention where the thread
408+
/// may take tens of milliseconds to be scheduled and enter ptrace-stop after
409+
/// PTRACE_INTERRUPT. On unloaded machines the stop typically arrives in <1ms.
410+
const STOP_TIMEOUT_PER_THREAD: Duration = Duration::from_millis(200);
394411

395-
/// Delay between retry attempts.
396-
const RETRY_DELAY: Duration = Duration::from_millis(5);
412+
/// Delay between retry attempts. Uses an exponential back-off starting from
413+
/// this value (10ms, 20ms, 40ms).
414+
const RETRY_BASE_DELAY: Duration = Duration::from_millis(10);
415+
416+
/// Maximum number of retry attempts per thread.
417+
const MAX_RETRIES: u32 = 3;
397418

398419
/// Returns true if err is worth retrying.
399420
///
400-
/// Only ETIMEDOUT (register-wait deadline expired before IP became readable)
401-
/// should be retired.
402-
///
403421
/// EPERM (Yama denial / missing PR_SET_PTRACER) and ESRCH (thread exited)
404422
/// are permanent for the receiver's lifetime and are not retried.
405423
fn is_transient_ptrace_error(err: &PtraceError) -> bool {
406424
matches!(err, PtraceError::Attach(_, libc::ETIMEDOUT))
407425
}
408426

409-
/// Attempt to capture a thread context, retrying once on transient failures.
427+
/// Attempt to capture a thread context, retrying on transient failures.
410428
///
411-
/// Uses a single per-thread deadline across attempts so that a slow thread
412-
/// cannot consume more than STOP_TIMEOUT_PER_THREAD total.
429+
/// Each attempt gets its own `STOP_TIMEOUT_PER_THREAD` budget (capped at the
430+
/// overall deadline) so that a retry after a timeout-induced failure actually
431+
/// has enough time to succeed. On older kernels (e.g. CentOS 7 / kernel 3.10)
432+
/// the first attempt can consume its entire budget waiting for registers to
433+
/// become readable; reusing that exhausted deadline would make the retry a
434+
/// no-op.
413435
///
414436
/// A capture that succeeds but produces zero frames is also retried: on a
415437
/// running thread with a confirmed non-zero IP, empty frames indicates a
416438
/// transient libunwind issue (e.g. stale address-space cache state) rather
417-
/// than a permanent problem.
439+
/// than a permanent problem. Retries use exponential back-off to give the
440+
/// kernel time to fully commit thread state between attempts.
418441
fn capture_with_retry(
419442
tid: libc::pid_t,
420443
resolve_frames: crate::StacktraceCollection,
421444
addr_space: UnwAddrSpaceT,
422445
overall_deadline: Instant,
423446
) -> Option<CapturedThreadContext> {
424-
let thread_deadline = (Instant::now() + STOP_TIMEOUT_PER_THREAD).min(overall_deadline);
447+
for attempt in 0..=MAX_RETRIES {
448+
let thread_deadline = (Instant::now() + STOP_TIMEOUT_PER_THREAD).min(overall_deadline);
425449

426-
let should_retry =
427450
match capture_thread_context(tid, resolve_frames, addr_space, thread_deadline) {
428451
Ok(ctx) if !ctx.stack_trace.frames.is_empty() => return Some(ctx),
429-
Ok(_) => true,
430-
Err(ref e) if is_transient_ptrace_error(e) => true,
431-
Err(_) => false,
432-
};
452+
Ok(_) => {} // 0 frames -- retry
453+
Err(ref e) if is_transient_ptrace_error(e) => {} // ETIMEDOUT -- retry
454+
Err(_) => return None, // permanent error
455+
}
433456

434-
if !should_retry {
435-
return None;
436-
}
457+
if attempt == MAX_RETRIES {
458+
break;
459+
}
437460

438-
let remaining = thread_deadline.saturating_duration_since(Instant::now());
439-
if remaining <= RETRY_DELAY {
440-
return None;
461+
let delay = RETRY_BASE_DELAY * 2u32.saturating_pow(attempt);
462+
if Instant::now() + delay >= overall_deadline {
463+
break;
464+
}
465+
std::thread::sleep(delay);
441466
}
442-
std::thread::sleep(RETRY_DELAY);
443467

444-
capture_thread_context(tid, resolve_frames, addr_space, thread_deadline).ok()
468+
// All attempts produced 0 frames or timed out; return None so the caller
469+
// records the thread with an incomplete stack rather than frames: [].
470+
None
445471
}
446472

447473
/// Stream thread contexts to a callback one at a time.

0 commit comments

Comments
 (0)