Модель акторов — это хороший подход к решению некоторых типов задач. Готовый акторный фреймворк, особенно в случае языка C++, может очень сильно облегчить жизнь разработчика. С программиста снимается изрядная часть забот по управлению рабочими контекстами, организации очередей сообщений, контролю за временем жизни сообщений и т.д. Но, как говорится, все хорошее в этой жизни либо противозаконно, либо аморально, либо ведет к ожирению ничего не дается бесплатно. Одна из проблем использования готового (т.е. чужого) акторного фреймворка состоит в том, что иногда он превращается в «черный ящик». Ты видишь, что ты отдаешь в этот «черный ящик», ты видишь, что из него приходит (если вообще приходит). Но далеко не всегда понятно, как из первого получается второе…
О чем речь?
Одна из самых распространенных проблем, с которой сталкиваются разработчики при использовании фреймворка SObjectizer — это неполучение отосланных сообщений. Т.е. сообщение отослано, но до получателя не дошло. Почему? А вот это уже интересный вопрос.
Есть несколько основных причин, по которым отосланное сообщение до получателя не доходит:
1. Получателя просто не существует. Уже не существует или еще не существует не суть важно. Получатель мог быть когда мы вызываем send, но он может прекратить свое существование еще до того, как send завершит свою работу. Либо, вызывая send, мы думаем, что получатель уже существует, но на самом деле его еще не успели создать.
2. Получатель не подписался на сообщение. Вот тупо забыли сделать подписку на конкретное сообщение и все. Когда сообщение отсылается, то оно даже не доставляется нужному нам агенту, т.к. он на сообщение просто не подписан. Или, как вариант, по ошибке подписали получателя на сообщение из другого почтового ящика.
3. Получатель не подписался на сообщение в нужном состоянии. Например, у агента-получается есть три состояния, в которых он хочет обрабатывать сообщение. Но подписку сделали только для двух состояний, а про третье забыли. Если в момент прихода сообщения агент-получатель находится в этом третьем состоянии, то сообщение до получателя доставлено не будет.
Есть и другие причины, но эти встречаются чаще всего.
По нашим наблюдениям, причины №2 и №3 наиболее распространенные. И на эти грабли наступают все — и новички, и опытные пользователи SObjectizer-а. Даже мы сами, разработчики SObjectizer-а, регулярно совершаем эти глупые ошибки. Как это происходит? Да очень просто.
Простой пример с неправильной подпиской
Вот код простейшего примера, в котором агенты pinger и ponger обмениваются сигналами ping и pong через общий multi-producer/multi-consumer почтовый ящик:
#include <so_5/all.hpp>
// Сигналы, которыми будут обмениваться pinger и ponger.
struct ping final : public so_5::signal_t {};
struct pong final : public so_5::signal_t {};
// Агент pinger, первым отсылает ping, затем отсылает ping в ответ на pong.
class pinger final : public so_5::agent_t {
// Почтовый ящик, через который будет происходить обмен сообщениями.
const so_5::mbox_t mbox_;
public:
// Конструктор получает почтовый ящик в качестве аргумента.
pinger(context_t ctx, so_5::mbox_t mbox)
: so_5::agent_t{std::move(ctx)}
, mbox_{std::move(mbox)} {
// Сразу подписываем агента на нужный сигнал.
so_subscribe(mbox_).event([this](mhood_t<pong>) {
std::cout << "pong!" << std::endl;
so_5::send<ping>(mbox_);
});
}
// В самом начале своей работы отсылаем первый сигнал ping.
virtual void so_evt_start() override {
so_5::send<ping>(mbox_);
}
};
// Агент ponger, только отвечает pong-ами в ответ на ping-и.
class ponger final : public so_5::agent_t {
// Почтовый ящик, через который будет происходить обмен сообщениями.
const so_5::mbox_t mbox_;
public:
// Конструктор получает почтовый ящик в качестве аргумента.
ponger(context_t ctx, so_5::mbox_t mbox)
: so_5::agent_t{std::move(ctx)}
, mbox_{std::move(mbox)} {
// Сразу подписываем агента на нужный сигнал.
so_subscribe_self().event([this](mhood_t<ping>) {
std::cout << "ping!" << std::endl;
so_5::send<pong>(mbox_);
});
}
};
int main() {
// Запускаем SObjectizer с двумя агентами в рамках одной кооперации.
so_5::launch([](so_5::environment_t & env) {
env.introduce_coop([](so_5::coop_t & coop) {
// Почтовый ящик, который потребуется агентам.
const auto mbox = coop.environment().create_mbox();
// Сами агенты.
coop.make_agent<pinger>(mbox);
coop.make_agent<ponger>(mbox);
});
});
}
В этом простом коде есть маленькая ошибка, из-за которой при запуске примера мы не увидим печати сообщений ping/pong. Возможно, кто-то уже понял в чем дело. Но, вообще-то говоря, такие досадные ошибки обнаруживать не так-то и просто. Особенно когда имеешь дело не с примерами, а с реальными агентами, логика и реализация которых бывает значительно сложнее.
Эффект «черного ящика» и что же с этим делать?
В показанном выше примере мы столкнулись с тем, что SObjectizer для нас стал «черным ящиком». Мы какие-то управляющие воздействия ему выдаем, но нужного нам результата не случается. Но почему? В чем причина? Как докопаться до этой причины?
И вот тут оказывается, что докопаться до причины не так-то и просто. Нужно как-то отлаживать наш небольшой пример, но как это сделать?
Если мы пользуемся отладчиками, то можно поставить точки прерываний в нескольких местах. Например:
1. В методе pinger::so_evt_start() для того, чтобы убедиться, что первый send для ping-а вызывается.
2. В обработчике сигнала ping в агенте ponger-е для того, чтобы убедиться, что первый ping приходит и pong в ответ отсылается.
Либо, если мы настоящие программисты и считаем, что отладчики — это для слабаков, то мы можем расставить отладочные печати в этих же местах.
И вот проделав эти действия мы обнаружим, что so_evt_start() вызывается, первый send для ping-а отрабатывает, а вот обработчик для pong-а не вызывается. Но почему? Мы же сделали подписку в конструкторе…
Тут как раз перед разработчиком, который использует SObjectizer, и появляется вопрос: «А как же заглянуть внутрь SObjectizer-а, чтобы понять, как именно происходит доставка сообщения?»
Вопрос не самый простой. Поскольку, если программист решит протрассировать вызов send-а, то он, во-первых, попадет в дебри деталей реализации SObjectizer-а и вряд ли получит от этого удовольствие (хотя некоторые заглядывали и говорили, что ничего страшного не увидели, но можно ли им верить?). Во-вторых, и это самое главное, он поймет, что доставка сообщения состоит из двух частей. Первая часть — это постановка сообщения в очереди заявок для подписчиков. Вторая часть — это обслуживание конкретной заявки, которая извлекается из очереди конкретным диспетчером.
И хорошо, если проблема обнаружится в первой части. Например, разработчик увидит, что для сообщения нет подписчиков и поэтому для сообщения никаких заявок не генерируется. А вот если заявки сгенерировались, но затем не произошел вызов обработчика, то придется тогда программисту погружаться еще глубже в потроха SObjectizer-а. И там-то он может обнаружить, что в SObjectizer-е есть разные диспетчеры, некоторые очень специфические, работающие не так, как другие, и найти место, где из очереди извлекается и анализируется очередная заявка, не так-то и просто.
В общем, если кто-то захочет отладчиком пройтись по механизму доставки сообщений в SObjectizer-е, то этому человеку можно будет только пожелать удачи. Поскольку мы сами не большие любители такого времяпрепровождения.
Ну а есть ли какая-нибудь альтернатива?
Есть. Это механизм, который официально называется message delivery tracing (или msg_tracing для простоты). Он был добавлен в SObjectizer-5 два с половиной года назад. Но, вероятно, мало кто о нем наслышан, хотя мы сами пользуемся им постоянно.
Механизм msg_tracing
При запуске SObjectizer-а можно включить механизм msg_tracing. Если он включен, то SObjectizer будет формировать текстовые сообщения, описывающие процесс доставки и обработки сообщений. Эти сообщения будут передаваться специальному объекту-tracer-у. Задачей tracer-а является сохранение или какая-либо другая обработка trace-сообщений. Пользователь может либо сам реализовать собственный tracer, либо же может воспользоваться одним из готовых tracer-ов из SObjectizer-а.
Давайте расширим показанный выше пример включением msg_tracing-а и посмотрим, что у нас получится. Итак, включаем msg_tracing и указываем, что все trace-сообщения должны отображаться на стандартный поток вывода:
int main() {
// Запускаем SObjectizer с двумя агентами в рамках одной кооперации.
so_5::launch([](so_5::environment_t & env) {
env.introduce_coop([](so_5::coop_t & coop) {
// Почтовый ящик, который потребуется агентам.
const auto mbox = coop.environment().create_mbox();
// Сами агенты.
coop.make_agent<pinger>(mbox);
coop.make_agent<ponger>(mbox);
});
},
[](so_5::environment_params_t & params) {
// Разрешаем трассировку механизма доставки сообщений.
// Направляем трассировочные сообщения на стандартый поток вывода.
params.message_delivery_tracer(so_5::msg_tracing::std_cout_tracer());
});
}
Запускаем модифицированный пример и получаем что-то вроде:
[tid=13728][mbox_id=5] deliver_message.no_subscribers [msg_type=struct ping][signal][overlimit_deep=1]
Эта запись говорит о том, что происходит операция deliver_message. При этом обнаружена ситуация, когда для сигнала типа ping, который отсылается в почтовый ящик с таким-то идентификатором, нет подписчиков.
Итак, мы видим, что сообщение отсылается в почтовый ящик, но на него него никто не подписан. Но как так, мы же сделали подписку, вот же она:
// Сразу подписываем агента на нужный сигнал.
so_subscribe_self().event([this](mhood_t<ping>) {
std::cout << "ping!" << std::endl;
so_5::send<pong>(mbox_);
});
И вот тут мы уже можем увидеть, что подписку-то мы сделали, но совсем не на тот mbox. Нам нужно было вызвать so_subscribe(mbox_), а мы вызвали so_subscribe_self(). Досадная ошибка, которая совершается с завидной регулярностью. Механизм msg_tracing позволил нам быстро разобраться с проблемой, не погружаясь с отладчиком в потроха SObjectizer-а.
msg_tracing — это отладочный механизм
Самая важная вещь, про которую нужно помнить, — это то, что msg_tracing является отладочным, вспомогательным механизмом. Он создан для того, чтобы помогать разработчику с отладкой разрабатываемых на базе SObjectizer-а приложений.
В частности, с msg_tracing-ом связаны дополнительные накладные расходы, серьезно удорожающие отсылку и получение сообщений. Именно поэтому msg_tracing нужно включать явно, делается это всего один раз и только перед стартом SObjectizer-а. Нельзя отключить msg_tracing после того, как SObjectizer был запущен со включенным msg_tracing-ом. Все настолько серьезно, что при включенном msg_tracing-е внутри SObjectizer-а используются другие структуры данных, содержащие в себе и дополнительные данные, и дополнительный код. Так, внутри SObjectizer-а при включении msg_tracing-а создаются другие типы mbox-ов.
Поэтому msg_tracing следует рассматривать как вспомогательный инструмент для отладки SObjectizer-приложений. И не стоит строить на его базе какие-то средства, которые должны будут работать в продакшене.
Текущее состояние механизма msg_tracing: его недостатки и возможные решения
Механизм msg_tracing был добавлен в версию 5.5.9 в октябре 2015-го года. Уже тогда было понятно, что сам этот механизм находится в зачаточном состоянии. Нужно было с чего-то начинать, нужно было получить что-то работающее, что можно было бы попробовать на практике, накопить опыт и понять, куда двигаться дальше.
За прошедшее время в существующем механизме msg_tracing было выявлено два серьезных просчета/недостатка.
Нет фильтрации trace-сообщений
Если при старте SObjectizer-а был включен msg_tracing, то SObjectizer создавал текстовые trace-сообщения для всех связанных с доставкой сообщений операций и передавал эти сообщения объекту-tracer-у. Это не представляет проблемы, когда msg_tracing используется для отладки небольшого приложения, примера или теста. Но если попробовать использовать msg_tracing в большом приложении с сотнями агентов внутри, которые обмениваются миллионами сообщений, то это проблема. Т.к. нужно каким-то образом отфильтровывать ненужные программисту trace-сообщения, а делать это с текстовыми trace-сообщениями не так-то просто.
Соответственно, время от времени заходили разговоры о том, чтобы дать пользователю SObjectizer-а возможность фильтровать trace-сообщения перед тем, как они будут переданы в tracer. Причем фильтр должен иметь дело не с окончательно сформированным текстовым сообщением, а с теми данными, которые затем преобразуются в текст.
Нет возможности управлять потоком trace-сообщений
Иногда хотелось бы запустить SObjectizer с заблокированными trace-сообщениями, а затем, в какой-то момент времени, «открыть краник» и разрешить trace-сообщениям попадать в tracer. А потом «перекрыть краник».
Соответственно, так же время от времени возникали разговоры о том, чтобы msg_tracing можно было включать и выключать в процессе работы SObjectizer-а. Хотя сложность здесь была в том, что при своем старте SObjectizer уже должен знать, что разработчик хочет иметь msg_tracing. В этом случае SObjectizer будет создавать другие типы mbox-ов и будет использовать другие методы доставки сообщений (с трассировкой происходящего).
Грядущая модификация msg_tracing в версии 5.5.22
На днях мы зафиксировали первую альфу новой версии SObjectizer-а. В ней реализовано расширение механизма msg_tracing. Было добавлено понятие фильтра для trace-сообщений.
Фильтр опционален. Если фильтр назначен, то прежде чем сформировать текстовое trace-сообщение, SObjectizer сперва спрашивает у фильтра, нужно ли разрешать дальнейшую обработку текущего trace-сообщения. Если фильтр говорит, что «да, можно», то формируется текстовое trace-сообщение и оно отдается объекту-tracer-у. Если же фильтр отвечает «нет, нельзя», то trace-сообщение не формируется и никуда не уходит.
Если же фильтра нет, то SObjectizer работает как раньше: текстовое trace-сообщение формируется и отдается объекту-tracer-у. Т.е. если программист не знает про фильтры или не хочет использовать фильтры, то механизм msg_tracing у него работает как раньше.
Кроме того, SObjectizer в версии 5.5.22 позволяет задавать и удалять msg_tracing-фильтры в процессе своей работы.
Пример использования msg_tracing-фильтров в версии 5.5.22
Для демонстрации новых возможностей механизма msg_tracing возьмем следующий пример. Есть два агента, которые работают практически одинаково. У них есть три состояния: first, second и third. В каждом из своих состояний агенты должны реагировать на два типа сообщений. По сообщению change_state нужно перейти в следующее состояние (т.е. из first в second, из second в third, из third в first). Если же приходит сообщение tick, то нужно, чтобы агент просто на него среагировал. Какова будет реакция — не важно.
Разница между первым и вторым агентом будет заключаться в том, что первый агент реагирует на сообщение tick в каждом из своих состояний. А вот второй агент подписаться на сообщение tick в состоянии second забыл. Соответственно, второй агент будет терять сообщения tick когда он находится в состоянии second. Вот именно эти потери мы и хотим обнаруживать с помощью механизма msg_tracing-а.
Для этого нам потребуется установить trace-фильтр следующего вида:
// Включаем трассировку тех сообщений, для который не
// найден метод-обработчик у подписчика. Это говорит о том,
// что агент не подписан в своем текущем состоянии.
so_environment().change_message_delivery_tracer_filter(
so_5::msg_tracing::make_filter(
[](const so_5::msg_tracing::trace_data_t & td) {
// Пытаемся получить информацию об обработчике сообщения.
const auto handler_ptr = td.event_handler_data_ptr();
if(handler_ptr) {
// Эта информация в trace-сообщении есть.
// Теперь можно посмотреть, найден ли сам обработчик.
if(nullptr == *handler_ptr)
// Обработчик не найден. Это именно та ситуация,
// которую мы ждем. Trace-сообщение можно разрешить.
return true;
}
// Во всех остальных случаях блокируем trace-сообщение.
return false;
}));
Выглядит это, наверное, страшно. Но давайте попробуем разобраться.
Метод change_message_delivery_tracer_filter() позволяет заменить trace-фильтр в процессе работы SObjectizer-а.
Вспомогательная функция make_filter создает объект нужного SObjectizer-у типа из лямбда-функции.
В эту лямбда функцию передается единственный аргумент — ссылка на интерфейс trace_data_t, из которого фильтр может извлекать относящиеся к trace-сообщению данные. В данном конкретном случае мы вызываем у trace_data_t единственный метод event_handler_data_ptr(). Данный метод возвращает optional<const event_handler_data*>. Соответственно, внутри optional может либо быть указатель, либо нет. Если указателя нет, то значит trace-сообщение нам не интересно, т.к. оно не относится к поиску обработчика сообщения. Если же указатель есть, но он нулевой, то это именно тот случай, который мы ждем: SObjectizer попытался найти обработчик для сообщения, но ничего не нашел. В этом и только в этом случае мы разрешаем trace-сообщение.
Если мы запустим этот пример, то увидим, что на консоли периодически возникают такие сообщения:
[tid=11880][agent_ptr=0x23be91d7cb0] demand_handler_on_message.find_handler [mbox_id=6][msg_type=struct base::tick][signal][state=second][evt_handler=NONE] [tid=11880][agent_ptr=0x23be91d7cb0] demand_handler_on_message.find_handler [mbox_id=6][msg_type=struct base::tick][signal][state=second][evt_handler=NONE] [tid=11880][agent_ptr=0x23be91d7cb0] demand_handler_on_message.find_handler [mbox_id=6][msg_type=struct base::tick][signal][state=second][evt_handler=NONE] [tid=11880][agent_ptr=0x23be91d7cb0] demand_handler_on_message.find_handler [mbox_id=6][msg_type=struct base::tick][signal][state=second][evt_handler=NONE]
Причем они то печаются, то не печатаются. Это потому, что у нас есть агент trace_controller, который то «включает» поток trace-сообщений, то «отключает» его.
#include <so_5/all.hpp>
// Базовый класс для агентов примера.
class base : public so_5::agent_t {
protected:
// Периодический сигнал, который агент должен обрабатывать
// в каждом из своих состояний.
struct tick final : public so_5::signal_t {};
// Периодический сигнал для смены состояния агента.
struct change_state final : public so_5::signal_t {};
// Состояния, в которых может находится агент.
state_t st_first{this, "first"},
st_second{this, "second"},
st_third{this, "third"};
// Идентификаторы периодических сообщений. Мы должны их сохранить,
// чтобы периодические сообщения не перестали приходить.
so_5::timer_id_t tick_timer_;
so_5::timer_id_t change_state_timer_;
public:
base(context_t ctx) : so_5::agent_t{std::move(ctx)} {}
// Общая часть инициализации всех агентов.
virtual void so_define_agent() override {
// Меняем начальное состояние.
this >>= st_first;
// Подписываемся на change_state для перехода в следующее состояние.
st_first.event([this](mhood_t<change_state>) { this >>= st_second; });
st_second.event([this](mhood_t<change_state>) { this >>= st_third; });
st_third.event([this](mhood_t<change_state>) { this >>= st_first; });
}
// Начальные действия, одинаковые для всех агентов.
virtual void so_evt_start() override {
using namespace std::chrono_literals;
// Заставляем агента менять состояние каждые 250ms.
change_state_timer_ = so_5::send_periodic<change_state>(*this, 250ms, 250ms);
// Заставляем приходить сообщение tick.
tick_timer_ = so_5::send_periodic<tick>(*this, 0ms, 100ms);
}
};
// Первый агент, который обрабатывает сигналы tick во всех своих состояниях.
class first_agent final : public base {
public:
using base::base;
virtual void so_define_agent() override {
base::so_define_agent();
// Добавляем недостающие подписки.
so_subscribe_self()
.in(st_first).in(st_second).in(st_third)
.event([](mhood_t<tick>){});
}
};
// Второй агент, который не подписывается на tick в st_second.
class second_agent final : public base {
public:
using base::base;
virtual void so_define_agent() override {
base::so_define_agent();
// Добавляем недостающие подписки.
so_subscribe_self()
.in(st_first).in(st_third)
.event([](mhood_t<tick>){});
}
};
// Агент, который будет время от времени "включать" и "выключать" поток
// интересующих нас trace-сообщений.
class trace_controller final : public so_5::agent_t {
// Сигнал для включения/выключения.
struct on_off final : public so_5::signal_t {};
// Состояния, в которых может находится этот агент.
state_t st_on{this}, st_off{this};
// Таймер для сигнала on_off.
so_5::timer_id_t timer_;
public:
trace_controller(context_t ctx) : so_5::agent_t{std::move(ctx)} {
st_off.event([this](mhood_t<on_off>) {
// Меняем состояние.
this >>= st_on;
// Включаем трассировку тех сообщений, для который не
// найден метод-обработчик у подписчика. Это говорит о том,
// что агент не подписан в своем текущем состоянии.
so_environment().change_message_delivery_tracer_filter(
so_5::msg_tracing::make_filter(
[](const so_5::msg_tracing::trace_data_t & td) {
// Пытаемся получить информацию об обработчике сообщения.
const auto handler_ptr = td.event_handler_data_ptr();
if(handler_ptr) {
// Эта информация в trace-сообщении есть.
// Теперь можно посмотреть, найден ли сам обработчик.
if(nullptr == *handler_ptr)
// Обработчик не найден. Это именно та ситуация,
// которую мы ждем. Trace-сообщение можно разрешить.
return true;
}
// Во всех остальных случаях блокируем trace-сообщение.
return false;
}));
});
st_on.event([this](mhood_t<on_off>) {
// Меняем состояние.
this >>= st_off;
// Выключаем трассировку.
so_environment().change_message_delivery_tracer_filter(
// Этот фильтр будет запрещать вообще все trace-сообщения.
so_5::msg_tracing::make_disable_all_filter());
});
this >>= st_off;
}
virtual void so_evt_start() override {
using namespace std::chrono_literals;
timer_ = so_5::send_periodic<on_off>(*this, 0ms, 1500ms);
}
};
int main() {
// Запускаем SObjectizer с двумя агентами в рамках одной кооперации.
so_5::launch([](so_5::environment_t & env) {
env.introduce_coop([](so_5::coop_t & coop) {
// В пример будут входить по одному агенту типа
// first_agent и second_agent.
coop.make_agent<first_agent>();
coop.make_agent<second_agent>();
// А так же агент, который будет периодически включать и
// выключать поток trace-сообщений.
coop.make_agent<trace_controller>();
});
},
[](so_5::environment_params_t & params) {
// Разрешаем трассировку механизма доставки сообщений.
// Направляем трассировочные сообщения на стандартый поток вывода.
params.message_delivery_tracer(so_5::msg_tracing::std_cout_tracer());
// Блокируем все trace-сообщения до тех пор, пока это не потребуется.
params.message_delivery_tracer_filter(
so_5::msg_tracing::make_disable_all_filter());
});
}
Что сейчас доступно через trace_data_t?
На данный момент интерфейс trace_data_t определен следующим образом:
class trace_data_t
{
...
public :
// Получить ID рабочей нити, на которой выполняется действие.
virtual optional<current_thread_id_t>
tid() const noexcept = 0;
// Получить информацию о типе сообщения, к которому относится действие.
virtual optional<std::type_index>
msg_type() const noexcept = 0;
// Получить информацию о том, из какого источника поступило сообщение.
virtual optional<msg_source_t>
msg_source() const noexcept = 0;
// Указатель на агента-получателя, которому адресуется сообщение.
virtual optional<const agent_t *>
agent() const noexcept = 0;
// Является ли сообщение обычным сообщением или же сигналом.
virtual optional<message_or_signal_flag_t>
message_or_signal() const noexcept = 0;
// Получить информацию о самом экземпляре сообщения.
virtual optional<message_instance_info_t>
message_instance_info() const noexcept = 0;
// Получить текстовое описание выполняемого действия.
virtual optional<compound_action_description_t>
compound_action() const noexcept = 0;
// Получить указатель на описание обработчика сообщения у подписчика.
// Этот указатель будет нулевым, если в текущем состоянии нет
// подписки на пришедшее сообщение.
virtual optional<const so_5::impl::event_handler_data_t *>
event_handler_data_ptr() const noexcept = 0;
};
Нужно особенно подчеркнуть, что далеко не всегда для trace-сообщения будет доступна вся эта информация. Например, если сообщение передано почтовому ящику для распределения по подписчикам, то event_handler_data_ptr() будет возвращать пустой optional. Если у почтового ящика нет подписчиков на данный тип сообщения, то и метод agent() будет возвращать пустой optional.
Зачем все это было описано?
Данная статья была написана для того, чтобы дать возможность тем заинтересованным разработчикам заранее узнать о том, что их ждет в очередной версии SObjectizer-а. Соответственно, они могут ознакомиться с предлагаемыми нововведениям, могут пощупать их, составить свое впечатление и высказать свое «Фи», если что-то не понравилось (so-5.5.22-alpha1 доступна для загрузки, так же обновлено и зеркало на github-е).
Ну а мы, соответственно, получим возможность понять, что и как следует улучшить еще до фиксации окончательной версии 5.5.22. Для нас это важно, поскольку у нас есть заскок на тему сохранения совместимости между версиями и мы очень не любим ломать эту самую совместимость.
Так что если кому-то интересна эта тема, то пожалуйста, высказывайтесь в комментариях. Постараемся прислушиваться к конструктивным соображениям.
PS. Примеры, использованные в данной статье, можно найти в этом репозитории.
PPS. У нас так же были планы сделать в версии 5.5.22 более расширенный вариант сбора статистики о времени работы обработчиков событий. Но, если никакого дополнительного фидбэка на эту тему мы не получим, то приоритет данной задачи будет понижен и, скорее всего, в релиз 5.5.22 она уже не попадет.
Автор: eao197