Хабрахабр

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 пикселей по длинной стороне.

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

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

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

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

А назвать свой демо-проект мы решили 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 изменяет размер запрошенной картинки и отдает отмасштабированную версию.

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

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

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

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

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

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

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

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

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

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

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

Агент 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.

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

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

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().

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

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

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

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

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

Наверное, Boost. Искать что-то готовое для этих целей в Интернетах мы не стали. Но тащить Boost только ради MultiIndex-а не хотелось, поэтому сделали свою тривиальную реализацию буквально на коленке. 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-сервера.

Вся асинхронная магия с HTTP-ответом происходит именно здесь. Второй ключевой момент — это вызов метода done() у полностью сформированного объекта resp. Т.е. Метод 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 отправляется непосредственно туда. Зачем?

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

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

template<typename T> auto set_body(std::shared_ptr<T> body);

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

В типе Magic::Blob есть метод data, но нет метода size(), зато есть метод length(). Отмасштабированная картинка в shrimp-е хранится в виде объекта Magick::Blob. Поэтому нам и потребовался класс-обертка 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 следует добавить еще и логирование.

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть