Desarrollamos camarones: controlamos solicitudes paralelas, iniciamos sesión a través de spdlog y más ...



La semana pasada, hablamos sobre nuestro pequeño proyecto de demostración, Shrimp , que muestra claramente cómo puede usar las bibliotecas C ++ RESTinio y SObjectizer en condiciones más o menos similares. Shrimp es una pequeña aplicación C ++ 17 que, a través de RESTinio, acepta solicitudes HTTP para escalar imágenes y atiende estas solicitudes en modo de subprocesos múltiples a través de SObjectizer e ImageMagick ++.

El proyecto resultó ser más que útil para nosotros. La alcancía de la lista de deseos para ampliar la funcionalidad de RESTinio y SObjectizer se ha reabastecido significativamente. Algo que incluso se ha incorporado en una versión muy reciente de RESTinio-0.4.7 . Así que decidimos no detenernos en la primera y más trivial versión de Shrimp, sino hacer una o dos iteraciones más en torno a este proyecto. Si alguien está interesado en qué y cómo lo hemos hecho durante este tiempo, eres bienvenido con cat.
Como spoiler: hablaremos sobre cómo nos deshicimos del procesamiento paralelo de solicitudes idénticas, cómo agregamos el registro a Shrimp utilizando la excelente biblioteca spdlog , y también hicimos un comando para forzar el reinicio del caché de imágenes transformadas.

v0.3: control de procesamiento paralelo de solicitudes idénticas


La primera versión de Shrimp, descrita en un artículo anterior, contenía una seria simplificación: no había control sobre si la misma solicitud se está procesando actualmente o no.

Imagine que por primera vez Shrimp recibe una solicitud del formulario "/demo.jpg?op=resize&max=1024". Todavía no existe dicha imagen en el caché de imágenes transformadas, por lo que la solicitud se está procesando. El procesamiento puede llevar un tiempo considerable, por ejemplo, unos pocos cientos de milisegundos.

El proceso de solicitud aún no se ha completado, y Shrimp nuevamente recibe la misma solicitud "/demo.jpg?op=resize&max=1024", pero de otro cliente. Aún no hay resultados de transformación en la memoria caché, por lo tanto, esta solicitud también se procesará.

Ni la primera ni la segunda solicitud aún se han completado, y Shrimp puede recibir nuevamente la misma solicitud "/demo.jpg?op=resize&max=1024". Y esta solicitud también será procesada. Resulta que la misma imagen se escala al mismo tamaño en paralelo varias veces.

Esto no es bueno Por lo tanto, lo primero que decidimos en Shrimp fue deshacernos de una jamba tan seria. Hicimos esto debido a dos contenedores difíciles en el agente transform_manager. El primer contenedor es una cola de espera de solicitudes de transformador gratuitas. Este es un contenedor llamado m_pending_requests. El segundo contenedor almacena solicitudes que ya han sido procesadas (es decir, se asignaron transformadores específicos a estas solicitudes). Este es un contenedor llamado m_inprogress_requests.

Cuando transform_manager recibe la siguiente solicitud, comprueba la presencia de la imagen terminada en el caché de imágenes transformadas. Si no hay una imagen convertida, se comprueban los contenedores m_inprogress_requests y m_pending_requests. Y si no hay una solicitud con tales parámetros en ninguno de estos contenedores, solo entonces se intenta colocar la solicitud en la cola m_pending_requests. Se ve así :

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

Se dijo anteriormente que m_inprogress_requests y m_pending_requests son contenedores complicados. Pero cual es el truco?

El truco es que estos contenedores combinan las propiedades de una cola FIFO regular (en la que se conserva el orden cronológico de la adición de elementos) y multimapas, es decir. Un contenedor asociativo en el que se pueden asignar múltiples valores a una sola clave.

Mantener el orden cronológico es importante, ya que los elementos más antiguos en m_pending_requests deben verificarse periódicamente y eliminarse de m_pending_requests aquellas solicitudes para las que se excede el tiempo de espera máximo. Y se necesita un acceso efectivo a los elementos por clave tanto para verificar la presencia de solicitudes idénticas en las colas, como para que todas las solicitudes duplicadas se puedan eliminar de la cola a la vez.

En Shrimp, ciclamos nuestro pequeño contenedor para estos fines. Aunque, si Boost se usara en Shrimp, Boost.MultiIndex podría usarse. Y, probablemente, con el tiempo, será necesario organizar una búsqueda efectiva en m_pending_requests por algún otro criterio, luego Boost.MultiIndex en Shrimp tendrá que activarse.

