понедельник, 10 апреля 2017 г.

[prog.thoughts] Очередная итерация вокруг movable/modifiable сообщений. Нужно сделать непростой выбор из двух(?) вариантов...

Тема movable/modifiable сообщений для SObjectizer-а, поднятая некоторое время назад, и, казалось бы, начавшая уже самостоятельно дышать, по мере своего развития стала получать серьезные удары. Один из них был обозначен вчера. Сегодня случился еще один. Посему есть желание еще раз подумать о том, а не следует ли что-нибудь подправить в консерватории?

Итак, был попробован подход, в котором создавались пары похожих, но несовместимых между собой сущностей: mbox_t и unique_mbox_t, mchain_t и unique_mchain_t.

Основное достоинство этого подхода в том, что в compile-time контролируется невозможность отсылки shared-сообщения в unique_mbox/mchain. Это очень сильное преимущество в сравнении с другими вариантами, поэтому именно с этого подхода и началась попытка реализации movable/modifiable сообщений в SO-5.5.19. Однако, чем дальше, тем больше шансов, что это достоинство будет нивелировано необходимостью иметь дело с разными типами mbox-ов и mchain-ов. Т.е. там, где раньше разработчик просто использовал mbox_t, сейчас ему придется заботиться и о mbox_t, и о unique_mbox_t. Например, если раньше могло быть что-то вроде:

template<typename MSG, typename... ARGS>
void make_and_send_to(
   const mbox_t & dest,
   const some_common_arg_type & common_arg,
   ARGS && ...args)
{
   so_5::send<MSG>(to, common_arg, std::forward<ARGS>(args)...);
}
...
if(has_free_workers)
   make_and_send_to<handle_request>(processor, request);
else if(can_be_delayed(request))
   make_and_send_to<delay_request>(wait_queue, request, delaying_options);
else
   make_and_send_to<reject_request>(rejector, request, no_free_workers, unable_to_delay, default_error_code);

То сейчас dest придется передавать шаблонным параметром. Т.е. делать что-то вроде:

template<typename MSG, typename MBOX_HANDLE, typename... ARGS>
void make_and_send_to(
   const MBOX_HANDLE & dest,
   const some_common_arg_type & common_arg,
   ARGS && ...args)
{
   so_5::send<MSG>(to, common_arg, std::forward<ARGS>(args)...);
}
...

К сожалению, в таком виде в качестве первого параметра для make_and_send_to можно будет передать вообще все, что угодно. Даже int. И получить затем портянку ошибок с отсутствием должного варианта so_5::send().

Но если в случае с mbox_t/unique_mbox_t деление на shared- и unique-mbox еще оправдано наличием как взаимодействия 1:N, так и 1:1, то вот для mchain-ов разделение на обычный (т.е. shared-) и unique-mchain уже достаточно сложно объяснить. Поскольку главная причина такого деления возникает из-за необходимости сохранять совместимость с предшествующими версиями SO-5, а отнюдь не из-за требований какой-то общепризнанной теоретической модели.

Поэтому я все больше и больше склоняюсь к тому, чтобы текущее направление развития movable/modifiable сообщений признать тупиковым. И сделать вариант, который основан на маркере unique_msg при отсылке и при получении сообщения.

Так, для того, чтобы отослать unique-сообщение единственному получателю потребуется сделать вызов so_5::send<so_5::unique_msg<M>>(dest, args...). А для получения сообщения нужно будет использовать so_5::mhood_t<so_5::unique_msg<M>>. Для обычных агентов это может выглядеть, например, так:

class demo final : public so_5::agent_t {
public :
   demo(context_t ctx) : so_5::agent_t(std::move(ctx)) {
      so_subscribe_self()
         // Подписываем агента на иммутабельное сообщение типа K.
         .event([this](const K & cmd) {...})
         // Подписываем агента на иммутабельное сообщение типа L.
         .event([this](mhood_t<L> cmd) {...})
         // Подписываем агента на мутабельное сообщение типа M.
         .event([this](mhood_t<unique_msg<M>> cmd) {...});
   }

   virtual void so_evt_start() override {
      // При старте отсылаем самому себе сообщения K, L и M.
      // Сообщения K и L идут как иммутабельные.
      so_5::send<K>(*this, ...);
      so_5::send<L>(*this, ...);
      // Сообщение M идет как мутабельное.
      so_5::send<unique_msg<M>>(*this, ...);
   }
};

