Metode Generik dalam Karat: Bagaimana Exonum Bergeser dari Besi ke Actix-web

Ekosistem Rust masih tumbuh. Akibatnya, perpustakaan baru dengan fungsionalitas yang ditingkatkan sering dirilis ke komunitas pengembang, sementara perpustakaan yang lebih lama menjadi usang. Ketika kami awalnya merancang Exonum, kami menggunakan kerangka kerja web Iron. Dalam artikel ini, kami menjelaskan bagaimana kami porting kerangka kerja Exonum ke actix-web menggunakan pemrograman generik.



Eksonum dengan zat besi


Dalam platform Exonum, kerangka besi digunakan tanpa abstraksi. Kami memasang penangan untuk sumber daya tertentu dan memperoleh parameter permintaan dengan mem-parsing URL menggunakan metode tambahan; hasilnya dikembalikan hanya dalam bentuk string.

Prosesnya tampak (kurang-lebih) seperti berikut:

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

Selain itu, kami menggunakan beberapa plugin middleware dalam bentuk header CORS. Kami menggunakan mount untuk menggabungkan semua penangan menjadi satu API.

Keputusan Kami untuk Beralih dari Besi


Besi adalah perpustakaan yang bagus, dengan banyak plugin. Namun, itu ditulis pada hari-hari ketika proyek seperti futures dan tokio tidak ada.

Arsitektur Besi melibatkan pemrosesan permintaan sinkron, yang dapat dengan mudah dipengaruhi oleh sejumlah besar koneksi yang terbuka secara bersamaan. Agar skalabel, Iron harus menjadi asinkron, yang akan melibatkan pemikiran ulang dan penulisan ulang seluruh kerangka kerja. Sebagai hasilnya, kami telah melihat perubahan bertahap dari penggunaan Besi oleh insinyur perangkat lunak.

Mengapa Kami Memilih Actix-Web


Actix-web adalah kerangka kerja populer yang berperingkat tinggi pada tolok ukur TechEmpower . Ini memiliki komunitas pengembang aktif, tidak seperti Iron, dan memiliki API yang dirancang dengan baik dan implementasi berkualitas tinggi berdasarkan kerangka aktor actix. Permintaan diproses secara tidak sinkron oleh kumpulan utas; jika meminta pemrosesan panic, aktor secara otomatis dihidupkan ulang.

Sebelumnya, muncul kekhawatiran bahwa actix-web berisi banyak kode tidak aman. Namun, jumlah kode tidak aman berkurang secara signifikan ketika kerangka kerja itu ditulis ulang dalam bahasa pemrograman yang aman - Rust. Insinyur Bitfury telah meninjau kode ini sendiri dan merasa percaya diri dalam stabilitas jangka panjangnya.

Untuk kerangka kerja Exonum, beralih ke actix memecahkan masalah stabilitas operasi. Kerangka kerja besi dapat gagal jika ada sejumlah besar koneksi. Kami juga menemukan bahwa API actix-web lebih sederhana, lebih produktif, dan lebih menyatu. Kami yakin bahwa pengguna dan pengembang akan lebih mudah menggunakan antarmuka pemrograman Exonum, yang sekarang dapat beroperasi lebih cepat berkat desain actix-web.

Apa Yang Kami Tuntut dari Kerangka Kerja Web


Selama proses ini kami menyadari bahwa penting bagi kami untuk tidak hanya mengubah kerangka kerja, tetapi juga untuk merancang arsitektur API baru yang independen dari kerangka kerja web tertentu. Arsitektur seperti itu akan memungkinkan untuk membuat penangan, dengan sedikit atau tanpa keprihatinan tentang spesifik web, dan mentransfernya ke backend apa pun. Konsepsi ini dapat diimplementasikan dengan menulis sebuah frontend yang akan menerapkan tipe dan sifat dasar.

Untuk memahami seperti apa tampilan frontend ini, mari tentukan apa sebenarnya API HTTP itu:

  • Permintaan dibuat secara eksklusif oleh klien; server hanya merespons mereka (server tidak memulai permintaan).
  • Meminta membaca data atau mengubah data.
  • Sebagai hasil dari pemrosesan permintaan, server mengembalikan respons, yang berisi data yang diperlukan, jika berhasil; atau informasi tentang kesalahan, jika terjadi kegagalan.

