понедельник, 20 июля 2015 г.

[prog.c++11] Прогресс в разработке SObjectizer-5.6.0. Часть 2: изъятие самых старых сообщений

Продолжение рассказа о том, как движется разработка версии 5.6.0. Данная часть посвящена такому аспекту механизма overload control, как автоматическое удаление самых старых сообщений.

Добавленные в версии 5.5.4 лимиты сообщений являются готовым к использованию механизмом overload control. Лимиты сообщений позволяют предохранять агента от перегрузки за счет специальной реакции на "лишние" самые новые сообщения. Когда появляется некое сообщение и выясняется, что именно оно превышает лимит для агента-получателя, то оно либо выбрасывается, либо преобразуется во что-то, либо пересылается кому-то.

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

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

Действительно, с изъятием сообщения определенного типа откуда-то из начала очереди сообщений, связано несколько проблем. Тут и хитрые структуры данных, которые требуются для выполнения таких трюков. И неизбежные потери производительности на их поддержание. И завязка поведения агентов и диспетчеров на то, что все должны знать про способы организации очередей сообщений. И подвисающие в воздухе вопросы реализации совсем других типов диспетчеров (например, диспетчера для Qt или Win32). И даже такие философские вопросы, как методы переупорядочивания остающихся в очереди сообщений при изъятии "лишнего": например, если очередь имеет вид (x1, y, z, x2, x3), то при добавлении x4 должна ли очередь принять вид (y, z, x2, x3, x4) или же (x2, y, z, x3, x4)?

В общем, длительное выкуривание бамбука в попытках найти решение этой проблемы, очень похоже, привело к тому, что удаление самых старых сообщений из очереди не будет частью механизма message limits. У лимитов так и останется всего четыре типа реакции на переполнение: игнорирование, переадресация, трансформация и вызов std::abort().

Тем не менее, удаление самых старых сообщений как часть встроенных средств обеспечения overload control станет доступным. Но не за счет message limits, а за счет ограничений на размеры очередей сообщений. Пояснить принцип попробую на примере описания нового диспетчера под условным названием adv_one_thread.

Давно существующий диспетчер one_thread обеспечивает т.н. тотальный fifo для всех привязанных к нему агентов. Т.е., если к one_thread-диспетчеру привязаны агенты A, B и C и затем некий агент X последовательно отсылает сообщение x1 агенту A, сообщение x2 агенту B и сообщение x3 агенту C, после чего сообщение x4 вновь агенту A, то one_thread обеспечит следующую последовательность вызова обработчиков: A(x1), B(x2), C(x3) и A(x4).

Однако, если бы агенты A, B и C были привязаны к thread_pool/adv_thread_pool диспетчерам, то при той же самой последовательности отсылки сообщений x(i), последовательность вызова обработчиков может быть совсем другой (даже если A, B и C будут запускаться на той же самой рабочей нити). Например, вот такой: A(x1), A(x4), B(x2) и C(x3).

Происходит это потому, что thread_pool/adv_thread_pool имеют не одну очередь заявок, а список очередей. И обрабатывают очереди из этого списка последовательно, обычно не более N сообщений из конкретной очереди. Т.о., в режиме individual_fifo, есть очередь агента A с содержимым (x1, x4), есть очередь агента B с содержимым (x2) и очередь агента C (x3). Сначала будет обработана очередь агента A, потом агента B, потом агента C. Отсюда и такая последовательность вызова обработчиков.

Новый диспетчер adv_one_thread будет предоставлять такую же логику: вместо одной общей очереди заявок, как в one_thread-диспетчере, будет список очередей заявок. Так, если агенты A, B и C привязываются к adv_one_thread в режиме individual_fifo, то у каждого из них будет своя собственная очередь.

Такая логика работы adv_one_thread ведет к двум интересным последствиям.


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

Т.е. при привязке агента A к диспетчеру adv_one_thread должно быть указано несколько параметров:

  • тип fifo: individual_fifo или cooperation_fifo (как и в случаях с thread_pool/adv_thread_pool-диспетчерах);
  • тип очереди: фиксированного размера или без ограничений на размер;
  • тип реакции на переполнение очереди фиксированного размера: выбрасывание самого старого сообщения, выбрасывание самого нового, вызов abort(). Может быть что-то еще.

Если агент A привязывается к adv_one_thread в режиме individual_fifo с очередью ограниченного размера и выбрасыванием самого старого сообщения, то при переполнении очереди будут выбрасываться самые старые из адресованных агенту A сообщений. Если же режим привязки cooperation_fifo, но так же с очередью ограниченного размера и выбрасыванием самого старого сообщения, то при переполнении очереди будут выбрасываться самые старые из адресованных любому агенту из кооперации агента А сообщения.

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


Во-вторых, можно управлять типом самой очереди. Т.е. если у очереди есть ограничение на размер, то можно использовать как преаллоцированную очередь фиксированного размера (работающую по принципу кольцевого буфера). А можно и динамически-растующую структуру, вроде std::list или std::deque.

Преаллоцированные очереди сообщений фиксированного размера -- это тоже одна из давних хотелок для SO-5, для которых пока не находилось способа воплощения в жизнь. А вот здесь появляется вполне реальный шанс.


В принципе, если эксперименты с adv_one_thread докажут состоятельность данной идеи, подобные модификации можно сделать и для других диспетчеров. Так, режимы fifo уже задаются при привязке агентов к thread_pool/adv_thread_pool диспетчерам. Добавить сюда еще и тип очереди (фиксированный размер или нет) + тип реакции на переполнение и вуаля! :)

Что до старых диспетчеров вроде active_obj/active_group, то с появлением приватных диспетчеров их актуальность сильно снизилась. Посему впору задуматься, а нет ли смысла объявить данные диспетчеры deprecated :)


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

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