@@ -392,56 +392,68 @@ pub fn capture_thread_context(
392392/// Maximum time to wait for a single thread to enter ptrace-stop.
393393const STOP_TIMEOUT_PER_THREAD : Duration = Duration :: from_millis ( 50 ) ;
394394
395- /// Delay between retry attempts.
396- const RETRY_DELAY : Duration = Duration :: from_millis ( 5 ) ;
395+ /// Delay between retry attempts. Uses an exponential back-off starting from
396+ /// this value (10ms, 20ms, 40ms).
397+ const RETRY_BASE_DELAY : Duration = Duration :: from_millis ( 10 ) ;
398+
399+ /// Maximum number of retry attempts per thread.
400+ const MAX_RETRIES : u32 = 3 ;
397401
398402/// Returns true if err is worth retrying.
399403///
400- /// Only ETIMEDOUT (register-wait deadline expired before IP became readable)
401- /// should be retired .
404+ /// ETIMEDOUT (register-wait deadline expired before IP became readable) and
405+ /// zero-frame captures are transient on older kernels and should be retried .
402406///
403407/// EPERM (Yama denial / missing PR_SET_PTRACER) and ESRCH (thread exited)
404408/// are permanent for the receiver's lifetime and are not retried.
405409fn is_transient_ptrace_error ( err : & PtraceError ) -> bool {
406410 matches ! ( err, PtraceError :: Attach ( _, libc:: ETIMEDOUT ) )
407411}
408412
409- /// Attempt to capture a thread context, retrying once on transient failures.
413+ /// Attempt to capture a thread context, retrying on transient failures.
410414///
411- /// Uses a single per-thread deadline across attempts so that a slow thread
412- /// cannot consume more than STOP_TIMEOUT_PER_THREAD total.
415+ /// Each attempt gets its own `STOP_TIMEOUT_PER_THREAD` budget (capped at the
416+ /// overall deadline) so that a retry after a timeout-induced failure actually
417+ /// has enough time to succeed. On older kernels (e.g. CentOS 7 / kernel 3.10)
418+ /// the first attempt can consume its entire budget waiting for registers to
419+ /// become readable; reusing that exhausted deadline would make the retry a
420+ /// no-op.
413421///
414422/// A capture that succeeds but produces zero frames is also retried: on a
415423/// running thread with a confirmed non-zero IP, empty frames indicates a
416424/// transient libunwind issue (e.g. stale address-space cache state) rather
417- /// than a permanent problem.
425+ /// than a permanent problem. Retries use exponential back-off to give the
426+ /// kernel time to fully commit thread state between attempts.
418427fn capture_with_retry (
419428 tid : libc:: pid_t ,
420429 resolve_frames : crate :: StacktraceCollection ,
421430 addr_space : UnwAddrSpaceT ,
422431 overall_deadline : Instant ,
423432) -> Option < CapturedThreadContext > {
424- let thread_deadline = ( Instant :: now ( ) + STOP_TIMEOUT_PER_THREAD ) . min ( overall_deadline) ;
433+ for attempt in 0 ..=MAX_RETRIES {
434+ let thread_deadline = ( Instant :: now ( ) + STOP_TIMEOUT_PER_THREAD ) . min ( overall_deadline) ;
425435
426- let should_retry =
427436 match capture_thread_context ( tid, resolve_frames, addr_space, thread_deadline) {
428437 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- } ;
438+ Ok ( _) => { } // 0 frames -- retry
439+ Err ( ref e) if is_transient_ptrace_error ( e) => { } // ETIMEDOUT -- retry
440+ Err ( _) => return None , // permanent error
441+ }
433442
434- if !should_retry {
435- return None ;
436- }
443+ if attempt == MAX_RETRIES {
444+ break ;
445+ }
437446
438- let remaining = thread_deadline. saturating_duration_since ( Instant :: now ( ) ) ;
439- if remaining <= RETRY_DELAY {
440- return None ;
447+ let delay = RETRY_BASE_DELAY * 2u32 . saturating_pow ( attempt) ;
448+ if Instant :: now ( ) + delay >= overall_deadline {
449+ break ;
450+ }
451+ std:: thread:: sleep ( delay) ;
441452 }
442- std:: thread:: sleep ( RETRY_DELAY ) ;
443453
444- capture_thread_context ( tid, resolve_frames, addr_space, thread_deadline) . ok ( )
454+ // All attempts produced 0 frames or timed out; return None so the caller
455+ // records the thread with an incomplete stack rather than frames: [].
456+ None
445457}
446458
447459/// Stream thread contexts to a callback one at a time.
0 commit comments