mirror of https://github.com/fafhrd91/actix-web
feat(introspection): implement experimental introspection feature with multiple App instances
This commit is contained in:
parent
2b52a60bc2
commit
7ff7768dc4
|
@ -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<actix_web::introspection::IntrospectionTree>,
|
||||
) -> 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<actix_web::introspection::IntrospectionTree>,
|
||||
) -> 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
|
||||
|
|
|
@ -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<actix_web::introspection::IntrospectionTree>,
|
||||
) -> 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(())
|
||||
}
|
|
@ -30,6 +30,8 @@ pub struct App<T> {
|
|||
data_factories: Vec<FnDataFactory>,
|
||||
external: Vec<ResourceDef>,
|
||||
extensions: Extensions,
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
introspector: Rc<RefCell<crate::introspection::IntrospectionCollector>>,
|
||||
}
|
||||
|
||||
impl App<AppEntry> {
|
||||
|
@ -46,6 +48,10 @@ impl App<AppEntry> {
|
|||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,8 @@ where
|
|||
pub(crate) default: Option<Rc<BoxedHttpServiceFactory>>,
|
||||
pub(crate) factory_ref: Rc<RefCell<Option<AppRoutingFactory>>>,
|
||||
pub(crate) external: RefCell<Vec<ResourceDef>>,
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
pub(crate) introspector: Rc<RefCell<crate::introspection::IntrospectionCollector>>,
|
||||
}
|
||||
|
||||
impl<T, B> ServiceFactory<Request> for AppInit<T, B>
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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<std::cell::RefCell<crate::introspection::IntrospectionCollector>>,
|
||||
}
|
||||
|
||||
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<Guards>,
|
||||
Option<Rc<ResourceMap>>,
|
||||
)>,
|
||||
std::rc::Rc<std::cell::RefCell<crate::introspection::IntrospectionCollector>>,
|
||||
) {
|
||||
(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
|
||||
|
|
|
@ -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<Mutex<IntrospectionNode>> = OnceLock::new();
|
||||
static DETAIL_REGISTRY: OnceLock<Mutex<HashMap<String, RouteDetail>>> = OnceLock::new();
|
||||
static DESIGNATED_THREAD: OnceLock<thread::ThreadId> = 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<IntrospectionNode> {
|
||||
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<HashMap<String, RouteDetail>> {
|
||||
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<Method>,
|
||||
guards: Vec<String>,
|
||||
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<String>,
|
||||
pub children: Vec<IntrospectionNode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct IntrospectionReportItem {
|
||||
pub full_path: String,
|
||||
|
@ -112,16 +66,10 @@ impl From<&IntrospectionNode> for Vec<IntrospectionReportItem> {
|
|||
};
|
||||
|
||||
if !node.methods.is_empty() || !node.guards.is_empty() {
|
||||
// Filter guards that are already represented in methods
|
||||
let filtered_guards: Vec<String> = 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<IntrospectionReportItem> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finalize_registry() {
|
||||
if !is_designated_thread() {
|
||||
return;
|
||||
}
|
||||
#[derive(Clone, Default)]
|
||||
pub struct IntrospectionCollector {
|
||||
details: HashMap<String, RouteDetail>,
|
||||
}
|
||||
|
||||
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<Method>,
|
||||
guards: Vec<String>,
|
||||
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<IntrospectionReportItem> = (&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<IntrospectionReportItem> = (&self.root).into();
|
||||
serde_json::to_string_pretty(&report_items).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn update_unique<T: Clone + PartialEq>(existing: &mut Vec<T>, new_items: &[T]) {
|
||||
|
@ -208,56 +207,3 @@ fn update_unique<T: Clone + PartialEq>(existing: &mut Vec<T>, new_items: &[T]) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn register_pattern_detail(
|
||||
full_path: String,
|
||||
methods: Vec<Method>,
|
||||
guards: Vec<String>,
|
||||
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<IntrospectionReportItem> = (&*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<IntrospectionReportItem> = (&*node).into();
|
||||
|
||||
serde_json::to_string_pretty(&report_items).unwrap()
|
||||
}
|
||||
|
|
|
@ -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::<Vec<_>>();
|
||||
|
||||
|
@ -464,7 +464,7 @@ where
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
introspection::register_pattern_detail(
|
||||
config.introspector.borrow_mut().register_pattern_detail(
|
||||
full_path.clone(),
|
||||
methods,
|
||||
guard_names,
|
||||
|
|
Loading…
Reference in New Issue