Shrimp: масштабируем и раздаем по HTTP картинки на современном C++ посредством ImageMagic++, SObjectizer и RESTinio



    Предисловие


    Наша небольшая команда занимается развитием двух OpenSource инструментов для C++разработчиков — акторного фреймворка SObjectizer и встраиваемого HTTP-сервера RESTinio. При этом мы регулярно сталкиваемся с парой нетривиальных вопросов:

    • какие фичи добавлять в библиотеку, а какие оставлять «за бортом»?
    • как наглядно показывать «идеологически правильные» способы использования библиотеки?

    Хорошо, когда ответы на такие вопросы появляются по ходу использования наших разработок в реальных проектах, когда разработчики приходят к нам со своими жалобами или хотелками. За счет удовлетворения хотелок пользователей мы наполняем свои инструменты функциональностью, которая продиктована самой жизнью, а не «высосана из пальца».

    Но до нас доходит информация далеко не о всех проблемах и сложностях, с которыми сталкиваются пользователи. И не всегда мы можем использовать полученную информацию и, особенно, примеры кода, в своих публичных материалах.

    Поэтому иногда мы сами для себя выдумываем небольшие задачки, решая которые мы вынужденно превращаемся из разработчиков инструментов в пользователей. Это позволяет нам взглянуть на собственные инструменты другими глазами и самим понять, что хорошо, что не очень, чего не хватает, а чего слишком много.

    Сегодня мы хотим рассказать как раз об одной такой «небольшой» задачке, в которой естественным образом объединились SObjectizer и RESTinio.

    Масштабирование и раздача картинок. Почему именно это?


    В качестве небольшой демо-задачи для самих себя мы выбрали HTTP-сервер, который раздает по запросам отмасштабированные картинки. Вы складываете изображения в какой-то каталог, запускаете HTTP-сервер, делаете к нему запрос вида:

    curl "http://localhost:8080/my_picture.jpg?op=resize&max=1920"

    и получаете в ответ картинку, отмасштабированную до 1920 пикселей по длинной стороне.

    На эту задачу выбор пал потому, что она отлично демонстрирует сценарии, ради которых мы в свое время начали разрабатывать RESTinio: есть давно работающий и отлаженный код на C или C++ к которому нужно приделать HTTP-вход и начать отвечать на входящие запросы. При этом, что важно, прикладная обработка запроса может занимать значительное время и поэтому невыгодно дергать прикладной код прямо на IO-контексте. HTTP-сервер должен быть асинхронным: принять и разобрать HTTP-запрос, отдать разобранный запрос куда-то для дальнейшей прикладной обработки, перейти к обслуживанию следующего HTTP-запроса, вернуться к отдаче ответа на HTTP-запрос когда этот ответ будет кем-то подготовлен.

    Именно это и происходит при обработке запросов на масштабирование картинок. HTTP-сервер способен выполнить свою непосредственную работу (т.е. чтение данных, парсинг HTTP-запроса) за доли миллисекунд. А вот масштабирование картинки может занимать десятки, сотни, а то и тысячи миллисекунд.

    А раз на масштабирование одной картинки может потребоваться много времени, то нужно сделать так, чтобы HTTP-сервер мог продолжать свою работу пока картинка масштабируется. Для этого нам потребуется разнести работу HTTP-сервера и масштабирования картинок на разные рабочие контексты. В простом случае это будут разные рабочие нити. Ну а поскольку мы живем во время многоядерных процессоров, то рабочих нитей у нас будет несколько. Часть из них будет обслуживать HTTP-запросы, часть — работу с картинками.

    Получается, что для раздачи по HTTP отмасштабированных картинок нам нужно и переиспользовать давно написанный, работающий C/С++ код (в данном случае ImageMagic++), и асинхронно обслуживать HTTP-запросы, и выполнять прикладную обработку запросов в несколько рабочих потоков. Отличная задача для RESTinio и SObjectizer-а, как нам показалось.

    А назвать свой демо-проект мы решили shrimp.

    Shrimp как он есть


    Что делает Shrimp?


    Shrimp запускается как консольное приложение, открывает и слушает указанный порт, принимает и обрабатывает HTTP GET-запросы вида:

    /<image>.<ext>
    /<image>.<ext>?op=resize&<side>=<value>
    

    Где:

    • image — это имя файла с картинкой для масштабирования. Например, my_picture или DSCF0069;
    • ext — это одно из поддерживаемых shrimp-ом расширений (jpg, jpeg, png или gif);
    • side — это указание стороны для которой задается размер. Может иметь либо значение width, в этом случае картинка масштабируется так, чтобы результирующая ширина была равна заданному значению, высота картинки выбирается автоматически с сохранением пропорций. Либо значение height, в этом случае масштабирование происходит по высоте. Либо max, в этом случае ограничивается длинная сторона, а shrimp сам определяет, является ли длинная сторона высотой или шириной;
    • value — это размер, под который происходит масштабирование.

    Если в URL задано только имя файла, без операции resize, то shrimp просто отдает в ответе исходную картинку. Если же указана операция resize, то shrimp изменяет размер запрошенной картинки и отдает отмасштабированную версию.

    При этом shrimp держит в памяти кэш отмасштабированных картинок. Если повторно запрашивается картинка с такими же параметрами resize, которая уже есть в кэше, то в ответ отдается значение из кэша. Если же картинки в кэше нет, то картинка считывается с диска, масштабируется, сохраняется в кэше и возвращается в ответе.

    Кэш периодически очищается. Из него выталкиваются картинки, которые прожили в кэше больше часа с момента последнего обращения к ним. Так же самые старые картинки выбрасываются из кэша, если кэш превышает свой максимальный размер (в демо-проекте это 100Mb).

    Мы подготовили страничку, зайдя на которую любой желающий может поэкспериментировать со shrimp-ом:



    На этой страничке можно задать размер картинки и нажать «Resize». Будет сделано два запроса к shrimp-серверу с одинаковыми параметрами. Скорее всего, первый запрос будет уникальным (т.е. картинки с такими параметрами resize в кэше еще не будет), поэтому при первом запросе будет потрачено время на реальное масштабирование изображения. А второй запрос, скорее всего, найдет уже отмасштабированную картинку в кэше и отдаст ее сразу же.

    Судить о том, отдана ли картинка из кэша или же была реально отмасштабирована можно по тексту под картинкой. Например, текст «Transformed (114.0ms)» говорит о том, что картинка была отмасштабирована и операция масштабирования заняла 114 миллисекунд.

    Как Shrimp это делает?


    Shrimp — это многопоточное приложение, которое запускает три группы рабочих нитей:

    1. Пул рабочих нитей, на которых работает HTTP-сервер. На этом пуле обслуживаются новые подключения, принимаются и разбираются входящие запросы, формируются и отсылаются ответы. HTTP-сервер реализован посредством библиотеки RESTinio.
    2. Отдельная рабочая нить, на которой работает SObjectizer-овский агент transform_manager. Этот агент обрабатывает полученные от HTTP-сервера запросы и поддерживает кэш отмасштабированных изображений.
    3. Пул рабочих нитей на которых работают SObjectizer-овские агенты transformer-ы. Именно они выполняют реальное масштабирование картинок с помощью ImageMagic++.

    Получается следующая схема работы:



    HTTP-сервер принимает входящий запрос, разбирает его, проверяет корректность. Если этот запрос не требует операции resize, то сам HTTP-сервер обрабатывает запрос посредством операции sendfile. Если же запрос требует операции resize, то запрос асинхронно отсылается агенту transform_manager.

    Агент transform_manager получает запросы от HTTP-сервера, проверяет наличие уже отмасштабированных картинок в кэше. Если картинка в кэше есть, то transform_manager сразу же формирует ответ для HTTP-сервера. Если картинки нет, то transform_manager отсылает запрос на масштабирование картинки одному из агентов transformer. Когда от transformer-а приходит результат масштабирования, то результат сохраняется в кэше и формируется ответ для HTTP-сервера.

    Агент transformer получает запросы от transform_manager-а, обрабатывает их и возвращает результат трансформации обратно агенту transform_manager.

    Что у Shrimp-а под капотом?


    Исходный код самой минималистичной версии shrimp-а, описанной в данной статье, можно найти вот в этом репозитории: shrimp-demo на BitBucket-а или на GitHub-е.

    Кода довольно много, хотя, по большей части, в этой версии shrimp-а код достаточно тривиален. Тем не менее, имеет смысл заострить внимание на некоторых аспектах реализации.

    Использование C++17 и самых свежих версий компиляторов


    В реализации shrimp-а мы решили использовать C++17 и самые свежие версии компиляторов, в частности GCC 7.3 и 8.1. Проект в большой степени исследовательский. Поэтому практическое знакомство C++17 в рамках такого проекта — это естественно и допустимо. Тогда как в более приземленных разработках, ориентированных на практическое промышленное применение здесь и сейчас, мы вынуждены оглядываться на довольно старые компиляторы и использовать разве что C++14, а то и всего лишь подмножество C++11.

    Нужно сказать, что C++17 производит приятное впечатление. Вроде бы в коде shrimp-а мы не так уж и много нововведений из семнадцатого стандарта задействовали, но положительный эффект от них почувствовать довелось: атрибут [[nodiscard]], std::optional/std::variant/std::filesystem прямо «из коробки», а не из внешних зависимостей, structured binding, if constexpr, возможность собрать на лямбдах visitor для std::visit… По отдельности это все мелочи, но в совокупности производят мощный кумулятивный эффект.

    Так что первый полезный результат, который мы получили разрабатывая shrimp: С++17 стоит того, чтобы на него перейти.

    HTTP-сервер средствами RESTinio


    Пожалуй, самой простой частью shrimp-а оказался HTTP-сервер и обработчик HTTP GET-запросов (http_server.hpp и http_server.cpp).

    Прием и диспетчеризация входящих запросов


    По сути, вся основная логика shrimp-овского HTTP-сервера сосредоточена в этой функции:

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

    Эта функция подготавливает обработчик HTTP GET-запросов с использованием реализованного в RESTinio ExpressJS-роутера. Когда HTTP-сервер получает GET-запрос, URL-которого попадает под заданное регулярное выражение, то вызывается заданная лямбда-функция.

    Эта лямбда функция делает несколько простых проверок корректности запроса, но в главном, ее работа сводится к простому выбору: если режим resize не задан, то запрошенная картинка будет возвращена в своем исходном виде с помощью эффективного системного sendfile. Если же режим resize задан, то формируется и отсылается сообщение агенту 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 );
    }

    Получается, что HTTP-сервер, приняв resize-запрос, отдает его агенту transform_manager посредством асинхронного сообщения, а сам продолжает обслуживать другие запросы.

    Раздача файлов с помощью sendfile


    Если HTTP-сервер обнаруживает запрос на исходную картину, без операции resize, то сервер сразу же отдает эту картинку посредством операции sendfile. Основной связанный с этим код выглядит следующим образом (полный код этой функции можно найти в репозитории):

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

    Ключевой момент здесь — это вызов restinio::sendfile(), а затем передача возвращенного данной функцией значения в set_body().

    Функция restinio::sendfile() создает операцию отдачи файла с помощью системного API. Когда эта операция передается в set_body(), то RESTinio понимает, что для тела HTTP-ответа будет использовано содержимое заданного в restinio::sendfile() файла. После чего задействует системный API для записи содержимого этого файла в TCP-сокет.

    Реализация кэша картинок


    Агент transform_manager хранит кэш преобразованных картинок, куда помещаются изображения после масштабирования. Этот кэш представляет из себя простой самодельный контейнер, который предоставляет доступ к своему содержимому двумя способами:

    1. Посредством поиска элемента по ключу (по аналогии с тем, как это происходит в стандартных контейнерах std::map и std::unordered_map).
    2. Посредством обращения к самому старому элементу кэша.

    Первый способ доступа используется когда нам нужно проверить наличие картинки в кэше. Второй — когда мы удаляем самые старые картинки из кэша.

    Искать что-то готовое для этих целей в Интернетах мы не стали. Наверное, Boost.MultiIndex здесь вполне себе подошел бы. Но тащить Boost только ради MultiIndex-а не хотелось, поэтому сделали свою тривиальную реализацию буквально на коленке. Вроде бы работает ;)

    Очередь ждущих запросов в transform_manager


    Агент transform_manager, несмотря на свой довольно таки приличный объем (hpp-файл порядка 250 строк и cpp-файл порядка 270 строк), в простейшей реализации shrimp-а оказался, на наш взгляд, довольно тривиальным.

    Один из моментов, который привносит заметную лепту в сложность и объем кода агента — это наличие в transform_manager не только кэша трансформированных картинок, но еще и очереди ожидающих запросов.

    У нас есть ограниченное количество агентов transformer-ов (в принципе, их количество должно приблизительно соответствовать количеству доступных вычислительных ядер). Если одномоментно приходит больше запросов, чем есть свободных transformer-ов, то мы можем либо сразу отрицательно ответить на запрос, либо поставить запрос в очередь. А потом взять из очереди, когда свободный transformer появится.

    В shrimp-е мы используем очередь ждущих запросов, которая определяется следующим образом:

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

    При получении запроса мы помещаем его в очередь с фиксацией времени получения запроса. Затем мы периодически проверяем, не истекло ли время ожидания для этого запроса. Ведь, в принципе, может случится так, что ранее пришла пачка «тяжелых» запросов, обработка которых затянулась слишком надолго. Неправильно бесконечно ждать появление свободного transformer-а, лучше спустя какое-то время отослать клиенту отрицательный ответ, означающий, что сервис сейчас перегружен.

    Для очереди ожидающих запросов так же есть ограничение на размер. Если очередь уже достигла своего максимального размера, то мы сразу же отказываемся обрабатывать запрос и говорим клиенту о том, что мы перегружены.

    С очередью ожидающих запросов связан один важный момент, на котором мы заострим внимание в заключении к статье.

    Тип sobj_shptr_t и переиспользование экземпляров сообщений


    В определении типа очереди ждущих запросов, а также в сигнатурах некоторых методов transform_manager-а можно увидеть использование типа sobj_shptr_t. Есть смысл остановится подробнее на том, что это за тип и почему он используется.

    Суть в том, что transform_manager получает запрос от HTTP-сервера в виде сообщения 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 }
       {}
    };

    и мы должны что-то сделать для сохранения этой информации в очереди ждущих запросов. Например, можно создать новый экземпляр resize_request_t и переместить в него значения из полученного сообщения.

    А можно вспомнить о том, что само сообщение в SObjectizer — это динамически созданный объект. И не простой объект, а со счетчиком ссылок внутри. И что в SObjectizer-е есть специальный тип умного указателя для таких объектов — intrusive_ptr_t.

    Т.е. мы можем не делать копию resize_request_t для очереди ждущих запросов, а можем просто поместить в эту очередь умный указатель на уже существующий экземпляр resize_request_t. Что мы и делаем. А для того, чтобы не писать везде довольно экзотическое имя so_5::intrusive_ptr_t, мы вводим свой псевдоним:

    template<typename T>
    using sobj_shptr_t = so_5::intrusive_ptr_t<T>;
    

    Асинхронные ответы клиентам


    Мы говорили, что HTTP-запросы обрабатываются асинхронно. И показали выше, как HTTP-сервер асинхронным сообщением отсылает запрос агенту transform_manager. Но что происходит с ответами на HTTP-запросы?

    Ответы также обслуживаются асинхронно. Например, в коде transform_manager можно увидеть следующее:

    void
    a_transform_manager_t::on_failed_resize(
       failed_resize_t & /*result*/,
       sobj_shptr_t<resize_request_t> cmd )
    {
       do_404_response( std::move(cmd->m_http_req) );
    }

    Этот код генерирует отрицательный ответ на HTTP-запрос в случае, когда отмасштабировать картинку по какой-то причине не удалось. Генерация ответа происходит во вспомогательной функции do_404_response, код которой можно представить следующим образом:

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

    Первый ключевой момент с do_404_response() — это то, что данная функция вызывается на рабочем контексте агента transform_manager, а вовсе не на рабочем контексте HTTP-сервера.

    Второй ключевой момент — это вызов метода done() у полностью сформированного объекта resp. Вся асинхронная магия с HTTP-ответом происходит именно здесь. Метод done() берет всю подготовленную в resp информацию и асинхронно отсылает ее HTTP-серверу. Т.е. возврат из do_404_response() произойдет сразу после того, как содержимое объекта resp будет поставлено в очередь HTTP-сервера.

    HTTP-сервер на своем рабочем контексте обнаружит наличие нового HTTP-ответа и начнет выполнять необходимые действия по отсылке ответа соответствующему клиенту.

    Тип datasizable_blob_t


    Еще один небольшой момент, который имеет смысл пояснить, ибо он наверняка непонятен без понимания тонкостей работы RESTinio. Речь про наличие, на первый взгляд, странного типа datasizeable_blob_t, определенного следующим образом:

    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;
    
       //! Value for `Last-Modified` http header field.
       const std::time_t m_last_modified_at{ std::time( nullptr ) };
    };

    Для того, чтобы пояснить, зачем нужен этот тип, нужно показать, как формируется HTTP-ответ с трансформированной картинкой:

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

    Обращаем внимание на вызов set_body(): умный указатель на экземпляр datasizable_blob_t отправляется непосредственно туда. Зачем?

    Дело в том, что RESTinio поддерживает несколько вариантов формирования тела HTTP-ответа. Самый простой — это передать в set_body() экземпляр типа std::string и RESTinio сохранит значение этого string-а внутри объекта resp.

    Но бывают случаи, когда значение для set_body() должно переиспользоваться сразу в нескольких ответах. Например, в shrimp-е это происходит когда shrimp получает несколько одинаковых запросов на трансформацию одной и той же картинки. В этом случае невыгодно копировать одно и то же значение в каждый ответ. Поэтому в RESTinio есть вариант set_body() вида:
    template<typename T> auto set_body(std::shared_ptr<T> body);

    Но в этом случае на тип T накладывается важное ограничение: в нем должны быть публичные методы data() и size(), которые нужны, чтобы RESTinio мог получить доступ к содержимому ответа.

    Отмасштабированная картинка в shrimp-е хранится в виде объекта Magick::Blob. В типе Magic::Blob есть метод data, но нет метода size(), зато есть метод length(). Поэтому нам и потребовался класс-обертка datasizable_blob_t, который предоставляет RESTinio нужный интерфейс для доступа к значению Magick::Blob.

    Периодические сообщения в transform_manager


    Агенту transform_manager время от времени нужно выполнять несколько действий:

    • выталкивать из кэша картинки, которые находятся в кэше слишком долго;
    • контролировать время нахождения запросов в очереди ожидания свободных transformer-ов.

    Агент transform_manager выполняет эти действия посредством периодических сообщений. Выглядит это следующим образом.

    Сперва определяются типы сигналов, которые будут использоваться в качестве периодических сообщений:

    struct clear_cache_t final : public so_5::signal_t {};
    struct check_pending_requests_t final : public so_5::signal_t {};

    Затем выполняется подписка агента, в том числе и на эти сигналы:

    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> ) {...}

    Благодаря подписке SObjectizer будет вызывать нужный обработчик при получении агентом соответствующего сигнала.

    И остается только запустить периодические сообщения при старте агента:

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

    Ключевой момент здесь — это сохранение timer_id, которые возвращаются функциями send_periodic(). Ведь периодический сигнал будет приходить лишь до тех пор, пока жив его timer_id. Поэтому, если возвращаемое send_periodic() значение не сохранить, то отсылка периодического сообщения будет сразу же отменена. Поэтому в классе a_transform_manager_t есть такие атрибуты:

    so_5::timer_id_t m_clear_cache_timer;
    so_5::timer_id_t m_check_pending_timer;

    Конец первой части


    Сегодня мы познакомили читателя с самой простой и минималистичной реализацией shrimp-а. Этой реализации достаточно, чтобы показать, как RESTinio и SObjectizer можно совместно использовать для чего-то более-менее похожего на реальную задачу, а не на простой HelloWorld. Но в ней есть ряд серьезных недостатков.

    Например, в агенте transform_manager есть некая проверка уникальности запроса. Но она работает только в случае, если трансформированное изображение уже есть в кэше. Если же изображения в кэше еще нет и одновременно приходят два одинаковых запроса для одной и той же картинки, то оба эти запроса будут отданы на обработку. Что не есть хорошо. Правильно было бы обработать только один из них, а второй отложить до тех пор, пока обработка первого не завершится.

    Такой более расширенный контроль за уникальностью запросов привел бы к гораздо более сложному и объемному коду transform_manager. Поэтому мы не стали его реализовывать сразу, а решили пойти эволюционным путем — от простого к сложному.

    Так же простейшая версия shrimp-а представляет из себя «черный ящик», который не показывает никаких следов своей работы. Что не очень удобно как при тестировании, так и при эксплуатации. Поэтому, по хорошему, в shrimp следует добавить еще и логирование.

    Эти и некоторые другие недостатки самой первой версии shrimp-а мы попробуем устранить в последующих версиях и описать в следующих статьях. Так что stay tuned.

    Если у кого-то возникнут вопросы по логике работы shrimp-а, RESTinio или SObjectizer-а, то мы с удовольствием ответим в комментариях. Кроме того, сам по себе shrimp — это демо проект, но если кто-то заинтересовался его функциональностью и хотел бы видеть в shrimp-е что-то еще, помимо операции resize, то дайте нам знать, мы с удовольствием прислушаемся к любым конструктивным идеям.

    Продолжение...
    Поделиться публикацией
    Комментарии 13
      +1
      хотелось бы посмотреть на результаты load и stress тестов. Сам недавно разработал такую же штуку, только на питоне фаршированным AVX.
        0
        А что хочется увидеть по результатам таких тестов? Производительность ImageMagic-а?
          0
          производительность солянки в целом
            0
            Скорее всего, через одну итерацию мы докер-образ для shrimp-а опубликуем, можно будет тогда самостоятельно погонять на чем угодно.
        0
        А есть какая-то статистика использования SObjectize и RESTinio в реальных сторонних проектах (кроме проектов Вашей команды)? На мой субъективный взгляд минусом SObjectize является синтаксис, от лямбд и специализаций шаблонов гружится голова…
          0
          Публичной статистики нет. Мы знаем нескольких людей, которые используют SObjectizer и RESTinio в своих разработках и задают нам вопросы, присылают предложения-замечания. Но публично доступных success stories нет пока.
          На мой субъективный взгляд минусом SObjectize является синтаксис, от лямбд и специализаций шаблонов гружится голова…
          Это, действительно, субъективно. Кроме того, интересно было бы посмотреть на варианты синтаксиса без лямбд и шаблонов. Не факт, что было бы проще. И, определенно, были бы люди, которым бы и такой синтаксис не понравился бы.
          +1
          Мы у себя в проекте пытаемся использовать SObjectizer. По началу действительно «не привычно», но за день-два написания кода привыкаешь и даже начинает нравиться )
            0
            планируете ли поддержку webp?
              +1
              Если это кому-нибудь будет интересно и полезно, то почему бы и нет?
                0
                на прошлой работе активно использовался webp (онлайн хранилище для доков + вьювер на базе хрома). На текущем проекте (онлайн маркетплейс, типа авито) как раз думаем как оптимизировать моб трафик и будем тестировать webp. В андроиде нет проблем с этим, в ios в бетах 10 были решения из коробки, но их убрали. Но есть неплохие стороние либы.
                  0
                  Если вы не против, давайте попробуем обговорить сценарий, который вам интересен. Вы из мобильного приложения по HTTP обращаетесь к серверу, в запросе сообщаете ID картинки + желаемый размер и хотите, чтобы в ответе пришла эта картинка в формате webp. Правильно?

                  Только в формате webp или хотелось бы иметь возможность выбора формата через поля в заголовках или в query-string?

                  ЗЫ. Есть еще несколько интересных моментов, можно их здесь обсудить, можно в личку перейти.
                    +1

                    1 — да
                    2 —


                    /<image>.<ext>
                    /<image>.<ext>?op=resize&<side>=<value>

                    ext — это одно из поддерживаемых shrimp-ом расширений (jpg, jpeg, png или gif);

                    вот бы в ext добавить webp и был бы шик.
                    3 — написал

                      0
                      1 — да
                      OK.
                      вот бы в ext добавить webp и был бы шик.
                      Понятно, спасибо!

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое