نقوم بتطوير الجمبري: نتحكم في الطلبات الموازية ، ونقوم بتسجيل الدخول من خلال spdlog والمزيد ...



في الأسبوع الماضي ، تحدثنا عن مشروعنا التجريبي الصغير ، Shrimp ، والذي يوضح بوضوح كيف يمكنك استخدام مكتبات C ++ RESTinio و SObjectizer في ظروف مشابهة تقريبًا. Shrimp هو تطبيق C ++ 17 صغير يقبل من خلال RESTinio طلبات HTTP لتغيير حجم الصورة ويخدم هذه الطلبات في وضع متعدد الخيوط من خلال SObjectizer و ImageMagick ++.

تبين أن المشروع أكثر من مفيد لنا. لقد تم تجديد بنك أصبع القائمة المفضلة لتوسيع وظائف RESTinio و SObjectizer بشكل كبير. شيء تم تجسيده حتى في إصدار حديث جدًا من RESTinio-0.4.7 . لذلك قررنا عدم التطرق إلى الإصدار الأول والأكثر تافهًا من الروبيان ، ولكن القيام بتكرار أو اثنين آخرين حول هذا المشروع. إذا كان شخص ما مهتمًا بما فعلناه وكيف فعلناه خلال هذا الوقت ، فأنت مرحب بك تحت القط.
كمفسد: سيكون الأمر حول كيفية التخلص من المعالجة المتوازية للطلبات المتطابقة ، وكيف أضفنا تسجيل الدخول إلى Shrimp باستخدام مكتبة spdlog الممتازة ، وأصدرنا أيضًا أمرًا بفرض إعادة تعيين ذاكرة التخزين المؤقت للصور المحولة.

v0.3: التحكم في المعالجة المتوازية للطلبات المتطابقة


النسخة الأولى من الروبيان ، الموصوفة في مقال سابق ، تحتوي على تبسيط خطير: لم يكن هناك سيطرة على ما إذا كان نفس الطلب قيد المعالجة حاليًا أم لا.

تخيل أنه لأول مرة يتلقى الجمبري طلبًا من النموذج "/demo.jpg؟op=resize&max=1024". لا توجد مثل هذه الصورة في ذاكرة التخزين المؤقت للصور المحولة بعد ، لذلك تتم معالجة الطلب. يمكن أن تستغرق المعالجة وقتًا طويلاً ، على سبيل المثال ، بضع مئات من المللي ثانية.

لم تكتمل معالجة الطلب بعد ، ويتلقى الجمبري مرة أخرى نفس الطلب "/demo.jpg؟op=resize&max=1024" ، ولكن من عميل آخر. لا توجد نتائج تحويل في ذاكرة التخزين المؤقت حتى الآن ، وبالتالي ستتم معالجة هذا الطلب أيضًا.

لم يكتمل الطلب الأول ولا الثاني حتى الآن ، ويمكن للروبيان مرة أخرى تلقي نفس الطلب "/demo.jpg؟op=resize&max=1024". وستتم معالجة هذا الطلب أيضًا. اتضح أن الصورة نفسها يتم تحجيمها إلى نفس الحجم بالتوازي عدة مرات.

هذا ليس جيدًا. لذلك ، كان أول شيء قررناه في Shrimp هو التخلص من مثل هذه الدعامة الخطيرة. لقد فعلنا ذلك بسبب حاويتين صعبتين في وكيل convert_manager. الحاوية الأولى هي قائمة انتظار انتظار طلبات المحولات المجانية. هذه حاوية باسم m_pending_requests. يخزن الحاوية الثانية الطلبات التي تمت معالجتها بالفعل (أي تم تخصيص محولات محددة لهذه الطلبات). هذه حاوية باسم m_inprogress_requests.

عندما يتلقى convert_manager الطلب التالي ، فإنه يتحقق من وجود الصورة النهائية في ذاكرة التخزين المؤقت للصور المحولة. في حالة عدم وجود صورة محولة ، يتم التحقق من حاويات m_inprogress_requests و m_pending_requests. وإذا لم يكن هناك طلب بمثل هذه المعلمات في أي من هذه الحاويات ، فسيتم فقط محاولة لوضع الطلب في قائمة انتظار m_pending_requests. يبدو هذا :

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

