Teknik pemrograman umum di Rust: bagaimana kami menerjemahkan Exonum dari Iron ke actix-web

Ekosistem Rust belum sepenuhnya menetap. Perpustakaan baru sering muncul di dalamnya, yang terasa lebih baik dari pendahulunya, dan kerangka kerja yang populer sebelumnya menjadi usang. Inilah yang terjadi dengan kerangka web Iron yang kami gunakan saat mengembangkan Exonum.

Actix-web terpilih sebagai pengganti Iron. Selanjutnya saya akan memberi tahu bagaimana kami mem-porting kode yang ada ke solusi baru menggunakan teknik pemrograman umum.


Gambar dari ulleo PD

Bagaimana kami menggunakan Besi


Di Exonum, kerangka besi digunakan tanpa abstraksi. Kami memasang penangan pada sumber daya tertentu, menerima parameter kueri dengan mem-parsing URL menggunakan metode tambahan, dan mengembalikan hasilnya hanya sebagai string.

Semuanya terlihat seperti ini:

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, beberapa add-on middleware dalam bentuk header CORS digunakan. Untuk menggabungkan semua penangan ke dalam satu API, kami menggunakan mount.

Mengapa Anda harus meninggalkannya?


Besi adalah pekerja keras yang baik dengan banyak tambahan. Namun, itu ditulis pada masa-masa yang jauh, ketika proyek seperti futures dan tokio tidak ada.

Arsitektur besi menyediakan pemrosesan permintaan yang sinkron, sehingga mudah dipasang pada blade bahu dengan sejumlah besar koneksi yang terbuka secara bersamaan. Agar Besi menjadi skalabel, ia harus dibuat tidak sinkron. Untuk melakukan ini, perlu memikirkan kembali dan menulis ulang seluruh kerangka kerja, tetapi pengembang secara bertahap meninggalkan pekerjaan di atasnya.

Mengapa kami beralih ke actix-web


Ini adalah kerangka kerja populer yang menempati peringkat tinggi dalam tolok ukur TechEmpower . Pada saat yang sama, ia, tidak seperti Iron, aktif berkembang. Actix-web memiliki API yang dirancang dengan baik dan implementasi berkualitas tinggi berdasarkan kerangka aktor actix. Permintaan diproses secara tidak sinkron oleh kumpulan utas, dan jika pemrosesan mengarah pada kepanikan, aktor secara otomatis memulai kembali.

Tentu saja, actix-web memiliki kekurangan, misalnya, berisi sejumlah besar kode tidak aman. Tetapi kemudian itu ditulis ulang di Safe Rust, yang memecahkan masalah ini.

Beralih ke actix memecahkan masalah stabilitas. Iron-backend dapat dijatuhkan oleh sejumlah besar koneksi. Secara umum, API baru adalah solusi yang lebih sederhana, lebih produktif, dan terpadu. Pengguna dan pengembang akan lebih mudah menggunakan antarmuka perangkat lunak, dan kecepatannya akan meningkat.

Apa yang kita inginkan dari kerangka kerja web


Penting bagi kami untuk tidak hanya mengubah Iron menjadi actix-web, tetapi untuk membuat fondasi untuk masa depan - untuk mengerjakan arsitektur API baru untuk abstrak dari kerangka web tertentu. Ini akan memungkinkan Anda untuk membuat penangan, hampir tanpa memikirkan spesifikasi web dan mentransfernya ke backend apa pun. Ini dapat dilakukan dengan menulis frontend yang akan beroperasi pada tipe dan tipe dasar.

Untuk memahami bagaimana tampilan frontend ini, mari kita tentukan apa itu HTTP API:

  • Permintaan dibuat secara eksklusif oleh klien, dan server hanya menjawabnya (tidak bertindak sebagai pemrakarsa).
  • Permintaan dibaca dan dimodifikasi.
  • Sebagai hasil dari kueri, server mengembalikan respons yang berisi data keberhasilan yang diinginkan, dan jika terjadi kesalahan, informasi tentangnya.

Jika kami 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 ekstensi dari entitas dasar ini. Jadi, untuk mengabaikan implementasi spesifik kerangka kerja web, kita perlu menulis penangan dengan gaya yang mirip dengan contoh di atas.

Sifat titik akhir untuk pemrosesan permintaan HTTP umum

Anda dapat pergi dengan cara paling sederhana dan paling mudah dan mendeklarasikan sifat Endpoint,
menggambarkan penerapan kueri tertentu:

 // ,   GET .      //    ,      . //         . 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>; } 