v0.4: inicio de sesión con spdlog


Intentamos dejar la primera versión de Shrimp lo más simple y compacta posible. Por eso, en la primera versión de Shrimp, no utilizamos el registro. En general

Por un lado, esto hizo posible mantener conciso el código de la primera versión, que no contenía más que la lógica comercial necesaria de Shrimp. Pero, por otro lado, la falta de tala complica tanto el desarrollo del camarón como su funcionamiento. Por lo tanto, tan pronto como lo pusimos en nuestras manos, inmediatamente arrastramos a Shrimp una excelente biblioteca moderna de C ++ para el registro: spdlog . La respiración se hizo más fácil de inmediato, aunque el código de algunos métodos creció en volumen.

Por ejemplo, el código anterior del método handle_not_transformed_image () con el registro comienza a parecerse a esto :

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

Configurar registradores spdlog


El inicio de sesión en Shrimp se realiza en la consola (es decir, en la secuencia de salida estándar). En principio, uno podría seguir un camino muy simple y crear en Shrimp la única instancia del spd-logger. Es decir uno podría llamar a stdout_color_mt (o stdout_logger_mt ), y luego pasar este registrador a todas las entidades en Shrimp. Pero tomamos un camino un poco más complicado: creamos manualmente el llamado sumidero (es decir, el canal donde spdlog generará los mensajes generados), y para las entidades Shrimp crearon registradores separados adjuntos a este sumidero.

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

Hay un punto sutil con la configuración de los registradores en spdlog: de manera predeterminada, el registrador ignora los mensajes con niveles de severidad de rastreo y depuración. A saber, demuestran ser más útiles al depurar. Por lo tanto, en make_logger, habilitamos de forma predeterminada el registro para todos los niveles, incluido el rastreo / depuración.

Debido al hecho de que cada entidad en Shrimp tiene su propio registrador con su propio nombre, podemos ver quién hace qué en el registro:



Seguimiento de SObjectizer con spdlog


Los tiempos de registro, que se realizan como parte de la lógica comercial principal de una aplicación SObjectizer, no son suficientes para depurar la aplicación. No está claro por qué se inicia alguna acción en un agente, pero en realidad no se realiza en otro agente. En este caso, el mecanismo msg_tracing integrado en SObjectizer ayuda mucho (de lo que hablamos en un artículo separado ). Pero entre las implementaciones estándar de msg_tracing para SObjectizer, no hay una que use spdlog. Haremos esta implementación para camarones nosotros mismos:

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

Aquí vemos la implementación de la interfaz especial SObjectizer tracer_t, en la que lo principal es el método virtual trace (). Es él quien realiza el rastreo de las partes internas de SObjectizer usando spdlog.

A continuación, esta implementación se instala como un trazador al iniciar 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 trace a través de spdlog


Además de rastrear lo que sucede dentro del SObjectizer, a veces puede ser muy útil rastrear lo que sucede dentro de RESTinio. En la versión actualizada de Shrimp, también se agrega ese rastro.

Este seguimiento se implementa a través de la definición de una clase especial que puede realizar el registro en 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; }; 

Esta clase no se hereda de nada, ya que el mecanismo de registro en RESTinio se basa en la programación generalizada y no en el enfoque tradicional orientado a objetos. Eso le permite deshacerse por completo de cualquier sobrecarga en los casos en que el registro no es necesario en absoluto ( cubrimos este tema con más detalle cuando hablamos sobre el uso de plantillas en RESTinio ).

A continuación, debemos indicar que el servidor HTTP usará la clase http_server_logger_t que se muestra arriba como su registrador. Esto se hace aclarando las propiedades del servidor 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; }; 

Bueno, entonces no queda nada por hacer: crear una instancia específica del spd-logger y enviar este registrador al servidor HTTP creado:

 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: reinicio forzado de la caché de imagen transformada


En el proceso de depuración de Shrimp, se descubrió una pequeña cosa que era un poco molesta: para vaciar el contenido del caché de la imagen transformada, tenía que reiniciar todo el Shrimp. Parecería un poco, pero desagradable.

Como es desagradable, debes deshacerte de esta deficiencia. Afortunadamente, esto no es del todo difícil.

Primero, definiremos otra URL en Shrimp a la que puede enviar solicitudes HTTP DELETE: "/ cache". En consecuencia, colgaremos nuestro controlador en esta 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; } 