قيل أعلاه أن m_inprogress_requests و m_pending_requests هي حاويات صعبة. لكن ما هي الحيلة؟

الحيلة هي أن هذه الحاويات تجمع بين خصائص كل من قائمة انتظار FIFO العادية (التي يتم فيها الحفاظ على الترتيب الزمني لإضافة العناصر) والخريطة المتعددة ، أي حاوية تجميعية يمكن فيها تعيين قيم متعددة لمفتاح واحد.

من المهم الاحتفاظ بالترتيب الزمني ، لأن العناصر الأقدم في m_pending_requests تحتاج إلى فحص دوري وإزالتها من m_pending_requests تلك الطلبات التي تم تجاوز الحد الأقصى للمهلة الزمنية لها. وهناك حاجة إلى الوصول الفعال للعناصر عن طريق المفتاح للتحقق من وجود طلبات متطابقة في قوائم الانتظار ، وبالتالي يمكن إزالة جميع الطلبات المكررة من قائمة الانتظار في كل مرة.

في Shrimp ، قمنا بتدوير الحاوية الصغيرة الخاصة بنا لهذه الأغراض. على الرغم من أنه إذا تم استخدام Boost في الروبيان ، فيمكن استخدام Boost.MultiIndex. وربما ، بمرور الوقت ، يجب تنظيم البحث الفعال في m_pending_requests وفقًا لبعض المعايير الأخرى ، ثم يجب تنشيط Boost.MultiIndex in Shrimp.

v0.4: تسجيل الدخول باستخدام spdlog


حاولنا ترك النسخة الأولى من الجمبري بسيطة ومضغوطة قدر الإمكان. بسبب ما لم نستخدمه في الإصدار الأول من الروبيان. بشكل عام.

من ناحية ، جعل هذا من الممكن إبقاء كود النسخة الأولى موجزا ، والذي لا يحتوي سوى على منطق الأعمال الجمبري الضروري. ولكن ، من ناحية أخرى ، فإن عدم وجود قطع الأشجار يعقد تطور الروبيان وتشغيله. لذلك ، بمجرد أن وضعنا أيدينا عليها ، سحبنا على الفور إلى Shrimp مكتبة ممتازة حديثة C ++ لتسجيل الدخول - spdlog . أصبح التنفس أسهل على الفور ، على الرغم من نمو حجم بعض الطرق.

على سبيل المثال ، يبدأ الكود أعلاه لطريقة handle_not_transformed_image () مع التسجيل ليبدو كالتالي:

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

تكوين مسجلات spdlog


يتم تسجيل الروبيان على وحدة التحكم (أي في دفق الإخراج القياسي). من حيث المبدأ ، يمكن للمرء أن يسير في مسار بسيط للغاية وأن يخلق في Shrimp المثيل الوحيد لمسجل spd. على سبيل المثال يمكن للمرء استدعاء stdout_color_mt (أو stdout_logger_mt ) ، ثم تمرير هذا المسجل إلى جميع الكيانات في Shrimp. لكننا ذهبنا بطريقة أكثر تعقيدًا: لقد أنشأنا ما يسمى يدويًا sink (أي القناة حيث ستخرج spdlog الرسائل التي تم إنشاؤها) ، وكيانات الروبيان قاموا بإنشاء سجلات منفصلة ملحقة بهذا الحوض.

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

هناك نقطة دقيقة مع تكوين أدوات تسجيل الدخول في spdlog: افتراضيًا ، يتجاهل المسجل الرسائل ذات مستويات خطورة التصحيح والتتبع. وهي أثبتت أنها مفيدة للغاية عند تصحيح الأخطاء. لذلك ، في make_logger ، نقوم افتراضيًا بتمكين التسجيل لجميع المستويات ، بما في ذلك التتبع / التصحيح.

نظرًا لحقيقة أن كل كيان في الجمبري لديه مسجل خاص به باسمه الخاص ، يمكننا أن نرى من يفعل ما في السجل:



تتبع SObjectizer مع spdlog