Setelah itu, Anda perlu menerapkan penangan ini dalam kerangka kerja tertentu. Katakanlah untuk actix-web terlihat seperti ini:

 //    actix-web.  ,   , //  `Endpoint`   . type FutureResponse = actix_web::FutureResponse<HttpResponse, actix_web::Error>; // ยซยป    actix-web.      //   .     , //     . type RawHandler = dyn Fn(HttpRequest<Context>) -> FutureResponse + 'static + Send + Sync; //   ,     ,     . #[derive(Clone)] struct RequestHandler { ///  . pub name: String, /// HTTP . pub method: actix_web::http::Method, ///  .  ,       . pub inner: Arc<RawHandler>, } 

Anda dapat menggunakan struktur untuk meneruskan parameter permintaan melalui konteks. Actix-web dapat secara otomatis deserialize parameter menggunakan serde. Misalnya, a = 15 & b = hello deserialized ke dalam struktur bentuk berikut:

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

Ini konsisten dengan jenis Permintaan terkait dari sifat Endpoint.

Sekarang mari kita menulis adaptor yang membungkus implementasi Endpoint spesifik di RequstHandler untuk actix-web. Harap perhatikan bahwa informasi tentang jenis Permintaan dan Respons hilang dalam proses. Teknik ini disebut tipe erasure. Tugasnya adalah mengubah penjadwalan 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, Anda dapat menambahkan penangan untuk permintaan POST dan berhenti, karena kami membuat sifat yang disarikan dari detail implementasi. Namun, masih belum terlalu ergonomis.

Ketik masalah

Saat menulis handler, banyak kode tambahan dihasilkan:

 //    . struct ElementCountEndpoint { elements: Rc<RefCell<Vec<Something>>>, } //   Endpoint. impl Endpoint for ElementCountEndpoint { type Request = (); type Result = usize; fn handle(&self, context: &Context, _request: ()) -> Result<usize, io::Error> { Ok(self.elements.borrow().len()) } } //    . let endpoint = ElementCountEndpoint::new(elements.clone()); let handler = RequestHandler::from_endpoint("/v1/element_count", endpoint); actix_backend.endpoint(handler); 

Idealnya, saya ingin dapat melewati penutupan normal sebagai seorang pawang, mengurangi jumlah kebisingan sintaks dengan urutan besarnya:

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

Saya akan berbicara tentang bagaimana melakukan ini nanti.

Perendaman mudah dalam pemrograman umum


Kita perlu menyadari kemampuan untuk secara otomatis menghasilkan adaptor yang mengimplementasikan Endpoint dengan tipe terkait yang benar. Dalam hal ini, hanya penutupan dengan penangan permintaan HTTP yang akan diumpankan ke input.

Argumen dan hasil penutupan dapat dari berbagai jenis, jadi di sini Anda harus bekerja dengan metode overloading. Karat tidak mendukung kelebihan beban secara langsung, tetapi memungkinkan untuk ditiru menggunakan sifat Into dan From.

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

Ekstraksi jenis dari tipe Fn

Di Rust, setiap penutupan memiliki tipe uniknya sendiri, yang tidak dapat ditulis secara eksplisit dalam program. Untuk memanipulasi penutupan, ada tipe Fn. Ini berisi tanda tangan fungsi dengan jenis argumen dan nilai balik, namun mengekstraksinya secara terpisah tidak begitu sederhana.

Gagasan utamanya adalah menggunakan struktur bantu dari bentuk berikut:

 ///       F: Fn(A) -> B. struct SimpleExtractor<A, B, F> {   //   .   inner: F,   _a: PhantomData<A>,   _b: PhantomData<B>, } 

Kami terpaksa menggunakan PhantomData karena Rust mengharuskan semua parameter generalisasi berada dalam definisi struktur. Namun, tipe penutupan tertentu atau fungsi F tidak digeneralisasi (meskipun mengimplementasikan tipe umum Fn). Tipe parameter A dan B tidak langsung digunakan di dalamnya.

Batasan sistem tipe Rust inilah yang tidak memungkinkan penggunaan strategi yang lebih sederhana - untuk mengimplementasikan sifat Endpoint untuk penutupan secara langsung:

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

Kompiler dalam hal ini 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 helper dari SimpleExtractor memungkinkan untuk menggambarkan transformasi Dari. Ini memungkinkan Anda untuk menyimpan fungsi apa pun dan mengekstrak 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, }; //   . fn my_handler(_: &Context, q: Query) -> String { format!("{} has {} apples.", qb, qa) } let fn_extractor = SimpleExtractor::from(my_handler); //  . 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, cocok untuk digunakan sebagai ganti sifat Endpoint. Misalnya, kita dapat dengan mudah mengimplementasikan konversi dari SimpleExtractor ke RequestHandler. Tapi tetap saja, ini bukan solusi lengkap. Kita juga perlu membedakan antara GET handler permintaan dan permintaan POST pada tingkat tipe (dan penangan sinkron dari yang asinkron). Jenis penanda yang disebut akan membantu kita dengan ini.

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

 ///   HTTP-. pub struct With<Q, I, R, F> { ///  -. pub handler: F, ///     . _query_type: PhantomData<Q>, ///   . _item_type: PhantomData<I>, ///  ,  . ///  ,       . _result_type: PhantomData<R>, } //   ,   . 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, } } } //     . 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 di mana untuk menggabungkan penangan permintaan dengan nama dan variasinya:

 #[derive(Debug)] pub struct NamedWith<Q, I, R, F, K> {   ///  .   pub name: String,   ///    .   pub inner: With<Q, I, R, F>,   ///  .   _kind: PhantomData<K>, } 

Setelah Anda dapat mendeklarasikan beberapa struktur kosong yang akan bertindak sebagai jenis marker. Marker memungkinkan Anda untuk mengimplementasikan masing-masing penangan kode konversinya sendiri ke RequestHandler yang dijelaskan sebelumnya.

 /// ,    .  HTTP   GET-. pub struct Immutable; /// ,   .  HTTP   POST, PUT, UPDATE ///    ,        POST. pub struct Mutable; 

Sekarang kita dapat mendefinisikan empat implementasi yang berbeda dari tipe Dari untuk semua kombinasi parameter template R dan K (nilai pengembalian pawang dan jenis permintaan).

 //     get . 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>, } } } //     post . 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>, } } } //     get . 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>, } } } //     post . 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

Sekarang untuk semua ini masih menulis "fasad", yang akan mengambil penutupan dan menambahkannya ke backend yang sesuai. Dalam kasus kami, hanya ada satu backend - actix-web - tetapi di belakang fasad Anda dapat menyembunyikan implementasi tambahan yang Anda suka, misalnya, generator spesifikasi Swagger.

 pub struct ServiceApiScope { actix_backend: actix::ApiBuilder, } impl ServiceApiScope { ///    Immutable    . pub fn endpoint<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>>, //  ,          //  NamedWith  RequestHandler. RequestHandler: From<NamedWith<Q, I, R, F, Immutable>>, { self.actix_backend.endpoint(name, endpoint); self } ///    Mutable . 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 hasilnya, serta sinkronisasi / asinkron dari pawang secara otomatis berasal dari tanda tangannya. Selain itu, Anda harus secara eksplisit menentukan nama permintaan, serta jenisnya.

Kerugian dari pendekatan


Pendekatan ini masih memiliki kekurangan. Secara khusus, endpoint dan endpoint_mut harus mengetahui spesifikasi penerapan backend spesifik . Ini tidak memungkinkan kami untuk menambahkan backend on the fly, tetapi fungsi seperti itu jarang diperlukan.

Masalah lain adalah bahwa Anda tidak dapat menentukan spesialisasi untuk penangan tanpa argumen tambahan. Dengan kata lain, jika kita menulis kode berikut, itu tidak akan dikompilasi, karena bertentangan dengan implementasi umum 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 masih harus menerima string JSON nol, yang dideserialisasi ke (). Masalah ini dapat diselesaikan dengan spesialisasi dalam gaya C ++, tetapi sejauh ini hanya tersedia dalam versi malam dari kompiler dan tidak jelas kapan "stabil".

Demikian pula, Anda tidak dapat mengkhususkan jenis nilai pengembalian. Bahkan jika permintaan tidak menyiratkannya, itu akan selalu mengembalikan JSON dengan nol.

Mendekripsi permintaan URL dalam permintaan GET juga membebankan beberapa pembatasan yang tidak jelas pada jenis parameter, tetapi ini sudah fitur implementasi serde-urlencoded.

Kesimpulan


Karenanya, kami menerapkan API yang memungkinkan Anda membuat penangan dengan mudah dan mudah, hampir tanpa memikirkan spesifikasi web. Kemudian mereka dapat ditransfer ke backend apa pun atau bahkan menggunakan beberapa backend pada saat yang sama.

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


All Articles