
Na semana passada,
falamos sobre o nosso pequeno projeto de demonstração, Shrimp , que mostra claramente como você pode usar as bibliotecas C ++
RESTinio e
SObjectizer em condições mais ou menos semelhantes. O Shrimp é um pequeno aplicativo C ++ 17 que, através do RESTinio, aceita solicitações HTTP para dimensionamento de imagens e atende a essas solicitações no modo multithread através do SObjectizer e ImageMagick ++.
O projeto acabou sendo mais do que útil para nós. O cofrinho de lista de desejos para expandir a funcionalidade do RESTinio e SObjectizer foi reabastecido significativamente. Algo que já foi incorporado em uma
versão muito
recente do RESTinio-0.4.7 . Por isso, decidimos não nos debruçar sobre a primeira e mais trivial versão do Shrimp, mas fazer mais uma ou duas iterações em torno deste projeto. Se alguém estiver interessado no que e como fizemos durante esse período, você é bem-vindo sob cat.
Como um spoiler: falaremos sobre como nos livramos do processamento paralelo de solicitações idênticas, como adicionamos o log ao Shrimp usando a excelente biblioteca spdlog e também fizemos um comando para forçar a restauração do cache das imagens transformadas.
v0.3: controle do processamento paralelo de pedidos idênticos
A primeira versão do Shrimp, descrita em um artigo anterior, continha uma séria simplificação: não havia controle sobre se a mesma solicitação está sendo processada ou não.
Imagine que, pela primeira vez, o Shrimp receba uma solicitação no formato "/demo.jpg?op=resize&max=1024". Ainda não existe uma imagem no cache de imagem transformada, portanto, a solicitação está sendo processada. O processamento pode levar um tempo considerável, digamos, algumas centenas de milissegundos.
O processamento da solicitação ainda não foi concluído e o Shrimp novamente recebe a mesma solicitação "/demo.jpg?op=resize&max=1024", mas de outro cliente. Ainda não há resultado de transformação no cache, portanto, essa solicitação também será processada.
O primeiro e o segundo pedidos ainda não foram concluídos, e o Shrimp pode novamente receber o mesmo pedido "/demo.jpg?op=resize&max=1024". E essa solicitação também será processada. Acontece que a mesma imagem é dimensionada para o mesmo tamanho em paralelo várias vezes.
Isso não é bom. Portanto, a primeira coisa que decidimos no Shrimp foi nos livrar de um batente tão sério. Fizemos isso devido a dois contêineres complicados no agente transform_manager. O primeiro contêiner é uma fila de espera para solicitações gratuitas de transformadores. Este é um contêiner chamado m_pending_requests. O segundo contêiner armazena solicitações que já foram processadas (ou seja, transformadores específicos foram alocados a essas solicitações). Este é um contêiner chamado m_inprogress_requests.
Quando transform_manager recebe a próxima solicitação, ele verifica a presença da imagem finalizada no cache de imagens transformadas. Se não houver imagem convertida, os contêineres m_inprogress_requests e m_pending_requests serão verificados. E se não houver uma solicitação com esses parâmetros em nenhum desses contêineres, apenas uma tentativa será feita para colocar a solicitação na fila m_pending_requests. É
assim :
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 ) ) {
Foi dito acima que m_inprogress_requests e m_pending_requests são contêineres complicados. Mas qual é o truque?
O truque é que esses contêineres combinam as propriedades de uma fila FIFO regular (na qual a ordem cronológica de adição de elementos é preservada) e o multimap, ou seja, Um contêiner associativo no qual vários valores podem ser mapeados para uma única chave.
Manter a ordem cronológica é importante, porque os elementos mais antigos em m_pending_requests precisam ser verificados periodicamente e removidos de m_pending_requests aqueles pedidos para os quais o tempo limite máximo foi excedido. E o acesso efetivo aos elementos por chave é necessário para verificar a presença de solicitações idênticas nas filas e para que todas as solicitações duplicadas possam ser removidas da fila por vez.
Na Shrimp, pedalamos
nosso pequeno contêiner para esses fins. Embora, se o Boost fosse usado no camarão, o Boost.MultiIndex poderia ser usado. E, provavelmente, com o tempo, uma pesquisa eficaz em m_pending_requests precisará ser organizada por alguns outros critérios; o Boost.MultiIndex no Shrimp terá que ser ativado.
v0.4: registrando com spdlog
Tentamos deixar a primeira versão do Shrimp o mais simples e compacta possível. Por causa disso, na primeira versão do Shrimp, não usamos log. Geralmente.
Por um lado, isso tornou possível manter o código da primeira versão conciso, contendo nada além da lógica de negócios necessária do Shrimp. Mas, por outro lado, a falta de exploração madeireira complica tanto o desenvolvimento do camarão quanto sua operação. Portanto, assim que colocamos nossas mãos nele, imediatamente arrastamos para o Shrimp uma excelente biblioteca C ++ moderna para registro -
spdlog . Respirar imediatamente se tornou mais fácil, embora o código de alguns métodos tenha aumentado em volume.
Por exemplo, o código acima do método handle_not_transformed_image () com registro começa com a seguinte aparência:
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 ) ) {
Configurando registradores spdlog
O login no Shrimp é feito no console (ou seja, no fluxo de saída padrão). Em princípio, pode-se seguir um caminho muito simples e criar no Shrimp a única instância do spd-logger. I.e. pode-se chamar
stdout_color_mt (ou
stdout_logger_mt ) e depois passar esse registrador para todas as entidades no Shrimp. Mas fomos um pouco mais complicados: criamos manualmente o chamado coletor (ou seja, o canal em que o spdlog emitirá as mensagens geradas) e, para as entidades de camarão, eles criaram registradores separados anexados a esse coletor.
Há um ponto sutil na configuração de registradores no spdlog: por padrão, o registrador ignora mensagens com níveis de gravidade de rastreamento e depuração. Ou seja, eles provam ser mais úteis ao depurar. Portanto, no make_logger, ativamos por padrão o log para todos os níveis, incluindo rastreamento / depuração.
Devido ao fato de que cada entidade no Shrimp tem seu próprio logger com seu próprio nome, podemos ver quem faz o que no log:

Rastreando SObjectizer com spdlog
Os tempos de log, que são executados como parte da lógica comercial principal de um aplicativo SObjectizer, não são suficientes para depurar o aplicativo. Não está claro por que alguma ação é iniciada em um agente, mas na verdade não é executada em outro agente. Nesse caso, o mecanismo msg_tracing embutido no SObjectizer ajuda muito (sobre o qual falamos
em um artigo separado ). Mas entre as implementações padrão msg_tracing para SObjectizer, não há uma que use spdlog. Faremos essa implementação para o Shrimp:
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) ) ); } };
Aqui vemos a implementação da interface especial do SObjectizer tracer_t, na qual o principal é o método virtual trace (). É ele quem realiza o rastreamento das partes internas do SObjectizer por meio do spdlog.
Em seguida, esta implementação é instalada como um rastreador ao iniciar o 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 ) ); } };
Rastreio RESTinio através do spdlog
Além de rastrear o que acontece dentro do SObjectizer, às vezes pode ser muito útil rastrear o que acontece dentro do RESTinio. Na versão atualizada do Shrimp, esse rastreamento também é adicionado.
Esse rastreamento é implementado através da definição de uma classe especial que pode executar o log no 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; };
Essa classe não é herdada de nada, uma vez que o mecanismo de registro no RESTinio é baseado em programação generalizada e não na abordagem tradicional orientada a objetos. Isso permite que você se livre de qualquer sobrecarga nos casos em que o log não é necessário (abordamos esse tópico com mais detalhes quando
falamos sobre o uso de modelos no RESTinio ).
Em seguida, precisamos indicar que o servidor HTTP usará a classe http_server_logger_t mostrada acima como seu logger. Isso é feito esclarecendo as propriedades do 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; };
Bem, não há mais o que fazer - crie uma instância específica do spd-logger e envie-o para o servidor HTTP criado:
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: redefinição forçada do cache de imagem transformado
No processo de depuração do Shrimp, uma pequena coisa foi descoberta: um pouco irritante: para liberar o conteúdo do cache de imagem transformado, era necessário reiniciar todo o Shrimp. Parece um pouco, mas desagradável.
Como é desagradável, você deve se livrar dessa falha. Felizmente, isso não é nada difícil.
Primeiro, definiremos outra URL no Shrimp para a qual você pode enviar solicitações HTTP DELETE: "/ cache". Conseqüentemente, colocaremos nosso manipulador neste 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; }
onde a função add_delete_cache_handler () se parece com isso:
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 ) { 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" ); }
Um pouco detalhado, mas nada complicado. A cadeia de consulta da consulta deve ter um parâmetro de token. Este parâmetro deve conter uma sequência com um valor especial para o token administrativo. Você só pode redefinir o cache se o valor do token do parâmetro token corresponder ao que foi definido quando o Shrimp foi iniciado. Se não houver parâmetro de token, a solicitação de processamento não será aceita. Se houver token, o agente transform_manager, dono do cache, receberá uma mensagem de comando especial, executando o qual o próprio agente transform_manager responderá à solicitação HTTP.
Em segundo lugar, implementamos o novo manipulador de mensagens delete_cache_request_t no 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" );
Há dois pontos aqui que devem ser esclarecidos.
O primeiro ponto na implementação de on_delete_cache_request () é a verificação do próprio valor do token. O token administrativo é definido através da variável de ambiente SHRIMP_ADMIN_TOKEN. Se essa variável estiver configurada e seu valor corresponder ao valor do parâmetro token da solicitação HTTP DELETE, o cache será limpo e uma resposta positiva à solicitação será gerada imediatamente.
E o segundo ponto na implementação de on_delete_cache_request () é o atraso forçado de uma resposta negativa ao HTTP DELETE. Se o valor incorreto do token administrativo chegar, você deve adiar a resposta ao HTTP DELETE para que não haja desejo de selecionar o valor do token por força bruta. Mas como fazer esse atraso? Afinal, chamar std :: thread :: sleep_for () não é uma opção.
É aqui que as mensagens pendentes do SObjectizer são resgatadas. Em vez de gerar imediatamente uma resposta negativa dentro de on_delete_cache_request (), o agente transform_manager simplesmente envia a si mesmo uma mensagem negativa_delete_cache_response_t pendente. O temporizador do SObjectizer contará o tempo definido e entregará esta mensagem ao agente após o atraso especificado. E agora no manipulador negative_delete_cache_response_t, você já pode gerar imediatamente uma resposta à solicitação 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) ); }
I.e. acontece o seguinte cenário:
- O servidor HTTP recebe uma solicitação HTTP DELETE, converte essa solicitação em uma mensagem delete_cache_request_t no agente transform_manager;
- O agente transform_manager recebe a mensagem delete_cache_request_t e imediatamente gera uma resposta positiva à solicitação ou envia uma mensagem negativa_delete_cache_response_t pendente;
- transform_manager recebe uma mensagem negative_delete_cache_response_t e gera imediatamente uma resposta negativa para a solicitação HTTP DELETE correspondente.
Fim da segunda parte
No final da segunda parte, é bastante natural fazer a pergunta: "O que vem depois?"
Além disso, provavelmente haverá outra iteração e outra atualização do nosso projeto de demonstração. Eu gostaria de fazer uma coisa como converter uma imagem de um formato para outro. Por exemplo, no servidor, a imagem está em jpg e, após a transformação, é enviada ao cliente no webp.
Também seria interessante anexar uma "página" separada com a exibição das estatísticas atuais sobre o trabalho do Camarão. Primeiro de tudo, é apenas curioso. Mas, em princípio, essa página também pode ser adaptada às necessidades de monitoramento da viabilidade do camarão.
Se alguém tiver sugestões sobre o que eu gostaria de ver em Camarão ou em artigos sobre Camarão, teremos prazer em ouvir qualquer pensamento construtivo.
Separadamente, quero observar um aspecto na implementação do Camarão, que nos surpreendeu um pouco. Este é um uso ativo de mensagens mutáveis ao se comunicar com o servidor HTTP. Geralmente, em nossa prática, acontece o oposto - mais frequentemente os dados são trocados por mensagens imunes. Não é assim aqui. Isso sugere que ouvimos conscientemente os desejos dos usuários no devido tempo e adicionamos mensagens mutáveis ao SObjectizer. Portanto, se você gostaria de ver algo no RESTinio ou no SObjectizer, fique à vontade para compartilhar suas idéias. Temos a certeza de ouvir os bons.
Bem, e em conclusão, gostaria de agradecer a todos que se interessaram e falaram sobre a primeira versão do Shrimp, tanto aqui em Habré quanto através de outros recursos. Obrigada
Para continuar ...