أوقات التسجيل ، التي يتم تنفيذها كجزء من منطق الأعمال الرئيسي لتطبيق SObjectizer ، ليست كافية لتصحيح التطبيق. ليس من الواضح سبب بدء إجراء ما في أحد العوامل ، ولكن لا يتم تنفيذه فعليًا في وكيل آخر. في هذه الحالة ، تساعد آلية msg_tracing المضمنة في SObjectizer كثيرًا (والذي تحدثنا عنه في مقالة منفصلة ). ولكن من بين تطبيقات msg_tracing القياسية لـ SObjectizer ، لا يوجد تطبيق يستخدم spdlog. سنفعل هذا التنفيذ لروبيان أنفسنا:

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

هنا نرى تنفيذ واجهة SObjectizer الخاصة tracer_t ، حيث يكون الشيء الرئيسي هو طريقة التتبع الظاهري (). هو الذي يقوم بتتبع الأجزاء الداخلية من SObjectizer باستخدام spdlog.

بعد ذلك ، يتم تثبيت هذا التطبيق كمتتبع عند بدء 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 من خلال spdlog


بالإضافة إلى تتبع ما يحدث داخل SObjectizer ، قد يكون من المفيد جدًا في بعض الأحيان تتبع ما يحدث داخل RESTinio. في النسخة المحدثة من الروبيان ، يتم إضافة هذا التتبع أيضًا.

يتم تنفيذ هذا التتبع من خلال تعريف فئة خاصة يمكنها إجراء تسجيل الدخول في 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; }; 

هذه الفئة ليست موروثة من أي شيء ، حيث أن آلية التسجيل في RESTinio مبنية على البرمجة المعممة ، وليس على النهج التقليدي الكائني. يسمح لك ذلك بالتخلص تمامًا من أي زيادة في النفقات في الحالات التي لا يكون فيها التسجيل ضروريًا على الإطلاق (تناولنا هذا الموضوع بمزيد من التفاصيل عندما تحدثنا عن استخدام القوالب في RESTinio ).

بعد ذلك ، نحتاج إلى الإشارة إلى أن خادم HTTP سيستخدم فئة http_server_logger_t الموضحة أعلاه كمسجل. يتم ذلك من خلال توضيح خصائص خادم 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; }; 

حسنًا ، إذن لم يبق شيء للقيام به - أنشئ نسخة محددة من spd-logger وأرسل هذا المسجل إلى خادم HTTP الذي تم إنشاؤه:

 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: إعادة تعيين إجباري لذاكرة التخزين المؤقت للصور المحولة


في عملية تصحيح الروبيان ، تم اكتشاف شيء صغير كان مزعجًا بعض الشيء: من أجل مسح محتويات ذاكرة التخزين المؤقت للصور المحولة ، كان عليك إعادة تشغيل الروبيان بأكمله. قد يبدو تافها ، ولكن غير سارة.

إذا كانت غير سارة ، فعليك التخلص من هذا القصور. لحسن الحظ ، هذا ليس بالأمر الصعب على الإطلاق.

أولاً ، سنحدد عنوان URL آخر في Shrimp يمكنك إرسال طلبات HTTP DELETE إليه: "/ cache". وفقًا لذلك ، سنعلق معالجنا على عنوان URL هذا:

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

حيث تبدو الوظيفة add_delete_cache_handler () كما يلي:

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

مطوّل قليلاً ، ولكن لا شيء معقد. يجب أن تحتوي سلسلة الاستعلام على معلمة الرمز المميز. يجب أن تحتوي هذه المعلمة على سلسلة ذات قيمة خاصة للرمز الإداري. يمكنك فقط إعادة تعيين ذاكرة التخزين المؤقت إذا كانت قيمة الرمز المميز من معلمة الرمز المميز تتطابق مع ما تم تعيينه عند إطلاق Shrimp. إذا لم تكن هناك معلمة رمزية ، فلن يتم قبول طلب المعالجة. إذا كان هناك رمز مميز ، فسيتم إرسال رسالة التحويل الخاصة إلى وكيل transform_manager ، الذي يمتلك ذاكرة التخزين المؤقت ، عن طريق تنفيذ أي وسيط يستجيب وكيل convert_manager نفسه لطلب HTTP.

ثانيًا ، ننفذ معالج الرسائل الجديد delete_cache_request_t في وكيل 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" ); } } 

هناك نقطتان هنا يجب توضيحهما.

النقطة الأولى في تنفيذ on_delete_cache_request () هي التحقق من قيمة الرمز المميز نفسه. يتم تعيين الرمز الإداري عبر متغير البيئة SHRIMP_ADMIN_TOKEN. إذا تم تعيين هذا المتغير وقيمته تتطابق مع القيمة من معلمة الرمز المميز لطلب HTTP DELETE ، فسيتم مسح ذاكرة التخزين المؤقت ويتم إنشاء استجابة إيجابية للطلب على الفور.

والنقطة الثانية في تنفيذ on_delete_cache_request () هي التأخير الإجباري للاستجابة السلبية لـ HTTP DELETE. إذا ظهرت القيمة الخاطئة للرمز المميز الإداري ، فيجب عليك تأخير الاستجابة إلى HTTP DELETE حتى لا تكون هناك رغبة في تحديد قيمة الرمز المميز بالقوة الغاشمة. لكن كيف تجعل هذا التأخير؟ بعد كل شيء ، استدعاء std :: thread :: sleep_for () ليس خيارًا.

هذا هو المكان الذي تأتي فيه رسائل SObjectizer المعلقة إلى الإنقاذ. بدلاً من إنشاء رد سلبي على الفور داخل on_delete_cache_request () ، يرسل وكيل transform_manager لنفسه رسالة سلبيّة معلقة deltete_cache_response_t معلقة. سيحسب مؤقت SObjectizer الوقت المحدد وسيصل هذه الرسالة إلى الوكيل بعد مرور التأخير المحدد. والآن في المعالج سلبي _ delete_cache_response_t ، يمكنك بالفعل إنشاء استجابة على الفور لطلب 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) ); } 

على سبيل المثال اتضح السيناريو التالي:

  • يتلقى خادم HTTP طلب HTTP DELETE ، ويحول هذا الطلب إلى رسالة delete_cache_request_t إلى وكيل transform_manager ؛
  • يتلقى وكيل transform_manager رسالة delete_cache_request_t وإما أن يولد ردًا إيجابيًا على الفور على الطلب ، أو يرسل إلى نفسه رسالة سلبيّة معلّقة سلبيّة معلّقة ؛
  • يتلقى transform_manager رسالة سلبية_delete_cache_response_t ويقوم على الفور بإنشاء رد سلبي على طلب HTTP DELETE المقابل.

نهاية الجزء الثاني


في نهاية الجزء الثاني ، من الطبيعي أن تسأل السؤال "ماذا بعد؟"

علاوة على ذلك ، ربما سيكون هناك تكرار آخر وتحديث آخر لمشروعنا التجريبي. أود أن أفعل شيئًا مثل تحويل صورة من تنسيق إلى آخر. لنفترض ، على الخادم ، أن الصورة موجودة بتنسيق jpg ، وبعد التحويل ، يتم إرسالها إلى العميل في صفحة الويب.

سيكون من المثير للاهتمام أيضًا إرفاق "صفحة" منفصلة مع عرض الإحصائيات الحالية حول عمل الروبيان. بادئ ذي بدء ، إنه مجرد فضول. ولكن ، من حيث المبدأ ، يمكن أيضًا تكييف هذه الصفحة لاحتياجات مراقبة صلاحية الروبيان.

إذا كان لدى شخص آخر اقتراحات حول ما أود رؤيته في Shrimp أو في مقالات حول Shrimp ، فسوف يسعدنا سماع أي أفكار بناءة.

بشكل منفصل ، أود أن أشير إلى جانب واحد في تنفيذ الروبيان ، والذي فاجأنا إلى حد ما. هذا هو الاستخدام النشط للرسائل القابلة للتغيير عند التواصل مع بعضها البعض وخادم HTTP. عادة ، في ممارستنا ، يحدث العكس - في كثير من الأحيان يتم تبادل البيانات من خلال الرسائل المناعية. ليس الأمر كذلك هنا. يشير هذا إلى أننا استمعنا عن علم إلى رغبات المستخدمين في الوقت المناسب وأضفنا رسائل قابلة للتغيير إلى SObjectizer. لذا إذا كنت ترغب في رؤية شيء ما في RESTinio أو SObjectizer ، فلا تتردد في مشاركة أفكارك. نحن على يقين من الاستماع إلى الصالحين.

حسنًا ، وفي الختام ، أود أن أشكر كل من أخذ الوقت وتحدث عن النسخة الأولى من الروبيان ، سواء هنا في حبري أو من خلال موارد أخرى. شكرا لك!

يتبع ...

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


All Articles