При этом можно обратить внимание на то, что и иммутабельные, и мутабельное сообщение отсылается в один и тот же direct-mbox агента-получателя. В первом подходе, который уже частично реализован, это невозможно. Там нужно для агента создавать отдельный unique_mbox, подписывать обработчик события на этот unique_mbox и сообщение отсылать не в обычный direct_mbox, а в отдельный unique_mbox.

Для mchain-ов может выглядеть так:

auto ch = create_mchain(env);

// Отсылаем три сообщения.
// Сообщения K и L идут как иммутабельные.
so_5::send<K>(ch, ...);
so_5::send<L>(ch, ...);
// Сообщение M идет как мутабельное.
so_5::send<so_5::unique_msg<M>>(ch, ...);

// Обрабатываем все сообщения из канала.
receive(from(ch),
   // Обработчик для иммутабельного K.
   [](const K & cmd) {...},
   // Обработчик для иммутабельного L.
   [](so_5::mhood_t<L> cmd) {...},
   // Обработчик для мутабельного M.
   [](so_5::mhood_t<so_5::unique_msg<M>> cmd) {...});

В общем, как мне думается, все достаточно наглядно и видно, где отсылается unique-сообщение и где ожидается именно unique-сообщение.

Однако, вот что в таком подходе не нравится:

  • нет проверок в compile-time. Т.е. если пользователь сделает send<unique_msg<M>> в multi-producer/multi-consumer mbox, то получит исключение в run-time, а не ошибку во время компиляции.
  • в один и тот же mbox можно отослать и иммутабельное, и мутабельное сообщение типа M, и эти экземпляры попадут в разные обработчики:

    so_5::send<M>(ch, ...);
    so_5::send<so_5::unique_msg<M>>(ch, ...);
    receive(from(ch),
       [](so_5::mhood_t<M>) { std::cout << "immutable M" << std::endl; },
       [](so_5::mhood_t<so_5::unique_msg<M>>) { std::cout << "mutable M" << std::endl; });

    И такая возможность лично мне представляется нормальной. Но вот что может приводить к путанице и ошибкам, так это вот такие вещи:

    // Отсылаем как иммутабельное сообщение.
    so_5::send<M>(ch, ...);
    ...
    receive(from(ch),
       // А получить пытаемся как мутабельное.
       [](so_5::mhood_t<so_5::unique_msg<M>>) {...});

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

    А с другой стороны, если сообщение отсылается как мутабельное, то разрешать ли обрабатывать его как иммутабельное? Вопрос, однако...

Ранее я так же думал, что у этого подхода есть еще недостаток, связанный с тем, что различия между иммутабельными и мутабельными сообщениями могут мешать писать обобщенный код, вроде показанной выше шаблонной функции make_and_send_to. Однако, затем подумалось, что если ввести шаблон immutable_msg<M> то с его помощью можно сделать вызовы send<M> и send<immutable_msg<M>> синонимами. Что позволит писать обобщенный код вот в таком виде:

template<typename MSG, typename... ARGS>
void make_and_send_to(
   const mbox_t & dest,
   const some_common_arg_type & common_arg,
   ARGS && ...args)
{
   so_5::send<MSG>(to, common_arg, std::forward<ARGS>(args)...);
}
...
if(has_free_workers)
   make_and_send_to<unique_msg<handle_request>>(processor, request);
else if(can_be_delayed(request))
   make_and_send_to<unique_msg<delay_request>>(wait_queue, request, delaying_options);
else
   make_and_send_to<immutable_msg<reject_request>>(rejector, request, no_free_workers, unable_to_delay, default_error_code);

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

Итак, сейчас стоит дилемма: продолжать ли непростой путь в сторону mbox/unique_mbox+mchain/unique_mchain или же нужно начать все сначала и двинуться в сторону send<unique_msg<M>>? Если у кого-то есть соображения, то прошу высказываться. Реальная возможность повлиять на вектор развития SObjectizer-а :)

PS. По ходу написания текста поста сам себя поймал на том, что лучше оперировать понятиями immutable_msg и mutable_msg, нежели понятиями shared- и unique-сообщения. Так что если второй подход получит право на жизнь, то будет использоваться нотация send<mutable_msg<M>>.

UPD. Еще один большой минус наличия mbox/unique_mbox и mchain/unique_mchain, это усложнение жизни тех пользователей, которые делают собственные реализации mbox-ов и mchain-ов. Например, собственная реализация mbox-а может понадобиться для балансировки нагрузки: пользователь может написать свой mbox, который будет адресовать очередное сообщение тому агенту, который меньше загружен. Соответственно, при появлении деления на mbox/unique_mbox такие трюки будет проделывать сложнее.

Комментариев нет: