
Prefácio
Nossa pequena equipe está desenvolvendo duas ferramentas OpenSource para desenvolvedores de C ++ - a
estrutura do ator
SObjectizer e o servidor HTTP incorporado RESTinio. No entanto, encontramos regularmente algumas perguntas não triviais:
- quais recursos adicionar à biblioteca e quais deixar "ao mar"?
- como demonstrar claramente as maneiras "ideologicamente corretas" de usar a biblioteca?
É bom quando as respostas a essas perguntas aparecem durante o uso de nossos desenvolvimentos em projetos reais, quando os desenvolvedores vêm até nós com suas reclamações ou lista de desejos. Devido à satisfação dos desejos dos usuários, preenchemos nossas ferramentas com funcionalidades ditadas pela própria vida, e não "sugadas pelo dedo".
Mas as informações nos chegam longe de todos os problemas e dificuldades que os usuários enfrentam. E nem sempre podemos usar as informações recebidas, e especialmente os exemplos de código, em nossos materiais públicos.
Portanto, às vezes pensamos em pequenos problemas para nós mesmos, resolvendo os quais somos forçados a transformar de desenvolvedores de ferramentas em usuários. Isso nos permite olhar para nossas próprias ferramentas com olhos diferentes e entender por nós mesmos o que é bom, o que não é bom, o que está faltando e o que é demais.
Hoje, queremos contar apenas uma dessas tarefas "pequenas", na qual SObjectizer e RESTinio se uniram naturalmente.
Dimensionamento e distribuição de imagens. Por que exatamente isso?
Como uma pequena tarefa de demonstração para nós mesmos, escolhemos um servidor HTTP que distribui imagens em escala mediante solicitação. Coloque as imagens em algum diretório, inicie o servidor HTTP, faça uma solicitação no formato:
curl "http://localhost:8080/my_picture.jpg?op=resize&max=1920"
e, em troca, você obtém uma imagem dimensionada para 1920 pixels no lado comprido.
A escolha recaiu nessa tarefa, pois demonstra perfeitamente os cenários para os quais começamos a desenvolver o RESTinio: existe um código de longa execução e depurado em C ou C ++, ao qual você precisa anexar uma entrada HTTP e começar a responder às solicitações recebidas. Ao mesmo tempo, o que é importante, o processamento do aplicativo de uma solicitação pode levar um tempo considerável e, portanto, não é rentável extrair o código do aplicativo diretamente no contexto de E / S. O servidor HTTP deve ser assíncrono: aceite e analise a solicitação HTTP, forneça a solicitação analisada em algum lugar para processamento adicional do aplicativo, continue atendendo à próxima solicitação HTTP, retorne a retornar a resposta à solicitação HTTP quando essa resposta for preparada por alguém.
É exatamente o que acontece ao processar solicitações de dimensionamento de imagens. Um servidor HTTP pode fazer seu trabalho direto (ou seja, ler dados, analisar uma solicitação HTTP) em uma fração de milissegundo. Mas dimensionar uma imagem pode levar dezenas, centenas ou até milhares de milissegundos.
E como pode levar muito tempo para dimensionar uma imagem, você precisa garantir que o servidor HTTP possa continuar funcionando enquanto a imagem é dimensionada. Para fazer isso, precisamos espalhar o trabalho do servidor HTTP e dimensionar imagens para diferentes contextos de trabalho. No caso simples, serão diferentes segmentos de trabalho. Bem, como vivemos em processadores com vários núcleos, teremos vários threads de trabalho. Alguns deles servirão solicitações HTTP, outros funcionarão com imagens.
Acontece que, para distribuir imagens escalonáveis via HTTP, precisamos reutilizar o código C / C ++ de gravação longa e funcional (neste caso ImageMagic ++), atender solicitações HTTP de forma assíncrona e executar o processamento de solicitações de aplicativos em vários fluxos de trabalho. Uma excelente tarefa para o RESTinio e o SObjectizer, como nos pareceu.
E decidimos nomear nosso camarão do projeto de demonstração.
Camarão como é
O que o camarão faz?
O Shrimp é executado como um aplicativo de console, abre e escuta na porta especificada, recebe e processa solicitações HTTP GET do formulário:
/<image>.<ext> /<image>.<ext>?op=resize&<side>=<value>
Onde:
- image é o nome do arquivo de imagem a ser dimensionado. Por exemplo, my_picture ou DSCF0069;
- ext é uma das extensões suportadas por camarão (jpg, jpeg, png ou gif);
- side é uma indicação do lado para o qual o tamanho está definido. Pode ter um valor de largura; nesse caso, a imagem é redimensionada para que a largura resultante seja igual ao valor especificado, a altura da imagem é selecionada automaticamente, mantendo a proporção da imagem. Ou o valor da altura, nesse caso, a escala ocorre na altura. No máximo, neste caso, o lado comprido é limitado e o próprio camarão determina se o lado comprido é altura ou largura;
- value é o tamanho em que a escala ocorre.
Se apenas o nome do arquivo for especificado na URL, sem a operação de redimensionamento, o camarão simplesmente retornará a imagem original na resposta. Se a operação de redimensionamento for especificada, o camarão altera o tamanho da imagem solicitada e fornece a versão em escala.
Ao mesmo tempo, o camarão mantém na memória um cache de imagens em escala. Se uma imagem for solicitada repetidamente com os mesmos parâmetros de redimensionamento, que já estão no cache, o valor do cache será retornado. Se não houver imagem no cache, a imagem será lida no disco, dimensionada, armazenada no cache e retornada em resposta.
O cache é limpo periodicamente. As imagens que permanecem no cache há mais de uma hora desde o último acesso a elas são removidas. Além disso, as imagens mais antigas são jogadas para fora do cache se o cache exceder seu tamanho máximo (em um projeto de demonstração é de 100 Mb).
Preparamos uma
página na qual qualquer pessoa pode experimentar camarão:

Nesta página, você pode definir o tamanho da imagem e clicar em "Redimensionar". Dois pedidos serão feitos ao servidor camarão com os mesmos parâmetros. Provavelmente, a primeira solicitação será exclusiva (ou seja, ainda não haverá um cache com esses parâmetros de redimensionamento), portanto, a primeira solicitação levará algum tempo para realmente dimensionar a imagem. E a segunda solicitação, provavelmente, encontrará a imagem já escalada no cache e a fornecerá imediatamente.
É possível julgar se uma imagem é fornecida a partir do cache ou se realmente foi dimensionada pelo texto sob a imagem. Por exemplo, o texto "Transformado (114.0ms)" indica que a imagem foi dimensionada e a operação de zoom levou 114 milissegundos.
Como o camarão faz isso?
O Shrimp é um aplicativo multithread que executa três grupos de threads de trabalho:
- O conjunto de threads de trabalho executando o servidor HTTP. Nesse pool, novas conexões são atendidas, solicitações de entrada são recebidas e analisadas, respostas são geradas e enviadas. O servidor HTTP é implementado através da biblioteca RESTinio.
- Um encadeamento de trabalho separado no qual o agente SO transformação de transform_manager é executado. Este agente processa solicitações recebidas do servidor HTTP e mantém um cache de imagens em escala.
- O conjunto de encadeamentos no qual os agentes do SObjectizer estão trabalhando nos transformadores. Eles realizam o dimensionamento real das imagens usando o ImageMagic ++.
Acontece o seguinte esquema de trabalho:

O servidor HTTP aceita a solicitação recebida, analisa-a e verifica a correção. Se essa solicitação não exigir uma operação de redimensionamento, o próprio servidor HTTP processará a solicitação por meio da operação
sendfile . Se a solicitação exigir uma operação de redimensionamento, a solicitação será enviada de forma assíncrona para o agente transform_manager.
O agente transform_manager recebe solicitações do servidor HTTP, verifica a presença de imagens já dimensionadas no cache. Se houver uma imagem no cache, o transform_manager gera imediatamente uma resposta para o servidor HTTP. Se não houver imagem, o transform_manager envia uma solicitação para dimensionar a imagem para um dos agentes do transformador. Quando o resultado da escala vem do transformador, o resultado é armazenado no cache e uma resposta é gerada para o servidor HTTP.
O agente transformador recebe solicitações de transform_manager, as processa e retorna o resultado da transformação de volta ao agente transform_manager.
O que o camarão tem sob o capô?
O código fonte da versão mais mínima do camarão descrita neste artigo pode ser encontrado neste repositório:
shrimp-demo no BitBucket ou
no GitHub .
Há muito código, embora, na maioria das vezes, nesta versão do camarão, o código seja bastante trivial. No entanto, faz sentido se concentrar em alguns aspectos da implementação.
Usando C ++ 17 e as versões mais recentes do compilador
Na implementação do camarão, decidimos usar o C ++ 17 e as versões mais recentes dos compiladores, em particular o GCC 7.3 e 8.1. O projeto é fortemente pesquisado. Portanto, uma introdução prática ao C ++ 17 no âmbito de um projeto desse tipo é natural e admissível. Enquanto em desenvolvimentos mais mundanos focados em aplicações industriais práticas aqui e agora, somos forçados a olhar para compiladores bastante antigos e usar talvez o C ++ 14, ou mesmo apenas um subconjunto do C ++ 11.
Devo dizer que o C ++ 17 causa uma boa impressão. Parece que não usamos tantas inovações do décimo sétimo padrão no código do camarão, mas elas tiveram um efeito positivo: o atributo [[nodiscard]], std :: opcional / std :: variant / std :: filesystem diretamente " pronto para uso ”, e não a partir de dependências externas, ligação estruturada, se for o caso, a capacidade de reunir visitantes para lambdas para std :: visit ... Individualmente, todos esses são insignificantes, mas juntos eles produzem um poderoso efeito cumulativo.
Portanto, o primeiro resultado útil que obtivemos ao desenvolver o camarão: C ++ 17 vale a pena mudar para ele.
Servidor HTTP usando ferramentas RESTinio
Talvez a parte mais fácil do camarão tenha sido o servidor HTTP e o manipulador de solicitações HTTP GET (
http_server.hpp e
http_server.cpp ).
Receber e enviar solicitações de entrada
Essencialmente, toda a lógica básica do servidor HTTP de camarão está concentrada nesta função:
void add_transform_op_handler( const app_params_t & app_params, http_req_router_t & router, so_5::mbox_t req_handler_mbox ) { router.http_get( R"(/:path(.*)\.:ext(.{3,4}))", restinio::path2regex::options_t{}.strict( true ), [req_handler_mbox, &app_params]( auto req, auto params ) { if( has_illegal_path_components( req->header().path() ) ) { return do_400_response( std::move( req ) ); } const auto opt_image_format = image_format_from_extension( params[ "ext" ] ); if( !opt_image_format ) { return do_400_response( std::move( req ) ); } if( req->header().query().empty() ) { return serve_as_regular_file( app_params.m_storage.m_root_dir, std::move( req ), *opt_image_format ); } const auto qp = restinio::parse_query( req->header().query() ); if( "resize" != restinio::value_or( qp, "op"sv, ""sv ) ) { return do_400_response( std::move( req ) ); } handle_resize_op_request( req_handler_mbox, *opt_image_format, qp, std::move( req ) ); return restinio::request_accepted(); } ); }
Essa função prepara o manipulador de solicitações HTTP GET usando o
roteador RESTinio
ExpressJS . Quando o servidor HTTP recebe uma solicitação GET, cuja URL se enquadra na expressão regular especificada, a função lambda especificada é chamada.
Essa função lambda faz algumas verificações simples sobre a exatidão da solicitação, mas, principalmente, seu trabalho se resume a uma escolha simples: se o redimensionamento não estiver definido, a imagem solicitada será retornada em sua forma original usando um arquivo de envio eficaz do sistema. Se o modo de redimensionamento estiver configurado, uma mensagem será gerada e enviada ao agente transform_manager:
void handle_resize_op_request( const so_5::mbox_t & req_handler_mbox, image_format_t image_format, const restinio::query_string_params_t & qp, restinio::request_handle_t req ) { try_to_handle_request( [&]{ auto op_params = transform::resize_params_t::make( restinio::opt_value< std::uint32_t >( qp, "width" ), restinio::opt_value< std::uint32_t >( qp, "height" ), restinio::opt_value< std::uint32_t >( qp, "max" ) ); transform::resize_params_constraints_t{}.check( op_params ); std::string image_path{ req->header().path() }; so_5::send< so_5::mutable_msg<a_transform_manager_t::resize_request_t>>( req_handler_mbox, std::move(req), std::move(image_path), image_format, op_params ); }, req ); }
Acontece que o servidor HTTP, tendo aceitado a solicitação de redimensionamento, a entrega ao agente transform_manager por meio de uma mensagem assíncrona e continua a atender a outras solicitações.
Compartilhamento de arquivos com sendfile
Se o servidor HTTP detectar uma solicitação para a imagem original, sem a operação de redimensionamento, o servidor envia imediatamente essa imagem através da operação sendfile. O código principal associado a isso é o seguinte (o código completo para esta função pode ser encontrado
no repositório ):
[[nodiscard]] restinio::request_handling_status_t serve_as_regular_file( const std::string & root_dir, restinio::request_handle_t req, image_format_t image_format ) { const auto full_path = make_full_path( root_dir, req->header().path() ); try { auto sf = restinio::sendfile( full_path ); ... return set_common_header_fields_for_image_resp( file_stat.st_mtim.tv_sec, resp ) .append_header( restinio::http_field::content_type, image_content_type_from_img_format( image_format ) ) .append_header( http_header::shrimp_image_src, image_src_to_str( http_header::image_src_t::sendfile ) ) .set_body( std::move( sf ) ) .done(); } catch(...) {} return do_404_response( std::move( req ) ); }
O ponto principal aqui é chamar
restinio :: sendfile () e, em seguida, passar o valor retornado por esta função para set_body ().
A função restinio :: sendfile () cria uma operação de upload de arquivo usando a API do sistema. Quando essa operação é passada para set_body (), o RESTinio entende que o conteúdo do arquivo especificado em restinio :: sendfile () será usado para o corpo da resposta HTTP. Em seguida, ele usa a API do sistema para gravar o conteúdo desse arquivo no soquete TCP.
Implementando o cache de imagem
O agente transform_manager armazena o cache de imagens convertidas, onde as imagens são colocadas após o dimensionamento. Esse cache é um contêiner personalizado simples que fornece acesso ao seu conteúdo de duas maneiras:
- Procurando um elemento por chave (semelhante à maneira como isso acontece nos contêineres padrão std :: map e std :: unordered_map).
- Acessando o item de cache mais antigo.
O primeiro método de acesso é usado quando precisamos verificar a disponibilidade da imagem no cache. A segunda é quando excluímos as imagens mais antigas do cache.
Não começamos a procurar algo pronto para esses fins na Internet. Provavelmente o Boost.MultiIndex seria bastante adequado aqui. Mas como eu não queria arrastar o Boost apenas pelo MultiIndex, fizemos
nossa implementação trivial literalmente de joelhos. Parece funcionar;)
Fila de solicitações pendentes no transform_manager
O agente transform_manager, apesar de seu tamanho bastante decente (um
arquivo hpp de cerca de 250 linhas e um
arquivo cpp de cerca de 270 linhas), em nossa implementação mais simples de camarão, acabou sendo bastante trivial, em nossa opinião.
Uma das coisas que contribui significativamente para a complexidade e quantidade de código do agente é a presença não apenas de um cache de imagens transformadas no transform_manager, mas também de filas de solicitações pendentes.
Temos um número limitado de agentes de transformação (em princípio, seu número deve corresponder aproximadamente ao número de núcleos de processamento disponíveis). Se mais solicitações forem simultâneas do que transformadores livres, poderemos responder imediatamente negativamente à solicitação ou enfileirá-la. E então retire-o da fila quando um transformador livre aparecer.
No camarão, usamos uma fila de pedidos em espera, definida da seguinte forma:
struct pending_request_t { transform::resize_request_key_t m_key; sobj_shptr_t<resize_request_t> m_cmd; std::chrono::steady_clock::time_point m_stored_at; pending_request_t( transform::resize_request_key_t key, sobj_shptr_t<resize_request_t> cmd, std::chrono::steady_clock::time_point stored_at ) : m_key{ std::move(key) } , m_cmd{ std::move(cmd) } , m_stored_at{ stored_at } {} }; using pending_request_queue_t = std::queue<pending_request_t>; pending_request_queue_t m_pending_requests; static constexpr std::size_t max_pending_requests{ 64u };
Após o recebimento da solicitação, nós a colocamos na fila, fixando o horário de recebimento da solicitação. Periodicamente, verificamos se o tempo limite dessa solicitação expirou. De fato, em princípio, pode acontecer que um pacote de solicitações "pesadas" tenha chegado mais cedo, cujo processamento demorou muito. É errado esperar incessantemente que um transformador gratuito apareça; é melhor enviar uma resposta negativa ao cliente após algum tempo, o que significa que o serviço agora está sobrecarregado.
Há também um limite de tamanho para a fila de solicitações pendentes. Se a fila já atingiu seu tamanho máximo, imediatamente nos recusamos a processar a solicitação e informamos ao cliente que estamos sobrecarregados.
Há um ponto importante relacionado à fila de solicitações pendentes, nas quais focaremos na conclusão do artigo.
Digite sobj_shptr_t e reutilize instâncias de mensagem
Ao determinar o tipo de fila de pedidos em espera, bem como nas assinaturas de alguns métodos do transform_manager, é possível ver o uso do tipo sobj_shptr_t. Faz sentido se debruçar com mais detalhes sobre que tipo é e por que é usado.
A linha inferior é que transform_manager recebe uma solicitação do servidor HTTP como uma mensagem resize_request_t:
struct resize_request_t final : public so_5::message_t { restinio::request_handle_t m_http_req; std::string m_image; image_format_t m_image_format; transform::resize_params_t m_params; resize_request_t( restinio::request_handle_t http_req, std::string image, image_format_t image_format, transform::resize_params_t params ) : m_http_req{ std::move(http_req) } , m_image{ std::move(image) } , m_image_format{ image_format } , m_params{ params } {} };
e precisamos fazer algo para armazenar essas informações na fila de solicitações em espera. Por exemplo, você pode criar uma nova instância de resize_request_t e mover os valores da mensagem recebida para ela.
E você pode se lembrar que a própria mensagem no SObjectizer é um objeto criado dinamicamente. E não é um objeto simples, mas com um contador de links dentro. E que no SObjectizer existe um tipo especial de ponteiro inteligente para esses objetos - intrusive_ptr_t.
I.e. não podemos fazer uma cópia de resize_request_t para a fila de solicitações em espera, mas podemos simplesmente colocar nessa fila um ponteiro inteligente para uma instância existente de resize_request_t. O que fazemos E para não escrever em todos os lugares o nome bastante exótico so_5 :: intrusive_ptr_t, inserimos nosso alias:
template<typename T> using sobj_shptr_t = so_5::intrusive_ptr_t<T>;
Respostas assíncronas aos clientes
Dissemos que as solicitações HTTP são processadas de forma assíncrona. E mostramos acima como o servidor HTTP envia uma consulta ao agente transform_manager com uma mensagem assíncrona. Mas o que acontece com as respostas às solicitações HTTP?
As respostas também são exibidas de forma assíncrona. Por exemplo, no código transform_manager, você pode ver o seguinte:
void a_transform_manager_t::on_failed_resize( failed_resize_t & , sobj_shptr_t<resize_request_t> cmd ) { do_404_response( std::move(cmd->m_http_req) ); }
Esse código gera uma resposta negativa à solicitação HTTP no caso em que a imagem não pôde ser dimensionada por algum motivo. A resposta é gerada na função auxiliar do_404_response, cujo código pode ser representado da seguinte maneira:
auto do_404_response( restinio::request_handle_t req ) { auto resp = req->create_response( 404, "Not Found" ); resp.append_header( restinio::http_field_t::server, "Shrimp draft server" ); resp.append_header_date_field(); if( req->header().should_keep_alive() ) resp.connection_keep_alive(); else resp.connection_close(); return resp.done(); }
O primeiro ponto-chave com do_404_response () é que essa função é chamada no contexto de trabalho do agente transform_manager e não no contexto de trabalho do servidor HTTP.
O segundo ponto principal é a chamada para o método done () no objeto resp totalmente formado. Toda mágica assíncrona com uma resposta HTTP acontece aqui. O método done () pega todas as informações preparadas e as envia de forma assíncrona para o servidor HTTP. I.e. um retorno de do_404_response () ocorrerá imediatamente após o conteúdo do objeto resp ser enfileirado pelo servidor HTTP.
O servidor HTTP em seu contexto de trabalho detectará a presença de uma nova resposta HTTP e começará a executar as ações necessárias para enviar a resposta ao cliente apropriado.
Digite datasizable_blob_t
Outro pequeno ponto que faz sentido para esclarecer, porque provavelmente é incompreensível sem entender os meandros do RESTinio. Estamos falando da presença de, à primeira vista, um tipo estranho de datasizeable_blob_t, definido da seguinte maneira:
struct datasizable_blob_t : public std::enable_shared_from_this< datasizable_blob_t > { const void * data() const noexcept { return m_blob.data(); } std::size_t size() const noexcept { return m_blob.length(); } Magick::Blob m_blob;
Para explicar por que esse tipo é necessário, você precisa mostrar como uma resposta HTTP é formada com uma imagem transformada:
void serve_transformed_image( restinio::request_handle_t req, datasizable_blob_shared_ptr_t blob, image_format_t img_format, http_header::image_src_t image_src, header_fields_list_t header_fields ) { auto resp = req->create_response(); set_common_header_fields_for_image_resp( blob->m_last_modified_at, resp ) .append_header( restinio::http_field::content_type, image_content_type_from_img_format( img_format ) ) .append_header( http_header::shrimp_image_src, image_src_to_str( image_src ) ) .set_body( std::move( blob ) ); for( auto & hf : header_fields ) { resp.append_header( std::move( hf.m_name ), std::move( hf.m_value ) ); } resp.done(); }
Prestamos atenção na chamada para set_body (): um ponteiro inteligente para a instância datasizable_blob_t é enviado diretamente para lá. Porque
O fato é que o
RESTinio suporta várias opções para formar o corpo de uma resposta HTTP . O mais simples é passar uma instância do tipo std :: string para set_body () e o RESTinio salvará o valor dessa string no objeto resp.
Mas há momentos em que o valor de set_body () deve ser reutilizado em várias respostas ao mesmo tempo. Por exemplo, no camarão, isso acontece quando o camarão recebe vários pedidos idênticos de transformação da mesma imagem.
Nesse caso, não é rentável copiar o mesmo valor em cada resposta. Portanto, no RESTinio, há uma variante set_body () do formulário: template<typename T> auto set_body(std::shared_ptr<T> body);
Porém, neste caso, uma limitação importante é imposta ao tipo T: ele deve conter os métodos public data () e size (), necessários para que o RESTinio possa acessar o conteúdo da resposta.A imagem em escala no camarão é armazenada como um objeto Magick :: Blob. Existe um método de dados no tipo Magic :: Blob, mas não existe o método size (), mas existe o método length (). Portanto, precisamos da classe wrapper datasizable_blob_t, que fornece ao RESTinio a interface necessária para acessar o valor do Magick :: Blob.Mensagens periódicas no transform_manager
O agente transform_manager precisa fazer várias coisas de tempos em tempos:- Puxe as fotos que estão no cache por muito tempo.
- controlar o tempo gasto por solicitações na fila de espera de transformadores livres.
O agente transform_manager executa essas ações por meio de mensagens periódicas. Parece o seguinte.Primeiro, os tipos de sinais que serão usados como mensagens periódicas são determinados: struct clear_cache_t final : public so_5::signal_t {}; struct check_pending_requests_t final : public so_5::signal_t {};
Em seguida, o agente é inscrito, incluindo estes sinais: void a_transform_manager_t::so_define_agent() { so_subscribe_self() .event( &a_transform_manager_t::on_resize_request ) .event( &a_transform_manager_t::on_resize_result ) .event( &a_transform_manager_t::on_clear_cache ) .event( &a_transform_manager_t::on_check_pending_requests ); } void a_transform_manager_t::on_clear_cache( mhood_t<clear_cache_t> ) {...} void a_transform_manager_t::on_check_pending_requests( mhood_t<check_pending_requests_t> ) {...}
Graças à assinatura, o SObjectizer chamará o manipulador desejado quando o agente receber o sinal correspondente.E resta apenas executar mensagens periódicas quando o agente é iniciado: void a_transform_manager_t::so_evt_start() { m_clear_cache_timer = so_5::send_periodic<clear_cache_t>( *this, clear_cache_period, clear_cache_period ); m_check_pending_timer = so_5::send_periodic<check_pending_requests_t>( *this, check_pending_period, check_pending_period ); }
O ponto principal aqui é salvar timer_id, retornado pelas funções send_periodic (). Afinal, um sinal periódico virá apenas enquanto seu timer_id estiver ativo. Portanto, se o valor de retorno send_periodic () não for salvo, o envio de uma mensagem periódica será imediatamente cancelado. Portanto, a classe a_transform_manager_t possui os seguintes atributos: so_5::timer_id_t m_clear_cache_timer; so_5::timer_id_t m_check_pending_timer;
Fim da primeira parte
Hoje, apresentamos ao leitor a implementação mais simples e minimalista do camarão. Essa implementação é suficiente para mostrar como o RESTinio e o SObjectizer podem ser usados juntos para algo mais ou menos como uma tarefa real, em vez de um simples HelloWorld. Mas tem uma série de falhas sérias.Por exemplo, no agente transform_manager, há uma certa verificação da exclusividade da solicitação. Mas isso só funciona se a imagem transformada já estiver no cache. Se ainda não houver uma imagem no cache e, ao mesmo tempo, duas solicitações idênticas vierem para a mesma imagem, ambas as solicitações serão enviadas para processamento. O que não é bom. Seria correto processar apenas um deles e adiar o segundo até que o processamento do primeiro seja concluído.Esse controle mais avançado sobre a exclusividade de solicitações levaria a um código transform_manager muito mais complexo e volumoso. Portanto, não começamos a implementá-lo imediatamente, mas decidimos seguir o caminho evolutivo - do simples ao complexo.Além disso, a versão mais simples do camarão é uma “caixa preta” que não mostra sinais de seu trabalho. O que não é muito conveniente durante o teste e durante a operação. Portanto, de uma maneira boa, o camarão também deve adicionar registros.Vamos tentar eliminar essas e algumas outras deficiências da primeira versão do camarão em versões futuras e descrevê-las em artigos futuros. Portanto, fique atento.Se alguém tiver dúvidas sobre a lógica do camarão, RESTinio ou SObjectizer, teremos o maior prazer em responder nos comentários. Além disso, o próprio camarão é um projeto de demonstração, mas se alguém estiver interessado em sua funcionalidade e desejar ver algo mais além da operação de redimensionamento, informe-nos, teremos o maior prazer em ouvir quaisquer idéias construtivas .Para continuar ...