El ecosistema de Rust todav铆a est谩 creciendo. Como resultado, las nuevas bibliotecas con funcionalidad mejorada se lanzan con frecuencia a la comunidad de desarrolladores, mientras que las bibliotecas m谩s antiguas se vuelven obsoletas. Cuando inicialmente dise帽amos Exonum, utilizamos el framework web Iron. En este art铆culo, describimos c贸mo portamos el framework Exonum a actix-web usando programaci贸n gen茅rica.
Exonum sobre hierro
En la plataforma Exonum, el marco de hierro se utiliz贸 sin abstracciones. Instalamos manejadores para ciertos recursos y obtuvimos par谩metros de solicitud al analizar URL usando m茅todos auxiliares; el resultado se devolvi贸 simplemente en forma de cadena.
El proceso se parec铆a (aproximadamente) al siguiente:
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"); }
Adem谩s, utilizamos algunos complementos de middleware en forma de encabezados CORS. Utilizamos mount para fusionar todos los controladores en una sola API.
Nuestra decisi贸n de alejarnos del hierro
Iron era una buena biblioteca, con muchos complementos. Sin embargo, fue escrito en los d铆as en que no exist铆an proyectos como futuros y
tokio .
La arquitectura de Iron implica el procesamiento de solicitudes sincr贸nicas, que pueden verse f谩cilmente afectadas por una gran cantidad de conexiones abiertas simult谩neamente. Para ser escalable, Iron necesitaba volverse as铆ncrono, lo que implicar铆a repensar y reescribir todo el marco. Como resultado, hemos visto una desviaci贸n gradual del uso de Iron por parte de ingenieros de software.
Por qu茅 elegimos Actix-Web
Actix-web es un marco popular que ocupa un lugar
destacado en los
puntos de referencia de TechEmpower . Tiene una comunidad de desarrolladores activa, a diferencia de Iron, y tiene una API bien dise帽ada y una implementaci贸n de alta calidad basada en el marco del actor actix. Las solicitudes son procesadas asincr贸nicamente por el grupo de subprocesos; si la solicitud procesa el p谩nico, el actor se reinicia autom谩ticamente.
Anteriormente, se plante贸 la preocupaci贸n de que actix-web conten铆a una gran cantidad de c贸digo inseguro. Sin embargo, la cantidad de c贸digo inseguro se redujo significativamente cuando el marco se reescribi贸 en un lenguaje de programaci贸n seguro: Rust. Los ingenieros de Bitfury han revisado este c贸digo ellos mismos y se sienten seguros de su estabilidad a largo plazo.
Para el marco Exonum, el cambio a actix resolvi贸 el problema de la estabilidad de la operaci贸n. El marco de Iron podr铆a fallar si hubiera una gran cantidad de conexiones. Tambi茅n hemos descubierto que la API actix-web es m谩s simple, m谩s productiva y m谩s unificada. Estamos seguros de que a los usuarios y desarrolladores les resultar谩 m谩s f谩cil usar la interfaz de programaci贸n Exonum, que ahora puede operar m谩s r谩pido gracias al dise帽o web actix.
Lo que requerimos de un marco web
Durante este proceso, nos dimos cuenta de que era importante para nosotros no simplemente cambiar los marcos, sino tambi茅n dise帽ar una nueva arquitectura API independiente de cualquier marco web espec铆fico. Dicha arquitectura permitir铆a crear controladores, con poca o ninguna preocupaci贸n por los detalles web, y transferirlos a cualquier backend. Esta concepci贸n puede implementarse escribiendo una interfaz que aplique tipos y rasgos b谩sicos.
Para comprender c贸mo debe verse esta interfaz, definamos qu茅 es realmente cualquier API HTTP:
- Las solicitudes son hechas exclusivamente por clientes; el servidor solo responde a ellas (el servidor no inicia solicitudes).
- Solicita leer datos o cambiar datos.
- Como resultado del procesamiento de la solicitud, el servidor devuelve una respuesta, que contiene los datos requeridos, en caso de 茅xito; o informaci贸n sobre el error, en caso de falla.
Si vamos a analizar todas las capas de abstracci贸n, resulta que cualquier solicitud HTTP es solo una llamada de funci贸n:
fn request(context: &ServiceContext, query: Query) -> Result<Response, ServiceError>
Todo lo dem谩s puede considerarse una extensi贸n de esta entidad b谩sica. Por lo tanto, para ser independientes de una implementaci贸n espec铆fica de un marco web, necesitamos escribir controladores en un estilo similar al ejemplo anterior.
Rasgo `Punto final` para el procesamiento gen茅rico de solicitudes HTTP
El enfoque m谩s simple y directo ser铆a declarar el rasgo `Endpoint`, que describe las implementaciones de solicitudes espec铆ficas:
Ahora necesitamos implementar este controlador en un marco espec铆fico. Por ejemplo, en actix-web tiene el siguiente aspecto:
Podemos usar estructuras para pasar par谩metros de solicitud a trav茅s del contexto. Actix-web puede deserializar par谩metros autom谩ticamente usando serde. Por ejemplo, a = 15 & b = hola se deserializa en una estructura como esta:
#[derive(Deserialize)] struct SimpleQuery { a: i32, b: String, }
Esta funcionalidad de deserializaci贸n concuerda bien con el tipo asociado Solicitud del rasgo `Punto final`.
A continuaci贸n, ideemos un adaptador que envuelva una implementaci贸n espec铆fica de 'Endpoint' en un RequestHandler para actix-web. Preste atenci贸n al hecho de que al hacerlo, la informaci贸n sobre los tipos de Solicitud y Respuesta desaparece. Esta t茅cnica se denomina borrado de tipo: transforma el env铆o est谩tico en uno din谩mico.
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>, } } }
En esta etapa, ser铆a suficiente agregar controladores para las solicitudes POST, ya que hemos creado un rasgo que es independiente de los detalles de implementaci贸n. Sin embargo, descubrimos que esta soluci贸n no era lo suficientemente avanzada.
Los inconvenientes del rasgo `Punto final`
Se genera una gran cantidad de c贸digo auxiliar cuando se escribe un controlador:
Idealmente, necesitamos poder pasar un cierre simple como manejador, reduciendo as铆 significativamente la cantidad de ruido sint谩ctico.
let elements = elements.clone(); actix_backend.endpoint("/v1/elements_count", move || { Ok(elements.borrow().len()) });
A continuaci贸n discutiremos c贸mo se puede hacer esto.
Inmersi贸n ligera en programaci贸n gen茅rica
Necesitamos agregar la capacidad de generar autom谩ticamente un adaptador que implemente el rasgo `Endpoint` con los tipos asociados correctos. La entrada consistir谩 solo en un cierre con un controlador de solicitud HTTP.
Los argumentos y el resultado del cierre pueden tener diferentes tipos, por lo que tenemos que trabajar con m茅todos de sobrecarga aqu铆. Rust no admite la sobrecarga directamente, pero permite que se emule usando los rasgos 'Into' y 'From'.
Adem谩s, el tipo devuelto del valor de cierre no tiene que coincidir con el valor devuelto de la implementaci贸n `Endpoint`. Para manipular este tipo, debe extraerse del tipo del cierre recibido.
Obteniendo tipos del rasgo `Fn`
En Rust, cada cierre tiene su propio tipo 煤nico, que no se puede indicar expl铆citamente en el programa. Para manipulaciones con cierres, utilizamos el rasgo `Fn`. El rasgo contiene la firma de la funci贸n con los tipos de argumentos y del valor devuelto, sin embargo, no es f谩cil recuperar estos elementos por separado.
La idea principal es utilizar una estructura auxiliar de la siguiente forma:
Tenemos que usar PhantomData, ya que Rust requiere que todos los par谩metros gen茅ricos est茅n indicados en la definici贸n de la estructura. Sin embargo, el tipo de cierre o funci贸n F en s铆 no es gen茅rico (aunque implementa un rasgo gen茅rico `Fn`). Los par谩metros de tipo A y B no se utilizan directamente en 茅l.
Es esta restricci贸n del sistema de tipo Rust lo que nos impide aplicar una estrategia m谩s simple al implementar el rasgo `Endpoint` directamente para los cierres:
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> {
En el caso anterior, el compilador devuelve un 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
La estructura auxiliar SimpleExtractor permite describir la conversi贸n de `From`. Esta conversi贸n nos permite guardar cualquier funci贸n y extraer los tipos de sus argumentos:
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, } } }
El siguiente c贸digo se compila correctamente:
#[derive(Deserialize)] struct Query { a: i32, b: String, };
Especializaci贸n y tipos de marcadores
Ahora tenemos una funci贸n con tipos de argumentos parametrizados expl铆citamente, que se pueden usar en lugar del rasgo `Endpoint`. Por ejemplo, podemos implementar f谩cilmente la conversi贸n de SimpleExtractor a RequestHandler. A煤n as铆, esta no es una soluci贸n completa. Necesitamos distinguir de alguna manera entre los controladores para las solicitudes GET y POST a nivel de tipo (y entre los controladores s铆ncronos y as铆ncronos). En esta tarea, los tipos de marcadores nos ayudan.
En primer lugar, reescribamos SimpleExtractor para que pueda distinguir entre resultados s铆ncronos y as铆ncronos. Al mismo tiempo, implementaremos el rasgo `De` para cada uno de los casos. Tenga en cuenta que los rasgos se pueden implementar para variantes espec铆ficas de estructuras gen茅ricas.
Ahora necesitamos declarar la estructura que combinar谩 el manejador de solicitudes con su nombre y tipo:
#[derive(Debug)] pub struct NamedWith<Q, I, R, F, K> {
A continuaci贸n, declaramos varias estructuras vac铆as que actuar谩n como tipos de marcadores. Los marcadores nos permitir谩n implementar para cada controlador su propio c贸digo para convertir el controlador en el RequestHandler descrito anteriormente.
Ahora podemos definir cuatro implementaciones diferentes del rasgo `De` para todas las combinaciones de par谩metros de plantilla R y K (el valor devuelto del controlador y el tipo de solicitud).
Fachada para el backend
El paso final es dise帽ar una fachada que acepte cierres y los agregue al backend correspondiente. En el caso dado, tenemos un 煤nico backend: actix-web. Sin embargo, existe el potencial de una implementaci贸n adicional detr谩s de la fachada. Por ejemplo: un generador de especificaciones Swagger.
pub struct ServiceApiScope { actix_backend: actix::ApiBuilder, } impl ServiceApiScope {
Observe c贸mo los tipos de los par谩metros de solicitud, el tipo del resultado de la solicitud y la sincron铆a / asincron铆a del controlador se derivan autom谩ticamente de su firma. Adem谩s, debemos especificar expl铆citamente el nombre y el tipo de la solicitud.
Inconvenientes del enfoque
El enfoque descrito anteriormente, a pesar de ser bastante efectivo, tiene sus inconvenientes. En particular, los
m茅todos endpoint y endpoint_mut deben considerar las peculiaridades de implementaci贸n de backends espec铆ficos . Esta restricci贸n nos impide agregar backends sobre la marcha, aunque esta funcionalidad rara vez es necesaria.
Otro problema es que
no podemos definir la especializaci贸n de un controlador sin argumentos adicionales . En otras palabras, si escribimos el siguiente c贸digo, no se compilar谩 ya que est谩 en conflicto con la implementaci贸n gen茅rica existente:
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, } } }
Como resultado, las solicitudes que no tienen ning煤n par谩metro a煤n deben aceptar la cadena JSON nula, que se deserializa en (). Este problema podr铆a resolverse mediante la especializaci贸n en estilo C ++, pero por ahora est谩 disponible solo en la versi贸n nocturna del compilador y no est谩 claro cu谩ndo se convertir谩 en una caracter铆stica estable.
Del mismo modo,
el tipo del valor devuelto no puede ser especializado . Incluso si la solicitud no implica un cierto tipo de valor devuelto, seguir谩 pasando JSON con nulo.
La decodificaci贸n de la consulta de URL en las solicitudes GET tambi茅n impone algunas restricciones obvias sobre el tipo de par谩metros , pero este problema se relaciona m谩s bien con las peculiaridades de la implementaci贸n codificada por el servidor.
Conclusi贸n
Como se describi贸 anteriormente, hemos implementado una API mejorada, que permite una creaci贸n simple y clara de controladores, sin la necesidad de preocuparse por los detalles web. Estos manejadores pueden funcionar con cualquier backend o incluso con varios backends simult谩neamente.