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

Но в один прекрасный день… Правильно! Прилетает мыло «ваша программа не работает» ((С) bash), телефоны разогреваются до красна, а юристы нервно перечитывают, что они там накидали в раздел «гарантийное обслуживание».

Ровно такая ситуация была и у нас. Делали мы довольно увесистый проект, суть которого можно было бы описать так (кратко, конечно): есть разный контент (клиентская база, маркетинговая база, база связей и прочее, прочее, прочее) и различные способы его представления (widget, popup, modal etc.). Иными словами, с нашей стороны была подготовлена платформа (API доступа к данным, визуализация, вся, как это модно говорить, экосистема (хотя я не знаю, что это значит, но звучит уж очень круто)), чтобы разработчики заказчика могли писать свои контролеры данных и просто файликом их «класть» в указанное место, после чего счастливо лицезреть, как появляется новенький виджет со списком текущих котировок по какому-нибудь мудрёному индексу.

И как я уже сказал, все складывалось хорошо. Провели несколько «мастер» классов, все показали, все рассказали, выпили пива и завертелось. Уже без нас.

Пока все не сломалось. Именно так: «все» и «сломалось». В какие-то моменты приложение просто стало намертво виснуть. Да так, что вкладку браузера не закроешь. Мало-мальски опытный web-developer тут же скажет – у вас цикл где-то заклинило ребятишки. И будет прав, что уж там.

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

И, как вы уже догадались, «клинило» как раз контроллер событий. На пальцах: событие A, вызывает событие B, а событие B – событие C, а оно, в свою очередь, вновь вызывает событие A. Та-да-м, встречайте цикл!

Наш обработчик событий был до безобразия простой и ютился в файлике на 44 строках кода. Однако он не умел делать весьма актуальную вещицу – проверять не в цикле ли он.

Много пить думать не пришлось и решение нашли довольно быстро. Хорошее оно или плохое – это все на ваш суд. Опишу лишь основную идею.

Единственный способ проверить «кто» вызвал цепочку событий (в нашем примере найти A, B и C) – это проверить stack. Чтобы получить stack нужно просто «выбросить» ошибку.

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

В общем теперь, если выполнить это (событие A вызывает B, B вызывает С, а C вновь вызывает A):

        var safeevents = new SafeEvents();
        safeevents.bind('A', function () {
            safeevents.trigger('B');
        });
        safeevents.bind('B', function () {
            safeevents.trigger('C');
        });
        safeevents.bind('C', function () {
            safeevents.trigger('A');
        });
        safeevents.trigger('A');

То на этот раз приложение не уйдет в лимб, а выбросит в консоль исключение «Uncaught Error: Event [A] called itself. Full chain: A, B, C». Profit. Теперь разработчику не нужно уходить на три дополнительных перекура, чтобы сообразить в чем собственно дело – все видно из сообщения в консоли.

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

        var safeevents = new SafeEvents();
        safeevents.bind('A', function () {
            safeevents.trigger('B');
        });
        safeevents.bind('B', function () {
            safeevents.trigger('C');
        });
        safeevents.bind('C', function () {
            /*
            * Use method "safely" to wrap your async methods and create safe callback.
            */
            setTimeout(safeevents.safely(function () {
                safeevents.trigger('A');
            }), 10);
        });
        safeevents.trigger('A');

Обратите внимание на функцию обратного вызова в таймере. Мы добавляем «обертку», чтобы передать данные о предыдущих событиях в асинхронных вызовах. И вновь в консоли мы увидим: «Uncaught Error: Event [A] called itself. Full chain: A, B, C».

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

        var safeevents = new SafeEvents();
        safeevents.bind('A', function () {
            safeevents.trigger('B');
        });
        safeevents.bind('B', function () {
            safeevents.trigger('C');
        });
        safeevents.bind('C', function () {
            safeevents.trigger('A');
        });
        safeevents.bind(safeevents.onloop, function (e, chain, last_event, stack) {
            console.log('Error message: ' + e);
            console.log('Full chain of events: ' + chain.join(', '));
            console.log('Last event (generated loop): ' + last_event);
            console.log('Error stack: ' + stack);
        });
        safeevents.trigger('A');

Теперь наше приложение вовсе не прерывается, но цикл при этом будет успешно предотвращен.

В общем переписав наш наипростейший обработчик событий и получив дополнительно 42 строки кода, мы решили весьма редкую, но довольно пакостную проблему с зацикливанием событий. Кто знает (я вот не знаю), может и вам оно пригодится. Все тут.

Счастья, добра и электричества в ваши дома.
Поделиться с друзьями
-->

Комментарии (13)


  1. Odrin
    24.10.2016 13:18
    +3

    Вы замеряли, как данное решение повлияло на производительность?


    1. AlexWriter
      24.10.2016 21:31

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

      С другой стороны, я делал тест на глубину цепочки, это да. Выложил на github в examples если интересно. Прогонка на 1000 событий, где 1000ое событие взывает нулевое, образуя цикл. Главным образом делалась проверка на утечки, но и производительность с этого теста тоже видна.


  1. vintage
    24.10.2016 13:39
    +4

    Исключение не обязательно бросать, чтобы получить из него стектрейс.


    1. arvitaly
      24.10.2016 17:40

      Вы об этом?
      https://github.com/v8/v8/wiki/Stack-Trace-API#customizing-stack-traces


      1. vintage
        24.10.2016 18:12

        Нет, об этом: new Error().stack


        1. alibertino
          24.10.2016 18:21

          Ну да, но для получения подробной информации о каждом элементе стека, нужно заменить Error.prepareStackTrace на свою функцию, иначе тут будет уже конкатенированная строка.


    1. AlexWriter
      24.10.2016 21:56

      Спасибо, сработали суеверия. Вы совершенно правы. Подправил, обновил.


  1. mayorovp
    24.10.2016 14:40
    +6

    У меня получилось проще: https://github.com/mayorovp/tiny-safeevents/blob/master/safeevents.js


    Вместо ловли стека через исключение я в явном виде отслеживаю этот стек во внутреннем массиве.


    1. AlexWriter
      24.10.2016 21:20

      Может я что-то делаю не так, но ваше решение не работает (будьте осторожны JS уйдет в глубокий нимб после запуска).

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


      1. mayorovp
        24.10.2016 21:23

        Это второй тест, который я проверял, он работает. Да, у меня по умолчанию циклы только детектятся — но не разрываются. Надо подписаться на 'onloop' и сделать throw, чтобы разорвать цикл.


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


        1. AlexWriter
          24.10.2016 22:07

          Не думаю, что событие должно порождать событие (то есть само себя). Из этой парадигмы мы исходили, да и исходим в проектировании. Если же возникнет такая ситуация, то лучше работать с такой ситуацией, как со специальным случаем, а не наоборот. Поэтому дефалтное поведение – остановить, а возможность «вырваться» из проверки остается через «небезопасный» асинхронный вызов.

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


  1. pan-alexey
    24.10.2016 21:43

    У меня возникает вопрос больше к использованию indexOf. Каюсь — не вникал глубоко в код, но допустим у меня вызывается событие Aa, а после него вызываю событие a — то выходит что условие indexOf отработает и будет исключение?


    1. AlexWriter
      24.10.2016 21:44

      Если у вас есть событие «Aa» и событие «a», то это два разных события.