The Rust ecosystem is still growing. As a result, new libraries with improved functionality are frequently released into the developer community, while older libraries become obsolete. When we initially designed Exonum, we used the Iron web-framework. In this article, we describe how we ported the Exonum framework to actix-web using generic programming.
Exonum on Iron
In the Exonum platform, the Iron framework was used without any abstractions. We installed handlers for certain resources and obtained request parameters by parsing URLs using auxiliary methods; the result was returned simply in the form of a string.
The process looked (approximately) like the following:
fn set_blocks_response(self, router: &mut Router) {
let blocks = move |req: &mut Request| -> IronResult<Response> {
let count: usize = self.required_param(req, "count")?;
let latest: Option<u64> = self.optional_param(req, "latest")?;
let skip_empty_blocks: bool = self.optional_param(req, "skip_empty_blocks")?
.unwrap_or(false);
let info = self.blocks(count, latest.map(Height), skip_empty_blocks)?;
self.ok_response(&::serde_json::to_value(info).unwrap())
};
router.get("/v1/blocks", blocks, "blocks");
}
In addition, we used some middleware plugins in the form of CORS headers. We used mount to merge all the handlers into a single API.
Our Decision to Shift Away from Iron
Iron was a good library, with plenty of plugins. However, it was written in the days when such projects as futures and tokio did not exist.
The architecture of Iron involves synchronous requests processing, which can be easily affected by a large number of simultaneously open connections. To be scalable, Iron needed to become asynchronous, which would involve rethinking and rewriting the whole framework. As a result, we’ve seen a gradual departure from using Iron by software engineers.
Why We Chose Actix-Web
Actix-web is a popular framework that ranks high on TechEmpower benchmarks. It has an active developer community, unlike Iron, and it has a well-designed API and high-quality implementation based on the actix actor framework. Requests are processed asynchronously by the thread pool; if request processing panics, the actor is automatically restarted.
Previously, concerns were raised that actix-web contained a lot of unsafe code. However, the amount of unsafe code was significantly reduced when the framework was rewritten in a safe programming language — Rust. Bitfury’s engineers have reviewed this code themselves and feel confident in its long-term stability.
For the Exonum framework, shifting to actix solved the issue of operation stability. The Iron framework could fail if there were a large number of connections. We have also found that the actix-web API is simpler, more productive and more unified. We are confident that users and developers will have an easier time using the Exonum programming interface, which can now operate faster thanks to the actix-web design.
What We Require from a Web Framework
During this process we realized it was important for us not to simply shift frameworks, but to also devise a new API architecture independent of any specific web framework. Such architecture would allow for creating handlers, with little to no concern about web specifics, and transferring them to any backend. This conception can be implemented by writing a frontend that would apply basic types and traits.
To understand what this frontend needs to look like, let’s define what any HTTP API really is:
- Requests are made exclusively by clients; the server only responds to them (the server does not initiate requests).
- Requests either read data or change data.
- As a result of the request processing, the server returns a response, which contains the required data, in case of success; or information about the error, in case of failure.
If we are to analyze all the abstraction layers, it turns out that any HTTP request is just a function call:
fn request(context: &ServiceContext, query: Query) -> Result<Response, ServiceError>
Everything else can be considered an extension of this basic entity. Thus, in order to be independent from a specific implementation of a web framework, we need to write handlers in a style similar to the example above.
Trait `Endpoint` for Generic Processing of HTTP-requests
The most simple and straightforward approach would be declaring the `Endpoint` trait, which describes the implementations of specific requests:
// A trait describing GET request handlers. It should be possible to call each of the handlers from any freed
// thread. This requirement imposes certain restrictions on the trait. Parameters and request results are
// configured using associated types.
trait Endpoint: Sync + Send + 'static {
type Request: DeserializeOwned + 'static;
type Response: Serialize + 'static;
fn handle(&self, context: &Context, request: Self::Request) -> Result<Self::Response, io::Error>;
}
Now we need to implement this handler in a specific framework. For example, in actix-web it looks like the following:
// Response type in actix-web. Note that they are asynchronous, even though `Endpoint` assumes that
// processing is synchronous.
type FutureResponse = actix_web::FutureResponse<HttpResponse, actix_web::Error>;
// A raw request handler for actix-web. This is what the framework ultimately works with. The handler
// receives parameters from an arbitrary context, through which the request parameters are passed.
type RawHandler = dyn Fn(HttpRequest<Context>) -> FutureResponse + 'static + Send + Sync;
// For convenience, let’s put everything we need from the handler into a single structure.
#[derive(Clone)]
struct RequestHandler {
/// The name of the resource.
pub name: String,
/// HTTP method.
pub method: actix_web::http::Method,
/// The raw handler. Note that it will be used from multiple threads.
pub inner: Arc<RawHandler>,
}
We can use structures for passing request parameters through the context. Actix-web can automatically deserialize parameters using serde. For example, a=15&b=hello is deserialized into a structure like this one:
#[derive(Deserialize)]
struct SimpleQuery {
a: i32,
b: String,
}
This deserialization functionality agrees well with the associated type Request from the `Endpoint` trait.
Next, lets devise an adapter which wraps a specific implementation of `Endpoint` into a RequestHandler for actix-web. Pay attention to the fact that while doing so, the information on Request and Response types disappears. This technique is called type erasure — it transforms static dispatching into a dynamic one.
impl RequestHandler {
fn from_endpoint<E: Endpoint>(name: &str, endpoint: E) -> RequestHandler {
let index = move |request: HttpRequest<Context>| -> FutureResponse {
let context = request.state();
let future = Query::from_request(&request, &())
.map(|query: Query<E::Request>| query.into_inner())
.and_then(|query| endpoint.handle(context, query).map_err(From::from))
.and_then(|value| Ok(HttpResponse::Ok().json(value)))
.into_future();
Box::new(future)
};
Self {
name: name.to_owned(),
method: actix_web::http::Method::GET,
inner: Arc::from(index) as Arc<RawHandler>,
}
}
}
At this stage, it would be enough just to add handlers for POST requests, as we have created a trait that is independent from the implementation details. However, we found that this solution was not quite advanced enough.
The Drawbacks of the `Endpoint` Trait
A large amount of auxiliary code is generated when a handler is written:
// A structure with the context of the handler.
struct ElementCountEndpoint {
elements: Rc<RefCell<Vec<Something>>>,
}
// Implementation of the `Endpoint` trait.
impl Endpoint for ElementCountEndpoint {
type Request = ();
type Result = usize;
fn handle(&self, context: &Context, _request: ()) -> Result<usize, io::Error> {
Ok(self.elements.borrow().len())
}
}
// Installation of the handler in the backend.
let endpoint = ElementCountEndpoint::new(elements.clone());
let handler = RequestHandler::from_endpoint("/v1/element_count", endpoint);
actix_backend.endpoint(handler);
Ideally, we need to be able to pass a simple closure as a handler, thus significantly reducing the amount of syntactic noise.
let elements = elements.clone();
actix_backend.endpoint("/v1/elements_count", move || {
Ok(elements.borrow().len())
});
Below we will discuss how this can be done.
Light Immersion into Generic Programming
We need to add the ability to automatically generate an adapter that implements the `Endpoint` trait with the correct associated types. The input will consist only of a closure with an HTTP request handler.
Arguments and the result of the closure can have different types, so we have to work with methods overloading here. Rust does not support overloading directly but allows it to be emulated using the `Into` and `From` traits.
In addition, the returned type of the closure value does not have to match the returned value of the `Endpoint` implementation. To manipulate this type, it must be extracted from the type of the received closure.
Fetching Types from the `Fn` Trait
In Rust, each closure has its own unique type, which cannot be explicitly indicated in the program. For manipulations with closures, we use the `Fn` trait. The trait contains the signature of the function with the types of the arguments and of the returned value, however, retrieving these elements separately is not easily done.
The main idea is to use an auxiliary structure of the following form:
/// Simplified example of extracting types from an F closure: Fn(A) -> B.
struct SimpleExtractor<A, B, F>
{
// The original function.
inner: F,
_a: PhantomData<A>,
_b: PhantomData<B>,
}
We have to use PhantomData, since Rust requires that all the generic parameters are indicated in the definition of the structure. However, the type of closure or function F itself is not a generic one (although it implements a generic `Fn` trait). The type parameters A and B are not used in it directly.
It is this restriction of the Rust type system that precludes us from applying a simpler strategy by implementing the `Endpoint` trait directly for closures:
impl<A, B, F> Endpoint for F where F: Fn(&Context, A) -> B {
type Request = A;
type Response = B;
fn handle(&self, context: &Context, request: A) -> Result<B, io::Error> {
// ...
}
}
In the case above, the compiler returns an error:
error[E0207]: the type parameter `A` is not constrained by the impl trait, self type, or predicates
--> src/main.rs:10:6
|
10 | impl<A, B, F> Endpoint for F where F: Fn(&Context, A) -> B {
| ^ unconstrained type parameter
The auxiliary structure SimpleExtractor makes it possible to describe the conversion of `From`. This conversion allows us to save any function and extract the types of its arguments:
impl<A, B, F> From<F> for SimpleExtractor<A, B, F>
where
F: Fn(&Context, A) -> B,
A: DeserializeOwned,
B: Serialize,
{
fn from(inner: F) -> Self {
SimpleExtractor {
inner,
_a: PhantomData,
_b: PhantomData,
}
}
}
The following code compiles successfully:
#[derive(Deserialize)]
struct Query {
a: i32,
b: String,
};
// Verification of the ordinary structure.
fn my_handler(_: &Context, q: Query) -> String {
format!("{} has {} apples.", q.b, q.a)
}
let fn_extractor = SimpleExtractor::from(my_handler);
// Verification of the closure.
let c = 15;
let my_closure = |_: &Context, q: Query| -> String {
format!("{} has {} apples, but Alice has {}", q.b, q.a, c)
};
let closure_extractor = SimpleExtractor::from(my_closure);
Specialization and Marker Types
Now we have a function with explicitly parameterized argument types, which can be used instead of the `Endpoint` trait. For example, we can easily implement the conversion from SimpleExtractor into RequestHandler. Still, this is not a complete solution. We need to somehow distinguish between the handlers for GET and POST requests at the type level (and between synchronous and asynchronous handlers). In this task, marker types come to our aid.
Firstly, let’s rewrite SimpleExtractor so that it can distinguish between synchronous and asynchronous results. At the same time, we will implement the `From` trait for each of the cases. Note that traits can be implemented for specific variants of generic structures.
/// Generic handler for HTTP-requests.
pub struct With<Q, I, R, F> {
/// A specific handler function.
pub handler: F,
/// Structure type containing the parameters of the request.
_query_type: PhantomData<Q>,
/// Type of the request result.
_item_type: PhantomData<I>,
/// Type of the value returned by the handler.
/// Note that this value can differ from the result of the request.
_result_type: PhantomData<R>,
}
// Implementation of an ordinary synchronous returned value.
impl<Q, I, F> From<F> for With<Q, I, Result<I>, F>
where
F: Fn(&ServiceApiState, Q) -> Result<I>,
{
fn from(handler: F) -> Self {
Self {
handler,
_query_type: PhantomData,
_item_type: PhantomData,
_result_type: PhantomData,
}
}
}
// Implementation of an asynchronous request handler.
impl<Q, I, F> From<F> for With<Q, I, FutureResult<I>, F>
where
F: Fn(&ServiceApiState, Q) -> FutureResult<I>,
{
fn from(handler: F) -> Self {
Self {
handler,
_query_type: PhantomData,
_item_type: PhantomData,
_result_type: PhantomData,
}
}
}
Now we need to declare the structure which will combine the request handler with its name and type:
#[derive(Debug)]
pub struct NamedWith<Q, I, R, F, K> {
/// The name of the handler.
pub name: String,
/// The handler with the extracted types.
pub inner: With<Q, I, R, F>,
/// The type of the handler.
_kind: PhantomData<K>,
}
Next, we declare several empty structures that will act as marker types. Markers will allow us to implement for each handler their own code to convert the handler into the previously described RequestHandler.
/// A handler that does not change the state of the service. In HTTP, GET-requests correspond to this
// handler.
pub struct Immutable;
/// A handler that changes the state of the service. In HTTP, POST, PUT, UPDATE and other similar
//requests correspond to this handler, but for the current case POST will suffice.
pub struct Mutable;
Now we can define four different implementations of the `From` trait for all combinations of template parameters R and K (the returned value of the handler and the type of the request).
// Implementation of a synchronous handler of GET requests.
impl<Q, I, F> From<NamedWith<Q, I, Result<I>, F, Immutable>> for RequestHandler
where
F: Fn(&ServiceApiState, Q) -> Result<I> + 'static + Send + Sync + Clone,
Q: DeserializeOwned + 'static,
I: Serialize + 'static,
{
fn from(f: NamedWith<Q, I, Result<I>, F, Immutable>) -> Self {
let handler = f.inner.handler;
let index = move |request: HttpRequest| -> FutureResponse {
let context = request.state();
let future = Query::from_request(&request, &())
.map(|query: Query<Q>| query.into_inner())
.and_then(|query| handler(context, query).map_err(From::from))
.and_then(|value| Ok(HttpResponse::Ok().json(value)))
.into_future();
Box::new(future)
};
Self {
name: f.name,
method: actix_web::http::Method::GET,
inner: Arc::from(index) as Arc<RawHandler>,
}
}
}
// Implementation of a synchronous handler of POST requests.
impl<Q, I, F> From<NamedWith<Q, I, Result<I>, F, Mutable>> for RequestHandler
where
F: Fn(&ServiceApiState, Q) -> Result<I> + 'static + Send + Sync + Clone,
Q: DeserializeOwned + 'static,
I: Serialize + 'static,
{
fn from(f: NamedWith<Q, I, Result<I>, F, Mutable>) -> Self {
let handler = f.inner.handler;
let index = move |request: HttpRequest| -> FutureResponse {
let handler = handler.clone();
let context = request.state().clone();
request
.json()
.from_err()
.and_then(move |query: Q| {
handler(&context, query)
.map(|value| HttpResponse::Ok().json(value))
.map_err(From::from)
})
.responder()
};
Self {
name: f.name,
method: actix_web::http::Method::POST,
inner: Arc::from(index) as Arc<RawHandler>,
}
}
}
// Implementation of an asynchronous handler of GET requests.
impl<Q, I, F> From<NamedWith<Q, I, FutureResult<I>, F, Immutable>> for RequestHandler
where
F: Fn(&ServiceApiState, Q) -> FutureResult<I> + 'static + Clone + Send + Sync,
Q: DeserializeOwned + 'static,
I: Serialize + 'static,
{
fn from(f: NamedWith<Q, I, FutureResult<I>, F, Immutable>) -> Self {
let handler = f.inner.handler;
let index = move |request: HttpRequest| -> FutureResponse {
let context = request.state().clone();
let handler = handler.clone();
Query::from_request(&request, &())
.map(move |query: Query<Q>| query.into_inner())
.into_future()
.and_then(move |query| handler(&context, query).map_err(From::from))
.map(|value| HttpResponse::Ok().json(value))
.responder()
};
Self {
name: f.name,
method: actix_web::http::Method::GET,
inner: Arc::from(index) as Arc<RawHandler>,
}
}
}
// Implementation of an asynchronous handler of POST requests.
impl<Q, I, F> From<NamedWith<Q, I, FutureResult<I>, F, Mutable>> for RequestHandler
where
F: Fn(&ServiceApiState, Q) -> FutureResult<I> + 'static + Clone + Send + Sync,
Q: DeserializeOwned + 'static,
I: Serialize + 'static,
{
fn from(f: NamedWith<Q, I, FutureResult<I>, F, Mutable>) -> Self {
let handler = f.inner.handler;
let index = move |request: HttpRequest| -> FutureResponse {
let handler = handler.clone();
let context = request.state().clone();
request
.json()
.from_err()
.and_then(move |query: Q| {
handler(&context, query)
.map(|value| HttpResponse::Ok().json(value))
.map_err(From::from)
})
.responder()
};
Self {
name: f.name,
method: actix_web::http::Method::POST,
inner: Arc::from(index) as Arc<RawHandler>,
}
}
}
Facade for the Backend
The final step is to devise a facade that would accept closures and add them into the corresponding backend. In the given case, we have a single backend — actix-web. However, there is the potential of additional implementation behind the facade. For example: a generator of Swagger specifications.
pub struct ServiceApiScope {
actix_backend: actix::ApiBuilder,
}
impl ServiceApiScope {
/// This method adds an Immutable handler to all backends.
pub fn endpoint<Q, I, R, F, E>(&mut self, name: &'static str, endpoint: E) -> &mut Self
where
// Here we list the typical restrictions which we have encountered earlier:
Q: DeserializeOwned + 'static,
I: Serialize + 'static,
F: Fn(&ServiceApiState, Q) -> R + 'static + Clone,
E: Into<With<Q, I, R, F>>,
// Note that the list of restrictions includes the conversion from NamedWith into RequestHandler
// we have implemented earlier.
RequestHandler: From<NamedWith<Q, I, R, F, Immutable>>,
{
self.actix_backend.endpoint(name, endpoint);
self
}
/// A similar method for Mutable handlers.
pub fn endpoint_mut<Q, I, R, F, E>(&mut self, name: &'static str, endpoint: E) -> &mut Self
where
Q: DeserializeOwned + 'static,
I: Serialize + 'static,
F: Fn(&ServiceApiState, Q) -> R + 'static + Clone,
E: Into<With<Q, I, R, F>>,
RequestHandler: From<NamedWith<Q, I, R, F, Mutable>>,
{
self.actix_backend.endpoint_mut(name, endpoint);
self
}
Note how the types of the request parameters, the type of the request result, and the synchrony/asynchrony of the handler are derived automatically from its signature. Additionally, we need to explicitly specify the name and type of the request.
Drawbacks of the Approach
The approach described above, despite being quite effective, has its drawbacks. In particular, endpoint and endpoint_mut methods should consider the implementation peculiarities of specific backends. This restriction prevents us from adding backends on the go, though this functionality is rarely required.
Another issue is that we cannot define the specialization of a handler without additional arguments. In other words, if we write the following code, it will not be compiled as it is in conflict with the existing generic implementation:
impl<(), I, F> From<F> for With<(), I, Result<I>, F>
where
F: Fn(&ServiceApiState) -> Result<I>,
{
fn from(handler: F) -> Self {
Self {
handler,
_query_type: PhantomData,
_item_type: PhantomData,
_result_type: PhantomData,
}
}
}
As a result, requests that do not have any parameters must still accept the JSON string null, which is deserialized into (). This problem could be solved by specialization in C ++ style, but for now it is available only in the nightly version of the compiler and it is not clear when it will become a stable feature.
Similarly, the type of the returned value cannot be specialized. Even if the request does not imply a certain type of the returned value, it will still pass JSON with null.
Decoding the URL query in GET requests also imposes some unobvious restrictions on the type of parameters, but this issue relates rather to the peculiarities of the serde-urlencoded implementation.
Conclusion
As described above, we have implemented an improved API, which allows for a simple and clear creation of handlers, without the need to worry about web specifics. These handlers can work with any backend or even with several backends simultaneously.
- Web: Bitfury.com, Exonum.com
- Social Media: Facebook, Twitter, LinkedIn
- R&D: Research and White Papers
Автор: BitfuryRussia