Kami mengembangkan Udang: kami mengontrol permintaan paralel, kami masuk melalui spdlog dan banyak lagi ...



Pekan lalu, kami berbicara tentang proyek demo kecil kami, Shrimp , yang dengan jelas menunjukkan bagaimana Anda dapat menggunakan perpustakaan C ++ RESTinio dan SObjectizer dalam kondisi yang kurang lebih serupa. Shrimp adalah aplikasi C ++ 17 kecil yang, melalui RESTinio, menerima permintaan HTTP untuk penskalaan gambar dan melayani permintaan ini dalam mode multi-berulir melalui SObjectizer dan ImageMagick ++.

Proyek ini ternyata lebih bermanfaat bagi kami. Piggy bank Wishlist untuk memperluas fungsi RESTinio dan SObjectizer telah secara signifikan diisi ulang. Sesuatu yang bahkan telah diwujudkan dalam versi terbaru RESTinio-0.4.7 . Jadi kami memutuskan untuk tidak memikirkan Udang versi pertama dan paling sepele, tetapi untuk melakukan satu atau dua iterasi lagi di sekitar proyek ini. Jika seseorang tertarik pada apa dan bagaimana yang telah kami lakukan selama ini, Anda dipersilakan di bawah kucing.
Sebagai spoiler: ini tentang bagaimana kami menyingkirkan pemrosesan paralel dari permintaan yang identik, bagaimana kami menambahkan logging ke Shrimp menggunakan pustaka spdlog yang luar biasa , dan juga membuat perintah untuk memaksa cache dari gambar yang diubah untuk diatur ulang.

v0.3: kontrol pemrosesan paralel permintaan identik


Versi udang pertama, yang dijelaskan dalam artikel sebelumnya, mengandung penyederhanaan serius: tidak ada kontrol atas apakah permintaan yang sama sedang diproses atau tidak.

Bayangkan bahwa untuk pertama kalinya Shrimp menerima permintaan dari bentuk "/demo.jpg?op=resize&max=1024". Tidak ada gambar seperti itu di cache gambar yang diubah, jadi permintaan sedang diproses. Pemrosesan dapat memakan waktu cukup lama, katakanlah, beberapa ratus milidetik.

Pemrosesan permintaan belum selesai, dan Shrimp kembali menerima permintaan yang sama "/demo.jpg?op=resize&max=1024", tetapi dari klien lain. Belum ada hasil transformasi dalam cache, oleh karena itu permintaan ini juga akan diproses.

Permintaan pertama maupun kedua belum selesai, dan Shrimp dapat kembali menerima permintaan yang sama "/demo.jpg?op=resize&max=1024". Dan permintaan ini juga akan diproses. Ternyata gambar yang sama diskalakan dengan ukuran yang sama secara paralel beberapa kali.

Ini tidak baik. Karena itu, hal pertama yang kami putuskan di Shrimp adalah menyingkirkan kusen yang begitu serius. Kami melakukan ini karena dua wadah rumit di agen transform_manager. Kontainer pertama adalah antrian menunggu permintaan transformator gratis. Ini adalah wadah bernama m_pending_requests. Kontainer kedua menyimpan permintaan yang sudah diproses (mis. Transformer khusus dialokasikan untuk permintaan ini). Ini adalah wadah bernama m_inprogress_requests.

Ketika transform_manager menerima permintaan berikutnya, ia memeriksa keberadaan gambar yang sudah selesai dalam cache dari gambar yang diubah. Jika tidak ada gambar yang dikonversi, maka wadah m_inprogress_requests dan m_pending_requests diperiksa. Dan jika tidak ada permintaan dengan parameter seperti itu di salah satu wadah ini, hanya upaya yang dilakukan untuk menempatkan permintaan dalam antrian m_pending_requests. Ini terlihat seperti ini :

void a_transform_manager_t::handle_not_transformed_image( transform::resize_request_key_t request_key, sobj_shptr_t<resize_request_t> cmd ) { const auto store_to = [&](auto & queue) { queue.insert( std::move(request_key), std::move(cmd) ); }; if( m_inprogress_requests.has_key( request_key ) ) { //    . //         . store_to( m_inprogress_requests ); } else if( m_pending_requests.has_key( request_key ) ) { //      . store_to( m_pending_requests ); } else if( m_pending_requests.unique_keys() < max_pending_requests ) { //           . store_to( m_pending_requests ); //    transformer-     . try_initiate_pending_requests_processing(); } else { //  ,   . do_503_response( std::move(cmd->m_http_req) ); } } 

Dikatakan di atas bahwa m_inprogress_requests dan m_pending_requests adalah wadah yang rumit. Tapi apa masalahnya?

Kuncinya adalah bahwa wadah ini menggabungkan sifat-sifat dari kedua antrian FIFO biasa (di mana urutan kronologis unsur-unsur tambahan dipertahankan) dan multimap, mis. Wadah asosiatif di mana beberapa nilai dapat dipetakan ke satu kunci.

Mempertahankan urutan kronologis itu penting, karena elemen tertua di m_pending_requests perlu diperiksa dan dihapus secara berkala dari m_pending_requests permintaan yang melebihi batas waktu maksimum. Dan akses efektif ke elemen dengan kunci diperlukan baik untuk memeriksa keberadaan permintaan identik dalam antrian, dan agar semua permintaan duplikat dapat dihapus dari antrian pada suatu waktu.

Di Shrimp, kami mengayuh sepaket kecil untuk tujuan ini. Meskipun, jika Boost digunakan dalam udang, Boost.MultiIndex dapat digunakan. Dan, mungkin, seiring waktu, pencarian yang efektif di m_pending_requests perlu diatur oleh beberapa kriteria lain, maka Boost.MultiIndex di Shrimp harus diaktifkan.

v0.4: masuk dengan spdlog


Kami mencoba membuat udang versi pertama sesederhana dan seringkas mungkin. Karena apa, dalam versi pertama udang, kami tidak menggunakan penebangan. Umumnya.

Di satu sisi, ini memungkinkan untuk menjaga agar kode versi pertama tetap ringkas, tidak mengandung apa pun kecuali logika bisnis Shrimp yang diperlukan. Namun, di sisi lain, kurangnya pembalakan menyulitkan pengembangan udang dan operasinya. Oleh karena itu, segera setelah kami mendapatkannya, kami segera menyeret ke perpustakaan C ++ modern yang bagus untuk logging - spdlog . Pernapasan segera menjadi lebih mudah, meskipun kode beberapa metode bertambah volumenya.

Sebagai contoh, kode di atas metode handle_not_transformed_image () dengan logging mulai terlihat seperti ini :

 void a_transform_manager_t::handle_not_transformed_image( transform::resize_request_key_t request_key, sobj_shptr_t<resize_request_t> cmd ) { const auto store_to = [&](auto & queue) { queue.insert( std::move(request_key), std::move(cmd) ); }; if( m_inprogress_requests.has_key( request_key ) ) { //    . m_logger->debug( "same request is already in progress; request_key={}", request_key ); //         . store_to( m_inprogress_requests ); } else if( m_pending_requests.has_key( request_key ) ) { //      . m_logger->debug( "same request is already pending; request_key={}", request_key ); store_to( m_pending_requests ); } else if( m_pending_requests.unique_keys() < max_pending_requests ) { //           . m_logger->debug( "store request to pending requests queue; request_key={}", request_key ); store_to( m_pending_requests ); //    transformer-     . try_initiate_pending_requests_processing(); } else { //  ,   . m_logger->warn( "request is rejected because of overloading; " "request_key={}", request_key ); do_503_response( std::move(cmd->m_http_req) ); } } 

Mengkonfigurasi logger spdlog


Masuk ke udang dilakukan di konsol (mis. Dalam aliran keluaran standar). Pada prinsipnya, seseorang dapat menempuh jalan yang sangat sederhana dan menciptakan di Udang satu-satunya contoh dari spd-logger. Yaitu seseorang dapat memanggil stdout_color_mt (atau stdout_logger_mt ), dan kemudian meneruskan pencatat ini ke semua entitas di Shrimp. Tapi kami pergi sedikit lebih rumit: kami secara manual menciptakan apa yang disebut sink (mis. saluran tempat spdlog akan menampilkan pesan yang dihasilkan), dan untuk entitas Udang mereka membuat logger terpisah yang melekat pada sink ini.

 //     . [[nodiscard]] spdlog::sink_ptr make_logger_sink() { auto sink = std::make_shared< spdlog::sinks::ansicolor_stdout_sink_mt >(); return sink; } [[nodiscard]] std::shared_ptr<spdlog::logger> make_logger( const std::string & name, spdlog::sink_ptr sink, spdlog::level::level_enum level = spdlog::level::trace ) { auto logger = std::make_shared< spdlog::logger >( name, std::move(sink) ); logger->set_level( level ); logger->flush_on( level ); return logger; } //        : auto manager = coop.make_agent_with_binder< a_transform_manager_t >( create_one_thread_disp( "manager" )->binder(), make_logger( "manager", logger_sink ) ); ... const auto worker_name = fmt::format( "worker_{}", worker ); auto transformer = coop.make_agent_with_binder< a_transformer_t >( create_one_thread_disp( worker_name )->binder(), make_logger( worker_name, logger_sink ), app_params.m_storage ); 

Ada titik halus dengan mengkonfigurasi logger di spdlog: secara default, logger mengabaikan pesan dengan tingkat keparahan penelusuran dan debug. Yaitu, mereka terbukti sangat berguna saat debugging. Oleh karena itu, di make_logger, kami secara default mengaktifkan pencatatan untuk semua level, termasuk jejak / debug.

Karena fakta bahwa setiap entitas di Shrimp memiliki logger sendiri dengan namanya sendiri, kita dapat melihat siapa yang melakukan apa dalam log:



Menelusuri SObjectizer dengan spdlog


Waktu pencatatan, yang dilakukan sebagai bagian dari logika bisnis utama aplikasi SObjectizer, tidak cukup untuk melakukan debug aplikasi. Tidak jelas mengapa beberapa tindakan dimulai pada satu agen, tetapi sebenarnya tidak dilakukan pada agen lain. Dalam hal ini, mekanisme msg_tracing yang dibangun ke dalam SObjectizer banyak membantu (yang telah kita bahas dalam artikel terpisah ). Tetapi di antara implementasi msg_tracing standar untuk SObjectizer, tidak ada yang menggunakan spdlog. Kami akan melakukan implementasi ini untuk Udang sendiri:

 class spdlog_sobj_tracer_t : public so_5::msg_tracing::tracer_t { std::shared_ptr<spdlog::logger> m_logger; public: spdlog_sobj_tracer_t( std::shared_ptr<spdlog::logger> logger ) : m_logger{ std::move(logger) } {} virtual void trace( const std::string & what ) noexcept override { m_logger->trace( what ); } [[nodiscard]] static so_5::msg_tracing::tracer_unique_ptr_t make( spdlog::sink_ptr sink ) { return std::make_unique<spdlog_sobj_tracer_t>( make_logger( "sobjectizer", std::move(sink) ) ); } }; 

Di sini kita melihat implementasi antarmuka SObjectizer khusus tracer_t, di mana yang utama adalah metode virtual trace (). Dialah yang melakukan penelusuran internal SObjectizer melalui spdlog.

Selanjutnya, implementasi ini diinstal sebagai pelacak ketika memulai SObjectizer:

 so_5::wrapped_env_t sobj{ [&]( so_5::environment_t & env ) {...}, [&]( so_5::environment_params_t & params ) { if( sobj_tracing_t::on == sobj_tracing ) params.message_delivery_tracer( spdlog_sobj_tracer_t::make( logger_sink ) ); } }; 

RESTinio melacak melalui spdlog


Selain melacak apa yang terjadi di dalam SObjectizer, terkadang sangat berguna untuk melacak apa yang terjadi di dalam RESTinio. Dalam versi udang yang diperbarui, jejak semacam itu juga ditambahkan.

Jejak ini diimplementasikan melalui definisi kelas khusus yang dapat melakukan logging di RESTinio:

 class http_server_logger_t { public: http_server_logger_t( std::shared_ptr<spdlog::logger> logger ) : m_logger{ std::move( logger ) } {} template< typename Builder > void trace( Builder && msg_builder ) { log_if_enabled( spdlog::level::trace, std::forward<Builder>(msg_builder) ); } template< typename Builder > void info( Builder && msg_builder ) { log_if_enabled( spdlog::level::info, std::forward<Builder>(msg_builder) ); } template< typename Builder > void warn( Builder && msg_builder ) { log_if_enabled( spdlog::level::warn, std::forward<Builder>(msg_builder) ); } template< typename Builder > void error( Builder && msg_builder ) { log_if_enabled( spdlog::level::err, std::forward<Builder>(msg_builder) ); } private: template< typename Builder > void log_if_enabled( spdlog::level::level_enum lv, Builder && msg_builder ) { if( m_logger->should_log(lv) ) { m_logger->log( lv, msg_builder() ); } } std::shared_ptr<spdlog::logger> m_logger; }; 

Kelas ini tidak diwarisi dari apa pun, karena mekanisme logging di RESTinio didasarkan pada pemrograman umum, dan bukan pada pendekatan berorientasi objek tradisional. Itu memungkinkan Anda untuk benar-benar menghilangkan overhead apa pun dalam kasus di mana penebangan tidak diperlukan sama sekali (kami membahas topik ini lebih detail ketika kami berbicara tentang menggunakan templat di RESTinio ).

Selanjutnya, kita perlu menunjukkan bahwa server HTTP akan menggunakan kelas http_server_logger_t yang ditunjukkan di atas sebagai logger-nya. Ini dilakukan dengan mengklarifikasi properti dari server HTTP:

 struct http_server_traits_t : public restinio::default_traits_t { using logger_t = http_server_logger_t; using request_handler_t = http_req_router_t; }; 

Nah, maka tidak ada lagi yang harus dilakukan - buat instance spesifik dari spd-logger dan kirim logger ini ke server HTTP yang dibuat:

 auto restinio_logger = make_logger( "restinio", logger_sink, restinio_tracing_t::off == restinio_tracing ? spdlog::level::off : log_level ); restinio::run( asio_io_ctx, shrimp::make_http_server_settings( thread_count.m_io_threads, params, std::move(restinio_logger), manager_mbox_promise.get_future().get() ) ); 

v0.5: reset paksa cache gambar yang diubah


Dalam proses debug Shrimp, satu hal kecil ditemukan yang sedikit mengganggu: untuk menyiram konten cache gambar yang diubah, Anda harus me-restart seluruh Shrimp. Tampaknya hal itu sepele, tetapi tidak menyenangkan.

Jika tidak menyenangkan, maka Anda harus menyingkirkan kekurangan ini. Untungnya, ini sama sekali tidak sulit.

Pertama, kami akan mendefinisikan URL lain di Shrimp tempat Anda dapat mengirim permintaan HTTP DELETE: "/ cache". Karenanya, kami akan menggantung penangan kami di URL ini:

 std::unique_ptr< http_req_router_t > make_router( const app_params_t & params, so_5::mbox_t req_handler_mbox ) { auto router = std::make_unique< http_req_router_t >(); add_transform_op_handler( params, *router, req_handler_mbox ); add_delete_cache_handler( *router, req_handler_mbox ); return router; } 

di mana fungsi add_delete_cache_handler () terlihat seperti ini:

 void add_delete_cache_handler( http_req_router_t & router, so_5::mbox_t req_handler_mbox ) { router.http_delete( "/cache", [req_handler_mbox]( auto req, auto /*params*/ ) { const auto qp = restinio::parse_query( req->header().query() ); auto token = qp.get_param( "token"sv ); if( !token ) { return do_403_response( req, "No token provided\r\n" ); } // Delegate request processing to transform_manager. so_5::send< so_5::mutable_msg<a_transform_manager_t::delete_cache_request_t> >( req_handler_mbox, req, restinio::cast_to<std::string>(*token) ); return restinio::request_accepted(); } ); } 

Sedikit bertele-tele, tapi tidak ada yang rumit. String kueri dari kueri harus memiliki parameter token. Parameter ini harus berisi string dengan nilai khusus untuk token administratif. Anda hanya dapat mengatur ulang cache jika nilai token dari parameter token cocok dengan apa yang ditetapkan saat Shrimp diluncurkan. Jika tidak ada parameter token, permintaan pemrosesan tidak diterima. Jika ada token, maka agen transform_manager, yang memiliki cache, dikirimi pesan perintah khusus, dengan mengeksekusi agen transform_manager itu sendiri yang akan menanggapi permintaan HTTP.

Kedua, kami menerapkan penangan pesan baru delete_cache_request_t di agen transform_manager_t:

 void a_transform_manager_t::on_delete_cache_request( mutable_mhood_t<delete_cache_request_t> cmd ) { m_logger->warn( "delete cache request received; " "connection_id={}, token={}", cmd->m_http_req->connection_id(), cmd->m_token ); const auto delay_response = [&]( std::string response_text ) { so_5::send_delayed< so_5::mutable_msg<negative_delete_cache_response_t> >( *this, std::chrono::seconds{7}, std::move(cmd->m_http_req), std::move(response_text) ); }; if( const char * env_token = std::getenv( "SHRIMP_ADMIN_TOKEN" ); // Token must be present and must not be empty. env_token && *env_token ) { if( cmd->m_token == env_token ) { m_transformed_cache.clear(); m_logger->info( "cache deleted" ); do_200_plaintext_response( std::move(cmd->m_http_req), "Cache deleted\r\n" ); } else { m_logger->error( "invalid token value for delete cache request; " "token={}", cmd->m_token ); delay_response( "Token value mismatch\r\n" ); } } else { m_logger->warn( "delete cache can't performed because there is no " "admin token defined" ); // Operation can't be performed because admin token is not avaliable. delay_response( "No admin token defined\r\n" ); } } 

Ada dua poin di sini yang harus diklarifikasi.

Poin pertama dalam implementasi on_delete_cache_request () adalah verifikasi nilai token itu sendiri. Token administratif diatur melalui variabel lingkungan SHRIMP_ADMIN_TOKEN. Jika variabel ini disetel dan nilainya cocok dengan nilai dari parameter token permintaan HTTP DELETE, maka cache dihapus dan respons positif terhadap permintaan segera dihasilkan.

Dan poin kedua dalam implementasi on_delete_cache_request () adalah penundaan respons negatif terhadap HTTP DELETE. Jika nilai token administratif yang salah telah datang, maka Anda harus menunda respons terhadap HTTP DELETE sehingga tidak ada keinginan untuk memilih nilai token dengan kekerasan. Tetapi bagaimana cara membuat penundaan ini? Lagi pula, memanggil std :: thread :: sleep_for () bukan pilihan.

Di sinilah pesan SObjectizer yang tertunda datang untuk menyelamatkan. Alih-alih segera menghasilkan respons negatif di dalam on_delete_cache_request (), agen transform_manager mengirimkan sendiri pesan negative_delete_cache_response_t yang tertunda. Timer SObjectizer akan menghitung waktu yang ditentukan dan mengirimkan pesan ini ke agen setelah penundaan yang ditentukan berlalu. Dan sekarang di handler negative_delete_cache_response_t, Anda sudah dapat segera menghasilkan respons terhadap permintaan HTTP DELETE:

 void a_transform_manager_t::on_negative_delete_cache_response( mutable_mhood_t<negative_delete_cache_response_t> cmd ) { m_logger->debug( "send negative response to delete cache request; " "connection_id={}", cmd->m_http_req->connection_id() ); do_403_response( std::move(cmd->m_http_req), std::move(cmd->m_response_text) ); } 

Yaitu ternyata skenario berikut:

  • Server HTTP menerima permintaan HTTP DELETE, mengubah permintaan ini menjadi pesan delete_cache_request_t ke agen transform_manager;
  • agen transform_manager menerima pesan delete_cache_request_t dan segera menghasilkan respons positif terhadap permintaan atau mengirim sendiri pesan negative_delete_cache_response_t yang tertunda;
  • transform_manager menerima pesan negative_delete_cache_response_t dan segera menghasilkan respons negatif terhadap permintaan HTTP DELETE yang sesuai.

Akhir dari bagian kedua


Pada akhir bagian kedua, wajar untuk mengajukan pertanyaan: "Apa selanjutnya?"

Selanjutnya, mungkin akan ada iterasi lain dan pembaruan proyek demo kami. Saya ingin melakukan hal seperti mengubah gambar dari satu format ke format lainnya. Katakanlah, di server, gambar di jpg, dan setelah transformasi, dikirim ke klien di webp.

Juga akan menarik untuk melampirkan "halaman" terpisah dengan tampilan statistik terkini tentang karya Udang. Pertama-tama, ini hanya ingin tahu. Namun, pada prinsipnya, halaman seperti itu juga dapat disesuaikan untuk kebutuhan pemantauan kelangsungan hidup udang.

Jika ada orang lain yang memiliki saran tentang apa yang ingin saya lihat di Udang atau di artikel di sekitar Udang, maka kami akan senang mendengar pemikiran konstruktif.

Secara terpisah, saya ingin mencatat satu aspek dalam penerapan udang, yang agak mengejutkan kami. Ini adalah penggunaan aktif pesan yang bisa berubah saat berkomunikasi satu sama lain dan server HTTP. Biasanya, dalam praktik kami, yang terjadi adalah sebaliknya - lebih sering data dipertukarkan melalui pesan imun. Tidak demikian di sini. Ini menunjukkan bahwa kami dengan sengaja mendengarkan keinginan pengguna pada waktunya dan menambahkan pesan yang dapat diubah ke SObjectizer. Jadi, jika Anda ingin melihat sesuatu di RESTinio atau SObjectizer, silakan bagikan ide Anda. Kami yakin akan mendengarkan yang baik.

Yah, dan sebagai kesimpulan, saya ingin mengucapkan terima kasih kepada semua orang yang meluangkan waktu dan berbicara tentang versi pertama udang, baik di sini di Habré dan melalui sumber daya lainnya. Terima kasih

Dilanjutkan ...

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


All Articles