42 строки кода для выхода из лимба

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

    Но в один прекрасный день… Правильно! Прилетает мыло «ваша программа не работает» ((С) 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 строки кода, мы решили весьма редкую, но довольно пакостную проблему с зацикливанием событий. Кто знает (я вот не знаю), может и вам оно пригодится. Все тут.

    Счастья, добра и электричества в ваши дома.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 13

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

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

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

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

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

              0
              Ну да, но для получения подробной информации о каждом элементе стека, нужно заменить Error.prepareStackTrace на свою функцию, иначе тут будет уже конкатенированная строка.
            0
            Спасибо, сработали суеверия. Вы совершенно правы. Подправил, обновил.
            +6

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


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

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

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

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


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

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

                  Так же ваше решение отличается еще тем, что переопределяется ряд нативных средств (setTimeout, setInterval и другие), у нас это запрещено религией), хотя по большому счету к делу это и не относится. Но в целом мне нравится. Спасибо за ваше время на эксперименты.
              0
              У меня возникает вопрос больше к использованию indexOf. Каюсь — не вникал глубоко в код, но допустим у меня вызывается событие Aa, а после него вызываю событие a — то выходит что условие indexOf отработает и будет исключение?
                0
                Если у вас есть событие «Aa» и событие «a», то это два разных события.

              Only users with full accounts can post comments. Log in, please.