From 931635d9269c0acbd7831e2ea7bb0c4d15cc9428 Mon Sep 17 00:00:00 2001 From: F4RAN Date: Fri, 12 Dec 2025 22:28:00 +0330 Subject: [PATCH 1/8] io: always cleanup AsyncFd registration list on deregister Fixes memory leak when fd is closed before AsyncFd drop. Fixes: #7563 --- tokio/src/runtime/io/driver.rs | 6 ++++- tokio/tests/io_async_fd.rs | 46 ++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/tokio/src/runtime/io/driver.rs b/tokio/src/runtime/io/driver.rs index 04540cf2b13..d1d5c7d8bfb 100644 --- a/tokio/src/runtime/io/driver.rs +++ b/tokio/src/runtime/io/driver.rs @@ -296,7 +296,9 @@ impl Handle { source: &mut impl Source, ) -> io::Result<()> { // Deregister the source with the OS poller **first** - self.registry.deregister(source)?; + // Cleanup ALWAYS happens + let os_result = self.registry.deregister(source); + if self .registrations @@ -307,6 +309,8 @@ impl Handle { self.metrics.dec_fd_count(); + os_result?; // Return error after cleanup + Ok(()) } diff --git a/tokio/tests/io_async_fd.rs b/tokio/tests/io_async_fd.rs index c9a7302d3fc..61ed10c26a9 100644 --- a/tokio/tests/io_async_fd.rs +++ b/tokio/tests/io_async_fd.rs @@ -956,3 +956,49 @@ async fn try_with_interest() { assert!(Arc::ptr_eq(&original, &returned)); } + +#[tokio::test] +async fn memory_leak_when_fd_closed_before_drop() { + use std::os::unix::io::{AsRawFd, RawFd}; + use std::sync::Arc; + use std::time::Duration; + use tokio::io::unix::AsyncFd; + + use nix::sys::socket::{self, AddressFamily, SockFlag, SockType}; + + // Wrapper that just holds a raw fd number + // This allows us to close it manually + struct RawFdWrapper { + fd: RawFd, + } + + impl AsRawFd for RawFdWrapper { + fn as_raw_fd(&self) -> RawFd { + self.fd + } + } + + for _ in 0..100 { + // Create a socket pair (works on both Linux and macOS) + let (fd_a, _fd_b) = socket::socketpair( + AddressFamily::Unix, + SockType::Stream, + None, + SockFlag::empty(), + ) + .expect("socketpair"); + let raw_fd = fd_a.as_raw_fd(); + set_nonblocking(raw_fd); + std::mem::forget(fd_a); + let fd_wrapper = RawFdWrapper { fd: raw_fd }; + let async_fd = AsyncFd::new(ArcFd(Arc::new(fd_wrapper))).unwrap(); + unsafe { + libc::close(raw_fd); + } + tokio::time::sleep(Duration::from_millis(1)).await; + // Now drop AsyncFd - deregister will fail, but cleanup should still happen + drop(async_fd); + } + + // If we get here without leaking memory, the test passes +} From 3d7d87db0350bc8892cd305ff4c4289dc7729531 Mon Sep 17 00:00:00 2001 From: F4RAN Date: Fri, 12 Dec 2025 22:30:43 +0330 Subject: [PATCH 2/8] fix: formatter issues --- tokio/src/runtime/io/driver.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tokio/src/runtime/io/driver.rs b/tokio/src/runtime/io/driver.rs index d1d5c7d8bfb..61b1ca805cd 100644 --- a/tokio/src/runtime/io/driver.rs +++ b/tokio/src/runtime/io/driver.rs @@ -298,7 +298,6 @@ impl Handle { // Deregister the source with the OS poller **first** // Cleanup ALWAYS happens let os_result = self.registry.deregister(source); - if self .registrations From b6452c7e4bc58275f4576aab348f7811b5223446 Mon Sep 17 00:00:00 2001 From: F4RAN Date: Sat, 13 Dec 2025 13:00:06 +0330 Subject: [PATCH 3/8] test: in linux environment --- tokio/src/runtime/handle.rs | 24 ++++++ tokio/src/runtime/io/driver.rs | 22 ++++- tokio/src/runtime/io/registration_set.rs | 27 ++++++ tokio/tests/io_async_fd.rs | 100 ++++++++++++++++++----- 4 files changed, 149 insertions(+), 24 deletions(-) diff --git a/tokio/src/runtime/handle.rs b/tokio/src/runtime/handle.rs index 5030adc6d92..c5899881569 100644 --- a/tokio/src/runtime/handle.rs +++ b/tokio/src/runtime/handle.rs @@ -488,10 +488,34 @@ impl Handle { } } +/// TEST PURPOSE RELATED TO PR #7773 +#[cfg(feature = "full")] +impl Handle { + /// Returns a reference to the IO driver handle (test-only, not part of public API) + #[doc(hidden)] + pub(crate) fn io_driver(&self) -> &crate::runtime::io::Handle { + self.inner.driver().io() + } + + /// Returns the number of pending registrations (test-only, not part of public API) + #[doc(hidden)] + pub fn io_pending_registration_count(&self) -> usize { + self.inner.driver().io().pending_registration_count() + } + + /// Returns the total number of registrations in the main list (test-only, not part of public API) + #[doc(hidden)] + pub fn io_total_registration_count(&self) -> usize { + self.inner.driver().io().total_registration_count() + } +} + impl std::panic::UnwindSafe for Handle {} impl std::panic::RefUnwindSafe for Handle {} + + cfg_taskdump! { impl Handle { /// Captures a snapshot of the runtime's state. diff --git a/tokio/src/runtime/io/driver.rs b/tokio/src/runtime/io/driver.rs index 61b1ca805cd..c802a256718 100644 --- a/tokio/src/runtime/io/driver.rs +++ b/tokio/src/runtime/io/driver.rs @@ -297,7 +297,8 @@ impl Handle { ) -> io::Result<()> { // Deregister the source with the OS poller **first** // Cleanup ALWAYS happens - let os_result = self.registry.deregister(source); + // let os_result = self.registry.deregister(source); + self.registry.deregister(source)?; if self .registrations @@ -308,7 +309,7 @@ impl Handle { self.metrics.dec_fd_count(); - os_result?; // Return error after cleanup + // os_result?; // Return error after cleanup Ok(()) } @@ -320,6 +321,23 @@ impl Handle { } } + +/// TEST PURPOSE RELATED TO PR #7773 +#[cfg(feature = "full")] +impl Handle { + /// Returns the number of pending registrations (test-only, not part of public API) + #[doc(hidden)] + pub fn pending_registration_count(&self) -> usize { + self.registrations.pending_release_count() + } + /// Returns the total number of registrations in the main list (test-only) + #[doc(hidden)] + pub fn total_registration_count(&self) -> usize { + self.registrations.total_registration_count(&mut self.synced.lock()) + } +} + + impl fmt::Debug for Handle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Handle") diff --git a/tokio/src/runtime/io/registration_set.rs b/tokio/src/runtime/io/registration_set.rs index 2796796de93..55c03257f0d 100644 --- a/tokio/src/runtime/io/registration_set.rs +++ b/tokio/src/runtime/io/registration_set.rs @@ -53,6 +53,33 @@ impl RegistrationSet { self.num_pending_release.load(Acquire) != 0 } + /// TEST PURPOSE RELATED TO PR #7773 + #[cfg(feature = "full")] + pub(super) fn pending_release_count(&self) -> usize { + self.num_pending_release.load(Acquire) + } + /// TEST PURPOSE RELATED TO PR #7773 + #[cfg(feature = "full")] + pub(super) fn total_registration_count(&self, synced: &mut Synced) -> usize { + // Count by temporarily draining the list, then restoring it + // This is safe for test purposes + let mut items = Vec::new(); + + // Drain all items + while let Some(item) = synced.registrations.pop_back() { + items.push(item); + } + + let count = items.len(); + + // Restore items in reverse order (since we popped from back) + for item in items.into_iter().rev() { + synced.registrations.push_front(item); + } + + count + } + pub(super) fn allocate(&self, synced: &mut Synced) -> io::Result> { if synced.is_shutdown { return Err(io::Error::new( diff --git a/tokio/tests/io_async_fd.rs b/tokio/tests/io_async_fd.rs index 61ed10c26a9..b52da2945a3 100644 --- a/tokio/tests/io_async_fd.rs +++ b/tokio/tests/io_async_fd.rs @@ -947,27 +947,23 @@ async fn try_new() { assert!(Arc::ptr_eq(&original, &returned)); } -#[tokio::test] -async fn try_with_interest() { - let original = Arc::new(InvalidSource); - - let error = AsyncFd::try_with_interest(original.clone(), Interest::READABLE).unwrap_err(); - let (returned, _cause) = error.into_parts(); - - assert!(Arc::ptr_eq(&original, &returned)); -} - +/// Regression test for issue #7563 +/// +/// This test reproduces the bug scenario where a file descriptor is closed +/// before dropping AsyncFd. When this happens: +/// - OS deregistration fails (fd already closed) +/// - Before fix: Early return prevents cleanup, ScheduledIo leaks in registrations list +/// - After fix: Cleanup always happens regardless of OS error #[tokio::test] async fn memory_leak_when_fd_closed_before_drop() { use std::os::unix::io::{AsRawFd, RawFd}; use std::sync::Arc; - use std::time::Duration; use tokio::io::unix::AsyncFd; + use tokio::runtime::Handle; use nix::sys::socket::{self, AddressFamily, SockFlag, SockType}; - // Wrapper that just holds a raw fd number - // This allows us to close it manually + // Wrapper that just holds a raw fd number - matches the issue reproduction struct RawFdWrapper { fd: RawFd, } @@ -978,7 +974,22 @@ async fn memory_leak_when_fd_closed_before_drop() { } } - for _ in 0..100 { + let rt_handle = Handle::current(); + + // Get baseline count + tokio::task::yield_now().await; + let initial_count = rt_handle.io_total_registration_count(); + + // Direct reproduction of issue #7563 bug scenario + // Exact reproduction: close fd before dropping AsyncFd + // With bug: OS deregister fails -> early return -> ScheduledIo leaks in registrations list + // With fix: Cleanup always happens regardless of OS error + const ITERATIONS: usize = 30; + + // Track count progression to detect leak + let mut max_count_seen = initial_count; + + for _ in 0..ITERATIONS { // Create a socket pair (works on both Linux and macOS) let (fd_a, _fd_b) = socket::socketpair( AddressFamily::Unix, @@ -989,16 +1000,61 @@ async fn memory_leak_when_fd_closed_before_drop() { .expect("socketpair"); let raw_fd = fd_a.as_raw_fd(); set_nonblocking(raw_fd); - std::mem::forget(fd_a); - let fd_wrapper = RawFdWrapper { fd: raw_fd }; - let async_fd = AsyncFd::new(ArcFd(Arc::new(fd_wrapper))).unwrap(); + std::mem::forget(fd_a); // Prevent OwnedFd from closing it + + // Wrap in Arc (matches issue reproduction pattern exactly) + let afd = Arc::new(RawFdWrapper { fd: raw_fd }); + + // Create AsyncFd - this registers the fd with the reactor + let async_fd = AsyncFd::new(ArcFd(afd.clone())).unwrap(); + + // Close fd BEFORE dropping AsyncFd - this is the bug trigger + // When AsyncFd is dropped, deregister_source() will be called with a closed fd + // On Linux with epoll, epoll_ctl(EPOLL_CTL_DEL) fails with EBADF unsafe { libc::close(raw_fd); } - tokio::time::sleep(Duration::from_millis(1)).await; - // Now drop AsyncFd - deregister will fail, but cleanup should still happen + + // Drop AsyncFd - with bug: if OS deregister fails, early return prevents cleanup + // with fix: cleanup happens even if OS deregister fails drop(async_fd); + + // Check count after each drop to catch leak early + tokio::task::yield_now().await; + let current_count = rt_handle.io_total_registration_count(); + max_count_seen = max_count_seen.max(current_count); } - - // If we get here without leaking memory, the test passes -} + + // Give driver more time to process + tokio::task::yield_now().await; + tokio::time::sleep(Duration::from_millis(100)).await; + + // Final check + let final_count = rt_handle.io_total_registration_count(); + max_count_seen = max_count_seen.max(final_count); + + // With the fix: count should stay near initial (cleanup happened) + // With the bug: count grows linearly (each ScheduledIo leaked) + // + // This test reproduces issue #7563. On Linux with epoll, the bug manifests + // as OS deregister fails with EBADF, causing early return and leak. + // On macOS with kqueue, behavior may differ, but the test still exercises + // the code path to ensure the fix works. + assert!( + final_count <= initial_count + 2 && max_count_seen <= initial_count + 2, + "REPRODUCED BUG #7563: Memory leak detected! \ + Final count: {} (initial: {}), max seen: {}. \ + With the bug, count would be ~{} ({} leaked ScheduledIo objects). \ + With the fix, count should be <= {}. \ + \ + This confirms the memory leak from issue #7563. \ + Bug: fd closed before AsyncFd drop -> OS deregister fails -> \ + early return prevents cleanup -> ScheduledIo leaks in registrations list.", + final_count, + initial_count, + max_count_seen, + initial_count + ITERATIONS, + max_count_seen.saturating_sub(initial_count), + initial_count + 2 + ); +} \ No newline at end of file From bf5c706f2dae995316de9aca047ee73be1c6ba7d Mon Sep 17 00:00:00 2001 From: F4RAN Date: Sat, 13 Dec 2025 13:07:26 +0330 Subject: [PATCH 4/8] test: linux test with fix --- tokio/src/runtime/io/driver.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tokio/src/runtime/io/driver.rs b/tokio/src/runtime/io/driver.rs index c802a256718..a8588b5179b 100644 --- a/tokio/src/runtime/io/driver.rs +++ b/tokio/src/runtime/io/driver.rs @@ -297,8 +297,8 @@ impl Handle { ) -> io::Result<()> { // Deregister the source with the OS poller **first** // Cleanup ALWAYS happens - // let os_result = self.registry.deregister(source); - self.registry.deregister(source)?; + let os_result = self.registry.deregister(source); + // self.registry.deregister(source)?; if self .registrations @@ -309,7 +309,7 @@ impl Handle { self.metrics.dec_fd_count(); - // os_result?; // Return error after cleanup + os_result?; // Return error after cleanup Ok(()) } From e659d60c4e07ffeade20c0c186af8f3a31a883a4 Mon Sep 17 00:00:00 2001 From: F4RAN Date: Sat, 13 Dec 2025 13:12:14 +0330 Subject: [PATCH 5/8] chore: remove additional method --- tokio/src/runtime/handle.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tokio/src/runtime/handle.rs b/tokio/src/runtime/handle.rs index c5899881569..1658c585e97 100644 --- a/tokio/src/runtime/handle.rs +++ b/tokio/src/runtime/handle.rs @@ -491,12 +491,6 @@ impl Handle { /// TEST PURPOSE RELATED TO PR #7773 #[cfg(feature = "full")] impl Handle { - /// Returns a reference to the IO driver handle (test-only, not part of public API) - #[doc(hidden)] - pub(crate) fn io_driver(&self) -> &crate::runtime::io::Handle { - self.inner.driver().io() - } - /// Returns the number of pending registrations (test-only, not part of public API) #[doc(hidden)] pub fn io_pending_registration_count(&self) -> usize { From b44e56d5f95875e42d91e1e1d2fe607ba136e7ca Mon Sep 17 00:00:00 2001 From: F4RAN Date: Sat, 13 Dec 2025 13:19:20 +0330 Subject: [PATCH 6/8] chore: remove additional debug comments --- tokio/src/runtime/io/driver.rs | 1 - tokio/tests/io_async_fd.rs | 52 +++++----------------------------- 2 files changed, 7 insertions(+), 46 deletions(-) diff --git a/tokio/src/runtime/io/driver.rs b/tokio/src/runtime/io/driver.rs index a8588b5179b..f7c8b7d334b 100644 --- a/tokio/src/runtime/io/driver.rs +++ b/tokio/src/runtime/io/driver.rs @@ -298,7 +298,6 @@ impl Handle { // Deregister the source with the OS poller **first** // Cleanup ALWAYS happens let os_result = self.registry.deregister(source); - // self.registry.deregister(source)?; if self .registrations diff --git a/tokio/tests/io_async_fd.rs b/tokio/tests/io_async_fd.rs index b52da2945a3..37d942da371 100644 --- a/tokio/tests/io_async_fd.rs +++ b/tokio/tests/io_async_fd.rs @@ -949,11 +949,8 @@ async fn try_new() { /// Regression test for issue #7563 /// -/// This test reproduces the bug scenario where a file descriptor is closed -/// before dropping AsyncFd. When this happens: -/// - OS deregistration fails (fd already closed) -/// - Before fix: Early return prevents cleanup, ScheduledIo leaks in registrations list -/// - After fix: Cleanup always happens regardless of OS error +/// Reproduces the bug where closing fd before dropping AsyncFd causes +/// OS deregister to fail, preventing cleanup and leaking ScheduledIo objects. #[tokio::test] async fn memory_leak_when_fd_closed_before_drop() { use std::os::unix::io::{AsRawFd, RawFd}; @@ -963,7 +960,6 @@ async fn memory_leak_when_fd_closed_before_drop() { use nix::sys::socket::{self, AddressFamily, SockFlag, SockType}; - // Wrapper that just holds a raw fd number - matches the issue reproduction struct RawFdWrapper { fd: RawFd, } @@ -975,22 +971,13 @@ async fn memory_leak_when_fd_closed_before_drop() { } let rt_handle = Handle::current(); - - // Get baseline count tokio::task::yield_now().await; let initial_count = rt_handle.io_total_registration_count(); - // Direct reproduction of issue #7563 bug scenario - // Exact reproduction: close fd before dropping AsyncFd - // With bug: OS deregister fails -> early return -> ScheduledIo leaks in registrations list - // With fix: Cleanup always happens regardless of OS error const ITERATIONS: usize = 30; - - // Track count progression to detect leak let mut max_count_seen = initial_count; for _ in 0..ITERATIONS { - // Create a socket pair (works on both Linux and macOS) let (fd_a, _fd_b) = socket::socketpair( AddressFamily::Unix, SockType::Stream, @@ -1000,61 +987,36 @@ async fn memory_leak_when_fd_closed_before_drop() { .expect("socketpair"); let raw_fd = fd_a.as_raw_fd(); set_nonblocking(raw_fd); - std::mem::forget(fd_a); // Prevent OwnedFd from closing it + std::mem::forget(fd_a); - // Wrap in Arc (matches issue reproduction pattern exactly) let afd = Arc::new(RawFdWrapper { fd: raw_fd }); - - // Create AsyncFd - this registers the fd with the reactor let async_fd = AsyncFd::new(ArcFd(afd.clone())).unwrap(); - // Close fd BEFORE dropping AsyncFd - this is the bug trigger - // When AsyncFd is dropped, deregister_source() will be called with a closed fd - // On Linux with epoll, epoll_ctl(EPOLL_CTL_DEL) fails with EBADF unsafe { libc::close(raw_fd); } - // Drop AsyncFd - with bug: if OS deregister fails, early return prevents cleanup - // with fix: cleanup happens even if OS deregister fails drop(async_fd); - - // Check count after each drop to catch leak early tokio::task::yield_now().await; + let current_count = rt_handle.io_total_registration_count(); max_count_seen = max_count_seen.max(current_count); } - // Give driver more time to process tokio::task::yield_now().await; tokio::time::sleep(Duration::from_millis(100)).await; - // Final check let final_count = rt_handle.io_total_registration_count(); max_count_seen = max_count_seen.max(final_count); - // With the fix: count should stay near initial (cleanup happened) - // With the bug: count grows linearly (each ScheduledIo leaked) - // - // This test reproduces issue #7563. On Linux with epoll, the bug manifests - // as OS deregister fails with EBADF, causing early return and leak. - // On macOS with kqueue, behavior may differ, but the test still exercises - // the code path to ensure the fix works. assert!( final_count <= initial_count + 2 && max_count_seen <= initial_count + 2, - "REPRODUCED BUG #7563: Memory leak detected! \ - Final count: {} (initial: {}), max seen: {}. \ - With the bug, count would be ~{} ({} leaked ScheduledIo objects). \ - With the fix, count should be <= {}. \ - \ - This confirms the memory leak from issue #7563. \ - Bug: fd closed before AsyncFd drop -> OS deregister fails -> \ - early return prevents cleanup -> ScheduledIo leaks in registrations list.", + "Memory leak detected: final count {} (initial: {}), max seen: {}. \ + With bug, count would be ~{} ({} leaked objects).", final_count, initial_count, max_count_seen, initial_count + ITERATIONS, - max_count_seen.saturating_sub(initial_count), - initial_count + 2 + max_count_seen.saturating_sub(initial_count) ); } \ No newline at end of file From 94f46b93d26b1b019f7cfd0e3089a30f9698e31b Mon Sep 17 00:00:00 2001 From: F4RAN Date: Sat, 13 Dec 2025 13:22:58 +0330 Subject: [PATCH 7/8] fix:formatter --- tokio/tests/io_async_fd.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tokio/tests/io_async_fd.rs b/tokio/tests/io_async_fd.rs index 37d942da371..46f42424eff 100644 --- a/tokio/tests/io_async_fd.rs +++ b/tokio/tests/io_async_fd.rs @@ -948,7 +948,7 @@ async fn try_new() { } /// Regression test for issue #7563 -/// +/// /// Reproduces the bug where closing fd before dropping AsyncFd causes /// OS deregister to fail, preventing cleanup and leaking ScheduledIo objects. #[tokio::test] @@ -976,7 +976,7 @@ async fn memory_leak_when_fd_closed_before_drop() { const ITERATIONS: usize = 30; let mut max_count_seen = initial_count; - + for _ in 0..ITERATIONS { let (fd_a, _fd_b) = socket::socketpair( AddressFamily::Unix, @@ -991,24 +991,24 @@ async fn memory_leak_when_fd_closed_before_drop() { let afd = Arc::new(RawFdWrapper { fd: raw_fd }); let async_fd = AsyncFd::new(ArcFd(afd.clone())).unwrap(); - + unsafe { libc::close(raw_fd); } - + drop(async_fd); tokio::task::yield_now().await; - + let current_count = rt_handle.io_total_registration_count(); max_count_seen = max_count_seen.max(current_count); } - + tokio::task::yield_now().await; tokio::time::sleep(Duration::from_millis(100)).await; - + let final_count = rt_handle.io_total_registration_count(); max_count_seen = max_count_seen.max(final_count); - + assert!( final_count <= initial_count + 2 && max_count_seen <= initial_count + 2, "Memory leak detected: final count {} (initial: {}), max seen: {}. \ @@ -1019,4 +1019,4 @@ async fn memory_leak_when_fd_closed_before_drop() { initial_count + ITERATIONS, max_count_seen.saturating_sub(initial_count) ); -} \ No newline at end of file +} From 0a0f94ed484cda3e4115461eddda7d015a123bba Mon Sep 17 00:00:00 2001 From: F4RAN Date: Sat, 13 Dec 2025 13:34:03 +0330 Subject: [PATCH 8/8] fix: style: fix clippy warnings and format code --- tokio/src/runtime/handle.rs | 4 ++-- tokio/src/runtime/io/driver.rs | 7 ++++--- tokio/src/runtime/io/registration_set.rs | 8 ++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/tokio/src/runtime/handle.rs b/tokio/src/runtime/handle.rs index 1658c585e97..9b3f46f172b 100644 --- a/tokio/src/runtime/handle.rs +++ b/tokio/src/runtime/handle.rs @@ -493,12 +493,14 @@ impl Handle { impl Handle { /// Returns the number of pending registrations (test-only, not part of public API) #[doc(hidden)] + #[allow(unreachable_pub)] pub fn io_pending_registration_count(&self) -> usize { self.inner.driver().io().pending_registration_count() } /// Returns the total number of registrations in the main list (test-only, not part of public API) #[doc(hidden)] + #[allow(unreachable_pub)] pub fn io_total_registration_count(&self) -> usize { self.inner.driver().io().total_registration_count() } @@ -508,8 +510,6 @@ impl std::panic::UnwindSafe for Handle {} impl std::panic::RefUnwindSafe for Handle {} - - cfg_taskdump! { impl Handle { /// Captures a snapshot of the runtime's state. diff --git a/tokio/src/runtime/io/driver.rs b/tokio/src/runtime/io/driver.rs index f7c8b7d334b..f16164381c4 100644 --- a/tokio/src/runtime/io/driver.rs +++ b/tokio/src/runtime/io/driver.rs @@ -320,23 +320,24 @@ impl Handle { } } - /// TEST PURPOSE RELATED TO PR #7773 #[cfg(feature = "full")] impl Handle { /// Returns the number of pending registrations (test-only, not part of public API) #[doc(hidden)] + #[allow(unreachable_pub)] pub fn pending_registration_count(&self) -> usize { self.registrations.pending_release_count() } /// Returns the total number of registrations in the main list (test-only) #[doc(hidden)] + #[allow(unreachable_pub)] pub fn total_registration_count(&self) -> usize { - self.registrations.total_registration_count(&mut self.synced.lock()) + self.registrations + .total_registration_count(&mut self.synced.lock()) } } - impl fmt::Debug for Handle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Handle") diff --git a/tokio/src/runtime/io/registration_set.rs b/tokio/src/runtime/io/registration_set.rs index 55c03257f0d..a44efd226ec 100644 --- a/tokio/src/runtime/io/registration_set.rs +++ b/tokio/src/runtime/io/registration_set.rs @@ -64,19 +64,19 @@ impl RegistrationSet { // Count by temporarily draining the list, then restoring it // This is safe for test purposes let mut items = Vec::new(); - + // Drain all items while let Some(item) = synced.registrations.pop_back() { items.push(item); } - + let count = items.len(); - + // Restore items in reverse order (since we popped from back) for item in items.into_iter().rev() { synced.registrations.push_front(item); } - + count }