donde la función add_delete_cache_handler () se ve así:

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

Un poco detallado, pero nada complicado. La cadena de consulta de la consulta debe tener un parámetro de token. Este parámetro debe contener una cadena con un valor especial para el token administrativo. Solo puede restablecer la memoria caché si el valor del token del parámetro del token coincide con lo establecido cuando se lanzó Shrimp. Si no hay ningún parámetro de token, la solicitud de procesamiento no se acepta. Si hay token, el agente transform_manager, que posee el caché, recibe un mensaje de comando especial, ejecutando el cual el agente transform_manager responderá a la solicitud HTTP.

En segundo lugar, implementamos el nuevo controlador de mensajes delete_cache_request_t en el agente 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" ); } } 

Aquí hay dos puntos que deben aclararse.

El primer punto en la implementación de on_delete_cache_request () es la verificación del valor del token en sí. El token administrativo se establece a través de la variable de entorno SHRIMP_ADMIN_TOKEN. Si se establece esta variable y su valor coincide con el valor del parámetro de token de la solicitud HTTP DELETE, la memoria caché se borra y se genera inmediatamente una respuesta positiva a la solicitud.

Y el segundo punto en la implementación de on_delete_cache_request () es el retraso forzado de una respuesta negativa a HTTP DELETE. Si el valor incorrecto del token administrativo ha llegado, entonces debería retrasar la respuesta a HTTP DELETE para que no haya deseo de seleccionar el valor del token por fuerza bruta. ¿Pero cómo hacer este retraso? Después de todo, llamar a std :: thread :: sleep_for () no es una opción.

Aquí es donde los mensajes pendientes de SObjectizer vienen al rescate. En lugar de generar inmediatamente una respuesta negativa dentro de on_delete_cache_request (), el agente transform_manager simplemente se envía un mensaje pendiente negativo_delete_cache_response_t. El temporizador SObjectizer contará el tiempo establecido y entregará este mensaje al agente después de que haya transcurrido el retraso especificado. Y ahora, en el controlador negative_delete_cache_response_t, ya puede generar inmediatamente una respuesta a la solicitud 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) ); } 

Es decir Resulta el siguiente escenario:

  • El servidor HTTP recibe una solicitud DELETE HTTP, convierte esta solicitud en un mensaje delete_cache_request_t al agente transform_manager;
  • El agente transform_manager recibe el mensaje delete_cache_request_t e inmediatamente genera una respuesta positiva a la solicitud o se envía un mensaje negativo negativo_delete_cache_response_t pendiente;
  • transform_manager recibe un mensaje negative_delete_cache_response_t e inmediatamente genera una respuesta negativa a la solicitud HTTP DELETE correspondiente.

Fin de la segunda parte.


Al final de la segunda parte, es bastante natural hacer la pregunta: "¿Qué sigue?"

Además, probablemente habrá otra iteración y otra actualización de nuestro proyecto de demostración. Me gustaría hacer algo como convertir una imagen de un formato a otro. Digamos, en el servidor, la imagen está en jpg, y después de la transformación, se envía al cliente en webp.

También sería interesante adjuntar una "página" separada con la visualización de las estadísticas actuales sobre el trabajo de los camarones. En primer lugar, es curioso. Pero, en principio, dicha página también se puede adaptar a las necesidades de monitoreo de la viabilidad de los camarones.

Si alguien más tiene sugerencias sobre lo que me gustaría ver en Shrimp o en artículos sobre Shrimp, nos complacerá escuchar cualquier pensamiento constructivo.

Por separado, quiero señalar un aspecto en la implementación de Shrimp, que nos sorprendió un poco. Este es un uso activo de mensajes mutables cuando se comunican entre sí y con el servidor HTTP. Por lo general, en nuestra práctica, sucede lo contrario: con mayor frecuencia se intercambian datos a través de mensajes inmunes. No es así aquí. Esto sugiere que escuchamos a sabiendas los deseos de los usuarios a su debido tiempo y agregamos mensajes mutables a SObjectizer. Entonces, si desea ver algo en RESTinio o SObjectizer, no dude en compartir sus ideas. Estamos seguros de escuchar a los buenos.

Bueno, y en conclusión, me gustaría agradecer a todos los que se tomaron el tiempo y hablaron sobre la primera versión de Shrimp, tanto en Habré como a través de otros recursos. Gracias

Continuará ...

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


All Articles