diff --git a/actix-web/examples/introspection.rs b/actix-web/examples/introspection.rs index 484160b37..2bca5a3e9 100644 --- a/actix-web/examples/introspection.rs +++ b/actix-web/examples/introspection.rs @@ -32,20 +32,20 @@ async fn main() -> std::io::Result<()> { } // GET /introspection for JSON response - async fn introspection_handler_json() -> impl Responder { - use actix_web::introspection::introspection_report_as_json; - - let report = introspection_report_as_json(); + async fn introspection_handler_json( + tree: web::Data, + ) -> impl Responder { + let report = tree.report_as_json(); HttpResponse::Ok() .content_type("application/json") .body(report) } // GET /introspection for plain text response - async fn introspection_handler_text() -> impl Responder { - use actix_web::introspection::introspection_report_as_text; - - let report = introspection_report_as_text(); + async fn introspection_handler_text( + tree: web::Data, + ) -> impl Responder { + let report = tree.report_as_text(); HttpResponse::Ok().content_type("text/plain").body(report) } @@ -242,7 +242,7 @@ async fn main() -> std::io::Result<()> { .to(HttpResponse::MethodNotAllowed), ) }) - .workers(1) + .workers(5) .bind("127.0.0.1:8080")?; server.run().await diff --git a/actix-web/examples/introspection_multi_servers.rs b/actix-web/examples/introspection_multi_servers.rs new file mode 100644 index 000000000..7ed2224e3 --- /dev/null +++ b/actix-web/examples/introspection_multi_servers.rs @@ -0,0 +1,52 @@ +// Example showcasing the experimental introspection feature with multiple App instances. +// Run with: `cargo run --features experimental-introspection --example introspection_multi_servers` + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + #[cfg(feature = "experimental-introspection")] + { + use actix_web::{web, App, HttpResponse, HttpServer, Responder}; + use futures_util::future; + + async fn introspection_handler( + tree: web::Data, + ) -> impl Responder { + HttpResponse::Ok() + .content_type("text/plain") + .body(tree.report_as_text()) + } + + async fn index() -> impl Responder { + HttpResponse::Ok().body("Hello from app") + } + + let srv1 = HttpServer::new(|| { + App::new() + .service(web::resource("/a").route(web::get().to(index))) + .service( + web::resource("/introspection").route(web::get().to(introspection_handler)), + ) + }) + .workers(8) + .bind("127.0.0.1:8081")? + .run(); + + let srv2 = HttpServer::new(|| { + App::new() + .service(web::resource("/b").route(web::get().to(index))) + .service( + web::resource("/introspection").route(web::get().to(introspection_handler)), + ) + }) + .workers(3) + .bind("127.0.0.1:8082")? + .run(); + + future::try_join(srv1, srv2).await?; + } + #[cfg(not(feature = "experimental-introspection"))] + { + eprintln!("This example requires the 'experimental-introspection' feature to be enabled."); + } + Ok(()) +} diff --git a/actix-web/src/app.rs b/actix-web/src/app.rs index f12d39979..1099731f3 100644 --- a/actix-web/src/app.rs +++ b/actix-web/src/app.rs @@ -30,6 +30,8 @@ pub struct App { data_factories: Vec, external: Vec, extensions: Extensions, + #[cfg(feature = "experimental-introspection")] + introspector: Rc>, } impl App { @@ -46,6 +48,10 @@ impl App { factory_ref, external: Vec::new(), extensions: Extensions::new(), + #[cfg(feature = "experimental-introspection")] + introspector: Rc::new(RefCell::new( + crate::introspection::IntrospectionCollector::new(), + )), } } } @@ -366,6 +372,8 @@ where factory_ref: self.factory_ref, external: self.external, extensions: self.extensions, + #[cfg(feature = "experimental-introspection")] + introspector: self.introspector, } } @@ -429,6 +437,8 @@ where factory_ref: self.factory_ref, external: self.external, extensions: self.extensions, + #[cfg(feature = "experimental-introspection")] + introspector: self.introspector, } } } @@ -453,6 +463,8 @@ where default: self.default, factory_ref: self.factory_ref, extensions: RefCell::new(Some(self.extensions)), + #[cfg(feature = "experimental-introspection")] + introspector: Rc::clone(&self.introspector), } } } diff --git a/actix-web/src/app_service.rs b/actix-web/src/app_service.rs index e323a9592..86f6a08b5 100644 --- a/actix-web/src/app_service.rs +++ b/actix-web/src/app_service.rs @@ -41,6 +41,8 @@ where pub(crate) default: Option>, pub(crate) factory_ref: Rc>>, pub(crate) external: RefCell>, + #[cfg(feature = "experimental-introspection")] + pub(crate) introspector: Rc>, } impl ServiceFactory for AppInit @@ -72,6 +74,10 @@ where // create App config to pass to child services let mut config = AppService::new(config, Rc::clone(&default)); + #[cfg(feature = "experimental-introspection")] + { + config.introspector = Rc::clone(&self.introspector); + } // register services mem::take(&mut *self.services.borrow_mut()) @@ -80,6 +86,9 @@ where let mut rmap = ResourceMap::new(ResourceDef::prefix("")); + #[cfg(feature = "experimental-introspection")] + let (config, services, _) = config.into_services(); + #[cfg(not(feature = "experimental-introspection"))] let (config, services) = config.into_services(); // complete pipeline creation. @@ -110,6 +119,8 @@ where // construct app service and middleware service factory future. let endpoint_fut = self.endpoint.new_service(()); + #[cfg(feature = "experimental-introspection")] + let introspector = Rc::clone(&self.introspector); // take extensions or create new one as app data container. let mut app_data = self.extensions.borrow_mut().take().unwrap_or_default(); @@ -132,7 +143,8 @@ where #[cfg(feature = "experimental-introspection")] { - crate::introspection::finalize_registry(); + let tree = introspector.borrow_mut().finalize(); + app_data.insert(crate::web::Data::new(tree)); } Ok(AppInitService { diff --git a/actix-web/src/config.rs b/actix-web/src/config.rs index 1d486b807..8c5a697ce 100644 --- a/actix-web/src/config.rs +++ b/actix-web/src/config.rs @@ -32,6 +32,9 @@ pub struct AppService { )>, #[cfg(feature = "experimental-introspection")] pub current_prefix: String, + #[cfg(feature = "experimental-introspection")] + pub(crate) introspector: + std::rc::Rc>, } impl AppService { @@ -44,6 +47,10 @@ impl AppService { services: Vec::new(), #[cfg(feature = "experimental-introspection")] current_prefix: "".to_string(), + #[cfg(feature = "experimental-introspection")] + introspector: std::rc::Rc::new(std::cell::RefCell::new( + crate::introspection::IntrospectionCollector::new(), + )), } } @@ -53,6 +60,24 @@ impl AppService { } #[allow(clippy::type_complexity)] + #[cfg(feature = "experimental-introspection")] + pub(crate) fn into_services( + self, + ) -> ( + AppConfig, + Vec<( + ResourceDef, + BoxedHttpServiceFactory, + Option, + Option>, + )>, + std::rc::Rc>, + ) { + (self.config, self.services, self.introspector) + } + + #[allow(clippy::type_complexity)] + #[cfg(not(feature = "experimental-introspection"))] pub(crate) fn into_services( self, ) -> ( @@ -77,6 +102,8 @@ impl AppService { root: false, #[cfg(feature = "experimental-introspection")] current_prefix: self.current_prefix.clone(), + #[cfg(feature = "experimental-introspection")] + introspector: std::rc::Rc::clone(&self.introspector), } } @@ -111,8 +138,6 @@ impl AppService { { use std::borrow::Borrow; - use crate::introspection; - // Build the full path for introspection let pat = rdef.pattern().unwrap_or("").to_string(); @@ -148,7 +173,12 @@ impl AppService { // Determine if the registered service is a resource let is_resource = rdef.pattern().is_some(); - introspection::register_pattern_detail(full_path, methods, guard_names, is_resource); + self.introspector.borrow_mut().register_pattern_detail( + full_path, + methods, + guard_names, + is_resource, + ); } self.services diff --git a/actix-web/src/introspection.rs b/actix-web/src/introspection.rs index cedd3653f..c52bea9df 100644 --- a/actix-web/src/introspection.rs +++ b/actix-web/src/introspection.rs @@ -1,61 +1,14 @@ -use std::{ - collections::HashMap, - fmt::Write as FmtWrite, - sync::{ - atomic::{AtomicBool, Ordering}, - Mutex, OnceLock, - }, - thread, -}; +use std::{collections::HashMap, fmt::Write as FmtWrite}; use serde::Serialize; use crate::http::Method; -static REGISTRY: OnceLock> = OnceLock::new(); -static DETAIL_REGISTRY: OnceLock>> = OnceLock::new(); -static DESIGNATED_THREAD: OnceLock = OnceLock::new(); -static IS_INITIALIZED: AtomicBool = AtomicBool::new(false); - -fn initialize_registry() { - REGISTRY.get_or_init(|| { - Mutex::new(IntrospectionNode::new( - ResourceType::App, - "".into(), - "".into(), - )) - }); -} - -fn get_registry() -> &'static Mutex { - REGISTRY.get().expect("Registry not initialized") -} - -fn initialize_detail_registry() { - DETAIL_REGISTRY.get_or_init(|| Mutex::new(HashMap::new())); -} - -fn get_detail_registry() -> &'static Mutex> { - DETAIL_REGISTRY - .get() - .expect("Detail registry not initialized") -} - -fn is_designated_thread() -> bool { - let current_id = thread::current().id(); - DESIGNATED_THREAD.get_or_init(|| { - IS_INITIALIZED.store(true, Ordering::SeqCst); - current_id // Assign the first thread that calls this function - }); - - *DESIGNATED_THREAD.get().unwrap() == current_id -} - #[derive(Clone)] pub struct RouteDetail { methods: Vec, guards: Vec, - is_resource: bool, // Indicates if this detail is for a final resource endpoint + is_resource: bool, } #[derive(Debug, Clone, Copy)] @@ -74,6 +27,7 @@ pub struct IntrospectionNode { pub guards: Vec, pub children: Vec, } + #[derive(Debug, Clone, Serialize)] pub struct IntrospectionReportItem { pub full_path: String, @@ -112,16 +66,10 @@ impl From<&IntrospectionNode> for Vec { }; if !node.methods.is_empty() || !node.guards.is_empty() { - // Filter guards that are already represented in methods let filtered_guards: Vec = node .guards .iter() - .filter(|guard| { - !node - .methods - .iter() - .any(|method| method.to_string() == **guard) - }) + .filter(|guard| !node.methods.iter().any(|m| m.to_string() == **guard)) .cloned() .collect(); @@ -143,62 +91,113 @@ impl From<&IntrospectionNode> for Vec { } } -pub(crate) fn finalize_registry() { - if !is_designated_thread() { - return; - } +#[derive(Clone, Default)] +pub struct IntrospectionCollector { + details: HashMap, +} - initialize_registry(); - initialize_detail_registry(); - - let detail_registry = get_detail_registry().lock().unwrap(); - let mut root = IntrospectionNode::new(ResourceType::App, "".into(), "".into()); - - // Build the introspection tree directly from the detail registry - for (full_path, _detail) in detail_registry.iter() { - let parts: Vec<&str> = full_path.split('/').collect(); - let mut current_node = &mut root; - - for (i, part) in parts.iter().enumerate() { - // Find the index of the existing child - let existing_child_index = current_node - .children - .iter() - .position(|n| n.pattern == *part); - - let child_index = if let Some(child_index) = existing_child_index { - child_index - } else { - // If it doesn't exist, create a new node and get its index - let child_full_path = parts[..=i].join("/"); - // Determine the kind based on whether this path exists as a resource in the detail registry - let kind = if detail_registry - .get(&child_full_path) - .is_some_and(|d| d.is_resource) - { - ResourceType::Resource - } else { - ResourceType::Scope - }; - let new_node = IntrospectionNode::new(kind, part.to_string(), child_full_path); - current_node.children.push(new_node); - current_node.children.len() - 1 - }; - - // Get a mutable reference to the child node - current_node = &mut current_node.children[child_index]; - - // If this node is marked as a resource, update its methods and guards - if let ResourceType::Resource = current_node.kind { - if let Some(detail) = detail_registry.get(¤t_node.full_path) { - update_unique(&mut current_node.methods, &detail.methods); - update_unique(&mut current_node.guards, &detail.guards); - } - } +impl IntrospectionCollector { + pub fn new() -> Self { + Self { + details: HashMap::new(), } } - *get_registry().lock().unwrap() = root; + pub fn register_pattern_detail( + &mut self, + full_path: String, + methods: Vec, + guards: Vec, + is_resource: bool, + ) { + self.details + .entry(full_path) + .and_modify(|d| { + update_unique(&mut d.methods, &methods); + update_unique(&mut d.guards, &guards); + if !d.is_resource && is_resource { + d.is_resource = true; + } + }) + .or_insert(RouteDetail { + methods, + guards, + is_resource, + }); + } + + pub fn finalize(&mut self) -> IntrospectionTree { + let detail_registry = std::mem::take(&mut self.details); + let mut root = IntrospectionNode::new(ResourceType::App, "".into(), "".into()); + + for (full_path, _) in detail_registry.iter() { + let parts: Vec<&str> = full_path.split('/').collect(); + let mut current_node = &mut root; + + for (i, part) in parts.iter().enumerate() { + let existing_child_index = current_node + .children + .iter() + .position(|n| n.pattern == *part); + + let child_index = if let Some(idx) = existing_child_index { + idx + } else { + let child_full_path = parts[..=i].join("/"); + let kind = if detail_registry + .get(&child_full_path) + .is_some_and(|d| d.is_resource) + { + ResourceType::Resource + } else { + ResourceType::Scope + }; + let new_node = IntrospectionNode::new(kind, part.to_string(), child_full_path); + current_node.children.push(new_node); + current_node.children.len() - 1 + }; + + current_node = &mut current_node.children[child_index]; + + if let ResourceType::Resource = current_node.kind { + if let Some(detail) = detail_registry.get(¤t_node.full_path) { + update_unique(&mut current_node.methods, &detail.methods); + update_unique(&mut current_node.guards, &detail.guards); + } + } + } + } + + IntrospectionTree { root } + } +} + +#[derive(Clone)] +pub struct IntrospectionTree { + pub root: IntrospectionNode, +} + +impl IntrospectionTree { + pub fn report_as_text(&self) -> String { + let report_items: Vec = (&self.root).into(); + + let mut buf = String::new(); + for item in report_items { + writeln!( + buf, + "{} => Methods: {:?} | Guards: {:?}", + item.full_path, item.methods, item.guards + ) + .unwrap(); + } + + buf + } + + pub fn report_as_json(&self) -> String { + let report_items: Vec = (&self.root).into(); + serde_json::to_string_pretty(&report_items).unwrap() + } } fn update_unique(existing: &mut Vec, new_items: &[T]) { @@ -208,56 +207,3 @@ fn update_unique(existing: &mut Vec, new_items: &[T]) { } } } - -pub(crate) fn register_pattern_detail( - full_path: String, - methods: Vec, - guards: Vec, - is_resource: bool, -) { - if !is_designated_thread() { - return; - } - initialize_detail_registry(); - let mut reg = get_detail_registry().lock().unwrap(); - reg.entry(full_path) - .and_modify(|d| { - update_unique(&mut d.methods, &methods); - update_unique(&mut d.guards, &guards); - // If the existing entry was not a resource but the new one is, update the kind - if !d.is_resource && is_resource { - d.is_resource = true; - } - }) - .or_insert(RouteDetail { - methods, - guards, - is_resource, - }); -} - -pub fn introspection_report_as_text() -> String { - let registry = get_registry(); - let node = registry.lock().unwrap(); - let report_items: Vec = (&*node).into(); - - let mut buf = String::new(); - for item in report_items { - writeln!( - buf, - "{} => Methods: {:?} | Guards: {:?}", - item.full_path, item.methods, item.guards - ) - .unwrap(); - } - - buf -} - -pub fn introspection_report_as_json() -> String { - let registry = get_registry(); - let node = registry.lock().unwrap(); - let report_items: Vec = (&*node).into(); - - serde_json::to_string_pretty(&report_items).unwrap() -} diff --git a/actix-web/src/resource.rs b/actix-web/src/resource.rs index df5f05fb0..7b20727c6 100644 --- a/actix-web/src/resource.rs +++ b/actix-web/src/resource.rs @@ -432,7 +432,7 @@ where }; #[cfg(feature = "experimental-introspection")] { - use crate::{http::Method, introspection}; + use crate::http::Method; let guards_routes = routes.iter().map(|r| r.guards()).collect::>(); @@ -464,7 +464,7 @@ where }) .collect::>(); - introspection::register_pattern_detail( + config.introspector.borrow_mut().register_pattern_detail( full_path.clone(), methods, guard_names,