Jika kita menganalisis semua lapisan abstraksi, ternyata setiap permintaan HTTP hanyalah panggilan fungsi:

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

Segala sesuatu yang lain dapat dianggap sebagai perpanjangan dari entitas dasar ini. Jadi, untuk menjadi independen dari implementasi spesifik kerangka kerja web, kita perlu menulis penangan dengan gaya yang mirip dengan contoh di atas.

Ciri `Endpoint` untuk Pemrosesan Generik permintaan-HTTP


Pendekatan yang paling sederhana dan langsung adalah mendeklarasikan sifat `Endpoint`, yang menggambarkan implementasi permintaan spesifik:

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

Sekarang kita perlu mengimplementasikan penangan ini dalam kerangka kerja tertentu. Sebagai contoh, di actix-web terlihat seperti berikut:

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

Kita dapat menggunakan struktur untuk melewatkan parameter permintaan melalui konteks. Actix-web dapat secara otomatis deserialize parameter menggunakan serde. Sebagai contoh, a = 15 & b = hello deserialized ke dalam struktur seperti ini:

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

Fungsi deserialisasi ini sangat sesuai dengan tipe Permintaan yang terkait dari sifat `Endpoint`.

Selanjutnya, mari buat sebuah adaptor yang membungkus implementasi spesifik dari `Endpoint` ke dalam RequestHandler untuk actix-web. Perhatikan fakta bahwa saat melakukannya, informasi tentang jenis Permintaan dan Respons menghilang. Teknik ini disebut tipe erasure - mengubah pengiriman statis menjadi dinamis.

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

Pada tahap ini, cukup dengan menambahkan penangan untuk permintaan POST, karena kami telah menciptakan sifat yang independen dari detail implementasi. Namun, kami menemukan bahwa solusi ini tidak cukup canggih.

Kekurangan dari Sifat `Endpoint`


Sejumlah besar kode bantu dihasilkan ketika pawang ditulis:

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

Idealnya, kita harus mampu melewati penutupan sederhana sebagai seorang pawang, sehingga secara signifikan mengurangi jumlah kebisingan sintaksis.

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

Di bawah ini kita akan membahas bagaimana ini bisa dilakukan.

Perendaman Ringan ke Pemrograman Generik


Kita perlu menambahkan kemampuan untuk secara otomatis menghasilkan adaptor yang mengimplementasikan sifat `Endpoint` dengan tipe terkait yang benar. Input hanya akan terdiri dari penutupan dengan penangan permintaan HTTP.

Argumen dan hasil penutupan dapat memiliki tipe yang berbeda, jadi kami harus bekerja dengan metode kelebihan beban di sini. Karat tidak mendukung kelebihan beban secara langsung tetapi memungkinkan untuk ditiru menggunakan sifat `Into` dan` From`.

Selain itu, tipe nilai penutupan yang dikembalikan tidak harus cocok dengan nilai yang dikembalikan dari implementasi `Endpoint`. Untuk memanipulasi tipe ini, itu harus diekstraksi dari tipe penutupan yang diterima.

Mengambil Jenis dari Karakter `Fn`


Di Rust, setiap penutupan memiliki tipe uniknya sendiri, yang tidak dapat secara eksplisit ditunjukkan dalam program. Untuk manipulasi dengan penutupan, kami menggunakan sifat `Fn`. Ciri tersebut berisi tanda tangan dari fungsi dengan jenis argumen dan nilai yang dikembalikan, namun, mengambil elemen-elemen ini secara terpisah tidak mudah dilakukan.

Gagasan utamanya adalah menggunakan struktur bantu dari bentuk berikut:

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

Kita harus menggunakan PhantomData, karena Rust mengharuskan semua parameter generik ditunjukkan dalam definisi struktur. Namun, tipe penutupan atau fungsi F itu sendiri bukan yang generik (meskipun menerapkan sifat `Fn` generik). Parameter tipe A dan B tidak digunakan secara langsung.

Pembatasan sistem tipe Rust inilah yang menghalangi kami untuk menerapkan strategi yang lebih sederhana dengan menerapkan sifat `Endpoint` secara langsung untuk penutupan:

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

Dalam kasus di atas, kompiler mengembalikan kesalahan:

 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 

Struktur bantu SimpleExtractor memungkinkan untuk mendeskripsikan konversi `Dari`. Konversi ini memungkinkan kami untuk menyimpan fungsi apa pun dan mengekstraksi jenis argumennya:

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

Kode berikut berhasil dikompilasi:

 #[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); 

Jenis Spesialisasi dan Penanda


Sekarang kita memiliki fungsi dengan tipe argumen parameter secara eksplisit, yang dapat digunakan sebagai ganti sifat `Endpoint`. Misalnya, kita dapat dengan mudah mengimplementasikan konversi dari SimpleExtractor ke RequestHandler. Meski begitu, ini bukan solusi lengkap. Kita perlu entah bagaimana membedakan antara penangan untuk permintaan GET dan POST pada tingkat tipe (dan antara penangan sinkron dan asinkron). Dalam tugas ini, jenis marker akan membantu kami.

Pertama, mari kita menulis ulang SimpleExtractor sehingga dapat membedakan antara hasil sinkron dan asinkron. Pada saat yang sama, kami akan menerapkan sifat `Dari` untuk masing-masing kasus. Perhatikan bahwa sifat dapat diimplementasikan untuk varian spesifik dari struktur generik.

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

Sekarang kita perlu mendeklarasikan struktur yang akan menggabungkan penangan permintaan dengan nama dan jenisnya:

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

Selanjutnya, kami mendeklarasikan beberapa struktur kosong yang akan bertindak sebagai tipe marker. Marker akan memungkinkan kita untuk menerapkan untuk masing-masing penangan kode mereka sendiri untuk mengubah penangan menjadi RequestHandler yang dijelaskan sebelumnya.

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

Sekarang kita dapat mendefinisikan empat implementasi yang berbeda dari sifat `Dari` untuk semua kombinasi parameter template R dan K (nilai yang dikembalikan dari penangan dan jenis permintaan).

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

Fasad untuk backend


Langkah terakhir adalah untuk merancang fasad yang akan menerima penutupan dan menambahkannya ke dalam backend yang sesuai. Dalam kasus yang diberikan, kami memiliki backend tunggal - actix-web. Namun, ada potensi implementasi tambahan di balik fasad. Misalnya: generator spesifikasi Swagger.

 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 } 

Perhatikan bagaimana jenis parameter permintaan, jenis hasil permintaan, dan sinkronisasi / asinkron dari pawang diturunkan secara otomatis dari tanda tangannya. Selain itu, kita perlu secara spesifik menentukan nama dan jenis permintaan.

Kerugian dari pendekatan


Pendekatan yang dijelaskan di atas, meskipun cukup efektif, memiliki kelemahan. Secara khusus, metode endpoint dan endpoint_mut harus mempertimbangkan kekhasan implementasi backend spesifik . Pembatasan ini mencegah kami menambahkan backend saat bepergian, meskipun fungsi ini jarang diperlukan.

Masalah lain adalah bahwa kita tidak dapat menentukan spesialisasi dari seorang pawang tanpa argumen tambahan . Dengan kata lain, jika kita menulis kode berikut, itu tidak akan dikompilasi karena bertentangan dengan implementasi generik yang ada:

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

Akibatnya, permintaan yang tidak memiliki parameter apa pun harus tetap menerima string JSON null, yang dideserialisasi ke (). Masalah ini dapat diselesaikan dengan spesialisasi dalam gaya C ++, tetapi untuk sekarang ini hanya tersedia dalam versi malam dari kompiler dan tidak jelas kapan akan menjadi fitur yang stabil.

Demikian pula, jenis nilai yang dikembalikan tidak dapat dikhususkan . Bahkan jika permintaan tidak menyiratkan tipe tertentu dari nilai yang dikembalikan, masih akan melewati JSON dengan nol.

Decoding kueri URL dalam permintaan GET juga membebankan beberapa pembatasan yang tidak jelas pada jenis parameter , tetapi masalah ini lebih berkaitan dengan kekhasan implementasi serde-urlencoded.

Kesimpulan


Seperti yang dijelaskan di atas, kami telah mengimplementasikan API yang ditingkatkan, yang memungkinkan pembuatan handler yang sederhana dan jelas, tanpa perlu khawatir tentang spesifikasi web. Penangan ini dapat bekerja dengan backend apa pun atau bahkan dengan beberapa backend secara bersamaan.





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


All Articles