From 4d8c394d666d49004bafad5cc4f20d8d9b990dd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 22 Aug 2026 02:30:02 +0200 Subject: [PATCH] test(fetch): cover multi-app scanner registration --- changelog.d/8546-multi-app-fetch-scanner.md | 8 +++++ crates/perry-runtime/src/gc/roots.rs | 13 ++++++++ crates/perry-stdlib/src/fetch/tests.rs | 37 +++++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 changelog.d/8546-multi-app-fetch-scanner.md diff --git a/changelog.d/8546-multi-app-fetch-scanner.md b/changelog.d/8546-multi-app-fetch-scanner.md new file mode 100644 index 0000000000..359d281151 --- /dev/null +++ b/changelog.d/8546-multi-app-fetch-scanner.md @@ -0,0 +1,8 @@ +### Fixed + +- Preserve Fetch `Request`, `Headers`, and `FormData` registry roots for every + application thread in an embedded multi-app host. Previously a + process-global registration latch could install the Fetch scanner only for + the first Perry heap, so a second in-process Next application could reject + its HTTP handler and return 500. Add a two-application-thread regression test + for the per-thread scanner contract. (#8546) diff --git a/crates/perry-runtime/src/gc/roots.rs b/crates/perry-runtime/src/gc/roots.rs index 223f22a3bc..5f9be0112f 100644 --- a/crates/perry-runtime/src/gc/roots.rs +++ b/crates/perry-runtime/src/gc/roots.rs @@ -192,6 +192,19 @@ pub(super) fn copy_only_root_scanner_counts() -> (usize, usize) { (rust_scanners, ffi_scanners) } +/// Return the number of named native-package mutable-root scanners registered +/// for the current Perry thread. +/// +/// The registry is deliberately thread-local: an embedding host may run +/// multiple independent Perry heaps in one process, and each heap's collector +/// must see the scanners installed by its stdlib provider. This small +/// diagnostic surface lets provider tests verify that contract without running +/// a collection over fabricated heap pointers. +#[doc(hidden)] +pub fn gc_named_ffi_mutable_root_scanner_count() -> usize { + FFI_NAMED_MUTABLE_ROOT_SCANNERS.with(|scanners| scanners.borrow().len()) +} + pub(super) fn registered_root_scanners_block_budgeted_gc() -> bool { let has_copy_only = ROOT_SCANNERS.with(|scanners| !scanners.borrow().is_empty()) || FFI_ROOT_SCANNERS.with(|scanners| !scanners.borrow().is_empty()); diff --git a/crates/perry-stdlib/src/fetch/tests.rs b/crates/perry-stdlib/src/fetch/tests.rs index fd71ac506d..11b5fa23e6 100644 --- a/crates/perry-stdlib/src/fetch/tests.rs +++ b/crates/perry-stdlib/src/fetch/tests.rs @@ -1,5 +1,42 @@ use super::*; +/// #8546: Coop hosts each in-process deployment on its own dedicated Perry +/// thread. The Fetch scanner registry is thread-local, so a process-global +/// registration latch makes the first Next application safe and leaves the +/// second application's Request / Headers roots invisible to its collector. +/// +/// Run the two applications sequentially so this test deterministically fails +/// with the old `Once`: app 1 consumes the process-global latch, then app 2 +/// starts with an empty thread-local scanner registry and cannot register. +#[test] +fn fetch_root_scanner_registers_for_each_application_thread() { + for application in 1..=2 { + let (before, after_first, after_second) = std::thread::spawn(|| { + let before = perry_runtime::gc::gc_named_ffi_mutable_root_scanner_count(); + gc::ensure_gc_registered(); + let after_first = perry_runtime::gc::gc_named_ffi_mutable_root_scanner_count(); + gc::ensure_gc_registered(); + let after_second = perry_runtime::gc::gc_named_ffi_mutable_root_scanner_count(); + (before, after_first, after_second) + }) + .join() + .expect("application thread panicked"); + + assert_eq!( + before, 0, + "application {application} must start with its own empty scanner registry" + ); + assert_eq!( + after_first, 1, + "application {application} did not install the Fetch root scanner" + ); + assert_eq!( + after_second, 1, + "application {application} registered the Fetch root scanner twice" + ); + } +} + #[test] fn fetch_handle_ids_use_high_small_handle_range() { use perry_runtime::value::addr_class;