Generische Methoden im Rost: Wie Exonum von Eisen zu Actix-Web verlagert wurde

Das Rust-Ökosystem wächst weiter. Infolgedessen werden häufig neue Bibliotheken mit verbesserter Funktionalität für die Entwicklergemeinschaft freigegeben, während ältere Bibliotheken veraltet sind. Als wir Exonum entworfen haben, haben wir das Iron Web-Framework verwendet. In diesem Artikel beschreiben wir, wie wir das Exonum-Framework mithilfe generischer Programmierung auf actix-web portiert haben.



Exonum auf Eisen


In der Exonum-Plattform wurde das Iron-Framework ohne Abstraktionen verwendet. Wir haben Handler für bestimmte Ressourcen installiert und Anforderungsparameter erhalten, indem wir URLs mithilfe von Hilfsmethoden analysiert haben. Das Ergebnis wurde einfach in Form einer Zeichenfolge zurückgegeben.

Der Prozess sah (ungefähr) wie folgt aus:

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"); } 

Zusätzlich haben wir einige Middleware-Plugins in Form von CORS-Headern verwendet. Wir haben mount verwendet, um alle Handler in einer einzigen API zusammenzuführen.

Unsere Entscheidung, uns von Eisen zu entfernen


Iron war eine gute Bibliothek mit vielen Plugins. Es wurde jedoch in den Tagen geschrieben, als es solche Projekte wie Futures und Tokio nicht gab.

Die Architektur von Iron beinhaltet die synchrone Verarbeitung von Anforderungen, die leicht durch eine große Anzahl gleichzeitig geöffneter Verbindungen beeinflusst werden kann. Um skalierbar zu sein, musste Iron asynchron werden, was das Überdenken und Umschreiben des gesamten Frameworks beinhalten würde. Infolgedessen haben wir eine allmähliche Abkehr von der Verwendung von Eisen durch Softwareentwickler festgestellt.

Warum wir uns für Actix-Web entschieden haben


Actix-Web ist ein beliebtes Framework, das bei TechEmpower-Benchmarks einen hohen Stellenwert einnimmt . Im Gegensatz zu Iron verfügt es über eine aktive Entwickler-Community, eine gut gestaltete API und eine qualitativ hochwertige Implementierung, die auf dem Actix-Actor-Framework basiert. Anforderungen werden vom Thread-Pool asynchron verarbeitet. Wenn die Verarbeitung von Anforderungen in Panik gerät, wird der Akteur automatisch neu gestartet.

Zuvor wurden Bedenken geäußert, dass actix-web viel unsicheren Code enthält. Die Menge an unsicherem Code wurde jedoch erheblich reduziert, als das Framework in einer sicheren Programmiersprache - Rust - neu geschrieben wurde. Die Ingenieure von Bitfury haben diesen Code selbst überprüft und sind zuversichtlich, dass er langfristig stabil ist.

Für das Exonum-Framework löste die Umstellung auf actix das Problem der Betriebsstabilität. Das Eisengerüst könnte versagen, wenn es eine große Anzahl von Verbindungen gäbe. Wir haben auch festgestellt, dass die actix-web-API einfacher, produktiver und einheitlicher ist. Wir sind zuversichtlich, dass Benutzer und Entwickler die Exonum-Programmierschnittstelle, die dank des actix-web-Designs jetzt schneller arbeiten kann, leichter nutzen können.

Was wir von einem Web Framework benötigen


Während dieses Prozesses wurde uns klar, dass es für uns wichtig ist, nicht nur Frameworks zu verschieben, sondern auch eine neue API-Architektur zu entwickeln, die von einem bestimmten Webframework unabhängig ist. Eine solche Architektur würde es ermöglichen, Handler zu erstellen, ohne sich um Webspezifikationen zu kümmern, und diese auf ein beliebiges Backend zu übertragen. Diese Konzeption kann implementiert werden, indem ein Frontend geschrieben wird, das grundlegende Typen und Merkmale anwendet.

Um zu verstehen, wie dieses Frontend aussehen muss, definieren wir, was eine HTTP-API wirklich ist:

  • Anfragen werden ausschließlich von Kunden gestellt; Der Server antwortet nur auf sie (der Server initiiert keine Anforderungen).
  • Anforderungen lesen entweder Daten oder ändern Daten.
  • Als Ergebnis der Anforderungsverarbeitung gibt der Server im Erfolgsfall eine Antwort zurück, die die erforderlichen Daten enthält. oder Informationen über den Fehler im Fehlerfall.

Wenn wir alle Abstraktionsschichten analysieren wollen, stellt sich heraus, dass jede HTTP-Anforderung nur ein Funktionsaufruf ist:

 fn request(context: &ServiceContext, query: Query) -> Result<Response, ServiceError> 

Alles andere kann als Erweiterung dieser grundlegenden Entität betrachtet werden. Um von einer bestimmten Implementierung eines Webframeworks unabhängig zu sein, müssen wir daher Handler in einem ähnlichen Stil wie im obigen Beispiel schreiben.

Merkmal `Endpunkt` für die generische Verarbeitung von HTTP-Anfragen


Der einfachste und einfachste Ansatz wäre die Deklaration des Endpunkt-Merkmals, das die Implementierung spezifischer Anforderungen beschreibt:

 // 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>; } 

Jetzt müssen wir diesen Handler in einem bestimmten Framework implementieren. In actix-web sieht es beispielsweise folgendermaßen aus:

 // 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>, } 

Wir können Strukturen verwenden, um Anforderungsparameter durch den Kontext zu übergeben. Actix-web kann Parameter mithilfe von serde automatisch deserialisieren. Zum Beispiel wird a = 15 & b = Hallo in eine Struktur wie diese deserialisiert:

 #[derive(Deserialize)] struct SimpleQuery { a: i32, b: String, } 

Diese Deserialisierungsfunktionalität stimmt gut mit dem zugehörigen Typ Anforderung vom Merkmal "Endpunkt" überein.

Als nächstes entwickeln wir einen Adapter, der eine bestimmte Implementierung von `Endpoint` in einen RequestHandler für actix-web einbindet. Beachten Sie, dass dabei die Informationen zu den Anforderungs- und Antworttypen verschwinden. Diese Technik wird als Typlöschung bezeichnet - sie wandelt statisches Dispatching in ein dynamisches um.

 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>, } } } 

Zu diesem Zeitpunkt würde es ausreichen, nur Handler für POST-Anforderungen hinzuzufügen, da wir ein Merkmal erstellt haben, das von den Implementierungsdetails unabhängig ist. Wir haben jedoch festgestellt, dass diese Lösung nicht weit genug fortgeschritten ist.

Die Nachteile des Endpunktmerkmals


Beim Schreiben eines Handlers wird eine große Menge an Hilfscode generiert:

 // 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); 

Idealerweise müssen wir in der Lage sein, einen einfachen Verschluss als Handler zu übergeben, wodurch das syntaktische Rauschen erheblich reduziert wird.

 let elements = elements.clone(); actix_backend.endpoint("/v1/elements_count", move || { Ok(elements.borrow().len()) }); 

Im Folgenden werden wir diskutieren, wie dies getan werden kann.

Leichtes Eintauchen in die generische Programmierung


Wir müssen die Möglichkeit hinzufügen, automatisch einen Adapter zu generieren, der das Merkmal "Endpunkt" mit den richtigen zugehörigen Typen implementiert. Die Eingabe besteht nur aus einem Abschluss mit einem HTTP-Anforderungshandler.

Argumente und das Ergebnis des Abschlusses können unterschiedliche Typen haben, daher müssen wir hier mit Methoden arbeiten, die überladen sind. Rust unterstützt das direkte Überladen nicht, ermöglicht jedoch die Emulation mit den Merkmalen "Into" und "From".

Außerdem muss der zurückgegebene Typ des Abschlusswerts nicht mit dem zurückgegebenen Wert der Endpoint-Implementierung übereinstimmen. Um diesen Typ zu manipulieren, muss er aus dem Typ des empfangenen Abschlusses extrahiert werden.

Abrufen von Typen aus dem Merkmal "Fn"


In Rust hat jeder Verschluss seinen eigenen eindeutigen Typ, der im Programm nicht explizit angegeben werden kann. Für Manipulationen mit Verschlüssen verwenden wir das Merkmal "Fn". Das Merkmal enthält die Signatur der Funktion mit den Typen der Argumente und des zurückgegebenen Werts. Das separate Abrufen dieser Elemente ist jedoch nicht einfach.

Die Hauptidee besteht darin, eine Hilfsstruktur der folgenden Form zu verwenden:

 /// 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>, } 

Wir müssen PhantomData verwenden, da Rust erfordert, dass alle generischen Parameter in der Definition der Struktur angegeben werden. Die Art des Verschlusses oder der Funktion F selbst ist jedoch keine generische (obwohl sie ein generisches "Fn" -Eigenschaft implementiert). Die Typparameter A und B werden darin nicht direkt verwendet.

Es ist diese Einschränkung des Systems vom Typ Rust, die uns daran hindert, eine einfachere Strategie anzuwenden, indem das Merkmal "Endpunkt" direkt für Schließungen implementiert wird:

 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> { // ... } } 

Im obigen Fall gibt der Compiler einen Fehler zurück:

 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 

Die Hilfsstruktur SimpleExtractor ermöglicht es, die Konvertierung von `From` zu beschreiben. Diese Konvertierung ermöglicht es uns, jede Funktion zu speichern und die Typen ihrer Argumente zu extrahieren:

 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, } } } 

Der folgende Code wird erfolgreich kompiliert:

 #[derive(Deserialize)] struct Query { a: i32, b: String, }; // Verification of the ordinary structure. fn my_handler(_: &Context, q: Query) -> String { format!("{} has {} apples.", qb, qa) } 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 {}", qb, qa, c) }; let closure_extractor = SimpleExtractor::from(my_closure); 

Spezialisierung und Markertypen


Jetzt haben wir eine Funktion mit explizit parametrisierten Argumenttypen, die anstelle des Merkmals "Endpoint" verwendet werden kann. Zum Beispiel können wir die Konvertierung von SimpleExtractor in RequestHandler einfach implementieren. Dies ist jedoch keine vollständige Lösung. Wir müssen irgendwie zwischen den Handlern für GET- und POST-Anforderungen auf Typebene (und zwischen synchronen und asynchronen Handlern) unterscheiden. Bei dieser Aufgabe helfen uns Markertypen.

Lassen Sie uns zunächst SimpleExtractor neu schreiben, damit zwischen synchronen und asynchronen Ergebnissen unterschieden werden kann. Gleichzeitig werden wir für jeden Fall das Merkmal "Von" implementieren. Beachten Sie, dass Merkmale für bestimmte Varianten generischer Strukturen implementiert werden können.

 /// 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, } } } 

Jetzt müssen wir die Struktur deklarieren, die den Anforderungshandler mit seinem Namen und Typ kombiniert:

 #[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>, } 

Als nächstes deklarieren wir mehrere leere Strukturen, die als Markertypen fungieren. Mit Markern können wir für jeden Handler einen eigenen Code implementieren, um den Handler in den zuvor beschriebenen RequestHandler zu konvertieren.

 /// 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; 

Jetzt können wir vier verschiedene Implementierungen des Merkmals "Von" für alle Kombinationen der Vorlagenparameter R und K definieren (den zurückgegebenen Wert des Handlers und den Typ der Anforderung).

 // 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>, } } } 

Fassade für das Backend


Der letzte Schritt besteht darin, eine Fassade zu entwerfen, die Verschlüsse akzeptiert und diese in das entsprechende Backend einfügt. Im gegebenen Fall haben wir ein einziges Backend - actix-web. Hinter der Fassade besteht jedoch das Potenzial einer zusätzlichen Umsetzung. Zum Beispiel: ein Generator mit Swagger-Spezifikationen.

 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 } 

Beachten Sie, wie die Typen der Anforderungsparameter, der Typ des Anforderungsergebnisses und die Synchronität / Asynchronität des Handlers automatisch von seiner Signatur abgeleitet werden. Zusätzlich müssen wir den Namen und den Typ der Anfrage explizit angeben.

Nachteile des Ansatzes


Der oben beschriebene Ansatz hat, obwohl er ziemlich effektiv ist, seine Nachteile. Insbesondere sollten die Methoden endpoint und endpoint_mut die Besonderheiten der Implementierung bestimmter Backends berücksichtigen . Diese Einschränkung verhindert, dass wir unterwegs Backends hinzufügen können, obwohl diese Funktionalität selten erforderlich ist.

Ein weiteres Problem ist, dass wir die Spezialisierung eines Handlers nicht ohne zusätzliche Argumente definieren können . Mit anderen Worten, wenn wir den folgenden Code schreiben, wird er nicht kompiliert, da er im Widerspruch zur vorhandenen generischen Implementierung steht:

 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, } } } 

Daher müssen Anforderungen, die keine Parameter haben, weiterhin die JSON-Zeichenfolge null akzeptieren, die in () deserialisiert wird. Dieses Problem könnte durch Spezialisierung auf C ++ - Stil gelöst werden. Derzeit ist es jedoch nur in der nächtlichen Version des Compilers verfügbar und es ist nicht klar, wann es zu einer stabilen Funktion wird.

Ebenso kann der Typ des zurückgegebenen Werts nicht spezialisiert werden . Auch wenn die Anforderung keinen bestimmten Typ des zurückgegebenen Werts impliziert, wird JSON mit null übergeben.

Das Dekodieren der URL-Abfrage in GET-Anforderungen führt auch zu einigen nicht offensichtlichen Einschränkungen für den Parametertyp . Dieses Problem bezieht sich jedoch eher auf die Besonderheiten der serde-urlencodierten Implementierung.

Fazit


Wie oben beschrieben, haben wir eine verbesserte API implementiert, die eine einfache und übersichtliche Erstellung von Handlern ermöglicht, ohne sich um Webspezifikationen kümmern zu müssen. Diese Handler können mit jedem Backend oder sogar mit mehreren Backends gleichzeitig arbeiten.





Source: https://habr.com/ru/post/de439258/


All Articles