How actix-web's application state and Data extractor works internally
source link: https://dev.to/joshchoo/how-actix-webs-application-state-and-data-extractor-works-internally-fm1
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
How actix-web's application state and Data extractor works internally
When developing web servers, we sometimes need a mechanism to share application state, such as configurations or database connections. actix-web
makes it possible to inject shared data into application state using the app_data
method and retrieve and pass the data to route handlers using Data extractor.
Let's explore just enough to understand how it works under the hood!
Note: The examples are based on zero2prod and actix-web v4 source code.
The following is an example of an actix-web
server that exposes a /subscriptions
route. subscribeHandler
accepts a Postgres database connection (PgConnection
), which we added to the application state via the app_data
method.
pub fn run(listener: TcpListener, connection: PgConnection) -> Result<Server, std::io::Error> {
let connection = web::Data::new(connection);
let server = HttpServer::new(move || {
App::new()
.route("/subscriptions", web::post().to(subscribeHandler))
// vvvvv Store PgConnection app data here
.app_data(connection.clone())
})
.listen(listener)?
.run();
Ok(server)
}
...
// Extract PgConnection from application state
pub async fn subscribeHandler(_connection: web::Data<PgConnection>) -> HttpResponse {
//...
}
Question: Since it is possible to store other data types, how does actix-web
know to retrieve and pass a PgConnection
value to subscribeHandler
instead of a different type?
Firstly, let's take a look at the web::post().to(...)
implementation:
// in actix-web's src/web.rs
pub fn to<F, I, R>(handler: F) -> Route
where
F: Handler<I, R>,
I: FromRequest + 'static, // <--- Notice this!
R: Future + 'static,
R::Output: Responder + 'static,
{
Route::new().to(handler)
}
We could dive deeper into the function calls, but it would get quite complex and unnecessary to know. So instead, take note of the FromRequest
trait associated with the Handler
.
Next, we shall take a look at web::Data<T>
, which subscribeHandler
takes as a parameter, and see that it implements the FromRequest
trait with the from_request
function:
// in actix-web's src/data.rs
pub struct Data<T: ?Sized>(Arc<T>);
impl<T: ?Sized + 'static> FromRequest for Data<T> {
type Config = ();
type Error = Error;
type Future = Ready<Result<Self, Error>>;
#[inline]
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
if let Some(st) = req.app_data::<Data<T>>() {
ok(st.clone())
} else {
log::debug!(
"Failed to construct App-level Data extractor. \
Request path: {:?} (type: {})",
req.path(),
type_name::<T>(),
);
err(ErrorInternalServerError(
"App data is not configured, to configure use App::data()",
))
}
}
}
from_request
calls the req.app_data::<Data<T>>()
, which returns an Option
. It returns the data, st
, if found. Otherwise it returns an Internal Server Error.
So, how exactly does calling req.app_data::<Data<T>>
return the correct PgConnection
data that we specified in the generic type parameter?
Let's look at the app_data
method implementation:
// in actix-web's src/request.rs
pub struct HttpRequest {
pub(crate) inner: Rc<HttpRequestInner>,
}
pub(crate) struct HttpRequestInner {
pub(crate) app_data: SmallVec<[Rc<Extensions>; 4]>,
}
impl HttpRequest {
pub fn app_data<T: 'static>(&self) -> Option<&T> {
// container has type `Extensions`
for container in self.inner.app_data.iter().rev() {
if let Some(data) = container.get::<T>() {
return Some(data);
}
}
None
}
}
It appears that the incoming HttpRequest
data contains app_data
, which the code iterates through and then calls container.get::<T>()
.
// in actix-http's src/extensions.rs
pub struct Extensions {
map: AHashMap<TypeId, Box<dyn Any>>,
}
impl Extensions
pub fn get<T: 'static>(&self) -> Option<&T> {
self.map
.get(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast_ref())
}
}
get::<T>()
tries to get the value at the key TypeId::of::<T>()
from the hashmap, map
. What does TypeId::of::<T>()
do?
/// A `TypeId` represents a globally unique identifier for a type.
pub struct TypeId {
t: u64,
}
impl TypeId {
/// Returns the `TypeId` of the type this generic function has been
/// instantiated with.
pub const fn of<T: ?Sized + 'static>() -> TypeId {
TypeId { t: intrinsics::type_id::<T>() }
}
}
Aha! Given a type, such as PgConnection
, Rust allows us to obtain a unique u64
value that identifies it! Hence calling TypeId::of::<PgConnection>()
gives us a value that we can use a key for the hashmap inside of app_data
.
But wait a moment! Isn't PgConnection
just a generic type parameter? So, at runtime, how does Rust ensure that calling TypeId::of::<PgConnection>()
gives us the value associated with PgConnection
and not some other type?
Rust generates different copies of generic functions during compilation for each concrete type needed, such as PgConnection
. This process is known as monomorphization. Given that we use web::Data<PgConnnection>
, Rust will compile copies of the from_request
, app_data
, get
and TypeId::of
functions that are specific to the PgConnection
type.
At runtime, when a request hits the /subscriptions
route, the PgConnection
-specific version of the TypeId::of
function will return a concrete TypeId
value associated with PgConnection
. This value is then used to search app_data
's internal hashmap and return the stored PgConnection
data that we added to the application state.
So, we've figured out how actix-web
retrieves PgConnection
from the application state. Now, to complete our understanding, let's look at how actix-web
stores data in the application state:
// Server code
App::new()
.route("/subscriptions", web::post().to(subscribeHandler))
// vvvvv Store PgConnection app data here
.app_data(connection.clone())
Digging into the app_data
method's implementation:
// in actix-web's src/app.rs
impl App {
pub fn app_data<U: 'static>(mut self, ext: U) -> Self {
self.extensions.insert(ext);
self
}
}
App
inserts the PgConnection
value into its own extensions
, which contains a hashmap (remember from earlier):
// in actix-http's src/extensions.rs
impl Extensions
pub fn insert<T: 'static>(&mut self, val: T) -> Option<T> {
self.map
.insert(TypeId::of::<T>(), Box::new(val))
.and_then(downcast_owned)
}
}
Hey! It's the same Extensions
type! The method inserts the PgConnection
value with the key of TypeId::of::<T>()
, which resolves to the PgConnection
-specific value.
There we have it! actix-web
stores data in application state's by inserting and retrieving them from a hashmap using keys derived from TypeId::of::<T>()
, which returns unique identifiers for different types. And this is made possible by monomorphization
in Rust!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK