0,0
рейтинг
21 января 2013 в 12:01

Разработка → Не надо давать обещания, или Promises наоборот из песочницы

Каждый программист, начинающий разрабатывать под Node.js, встаёт перед выбором стратегии организации асинхронного кода в проекте. В то время, как в небольших системных утилитах поддерживать гигиену асинхронного кода достаточно просто, при росте массы кода в проекте решение этой задачи начинает требовать введения дополнительного, так называемого control flow средства.

В этой статье будет рассмотрена небольшая control flow библиотека «Flowy», являющаяся развитием идей проекта Step Тима Касвелла, и ядро которой базируется на концепциях CommonJS Promises, а также приведены аргументы, почему же Promises — это так неудобно.




Как это выглядит


function leaveMessage(username, text, callback) {
    Flowy(
        function() {
            // concurrent execution of two queries
            model.users.findOne(username, this.slot());
            model.settings.findOne(username, this.slot());
        },
        function(err, user, settings) {
            // error propagating
            if (!user) throw new Error('user not found');
            if (!settings.canReceiveMessages) throw new Error('violating privacy settings');
            model.messages.create(user, text, this.slot());
        },
        function(err, message) {
            model.notifications.create(message, this.slot());
        },
        callback //any error will be automatically propagated to this point
    );
}

  • Каждый шаг, переданный в обертку Flowy выполняется в контексте библиотеки (переменная this). При этом контекст предоставляет возможность передавать данные на следующий шаг путем генерирования колбэков, которые можно передать классическим nodejs-like функциям в качестве последнего аргумента (вызов this.slot()).
  • Все, что выполняется в одном шаге, — выполняется параллельно.
  • Управление будет передано следующему шагу лишь после того, как все его «слоты» будут заполнены данными — все колбэки, сгенерированные вызовом this.slot() завершатся успешно, либо же первый из них получит сообщение об ошибке.
  • При возникновении ошибки в любом из шагов выполнение всей цепочки будет прервано и ошибка будет возвращена в последний шаг.


Почему это выглядит именно так?


Программисту, начинающему знакомство с API неблокирующей подсистемы ввода-вывода Node.js, предлагается интерфейс асинхронных вызовов следующего вида:

fs.readFile('/etc/passwd', 'utf8', function (err, data) {
    if (err) throw err;
    console.log(data);
});

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

Мы хотим сохранить «родные» nodejs-like интерфейсы функций и колбэков. Каждый шаг Flowy имеет интерфейс nodejs-колбэка, что позволяет легко оборачивать всю цепочку шагов в традиционную nodejs-функцию.

При этом, основной идеей Promises (в качестве примера реализации в дальнейшем будет использоваться библиотека «Q» Криса Коуэла) является замена передачи колбэка последним аргументом в асинхронный вызов созданием цепочки вызовов методов Promise:

// chaining promises: Q.fcall(step1).then(step2).then(step3).done()
return getUsername()
.then(function (username) {
    return getUser(username)
    .then(function (user) {
        // if we get here without an error, the value returned here
        // or the exception thrown here resolves the promise returned by the first line
    })
})

Первое, что бросается в глаза: функции возвращают Promise. Таким образом, для использования библиотеки необходимо все «классические» функции обернуть в Promise-адаптер (подробнее этот процесс описан на станице проекта), либо же разрабатывать код с жестко ориентированными на библиотеку интерфейсами (но при этом все публичные интерфейсы модуля необходимо будет обратно привести в классический вид, учитывая требование, сформулированное выше). Это неудобно. Это звучит пугающе и не менее пугающе выглядит. При этом сразу же на ум приходит второе требование к control flow библиотеке:

Библиотека должна быть лишь «клеем» между существующими частями системы и не становиться тяжелой зависимостью. Все особенности функционирования «Flowy» скрыты внутри шагов — того самого клея, — что позволяет функциям, использующим ее, оставаться «чистыми» для внешнего мира. Сор должен оставаться в избе.

При работе с библиотеками, позволяющими создавать цепочки (chaining) из асинхронных вызовов, часто возникает необходимость выполнить часть вызовов параллельно. Библиотека «Q» предоставляет следующее неловкое решение:

Q.allResolved(promises)
.then(function (promises) {
    promises.forEach(function (promise) {
        if (promise.isFulfilled()) {
            var value = promise.valueOf();
        } else {
            var exception = promise.valueOf().exception;
        }
    })
})

В добавок ко всему, если мы вдруг захотим нарушить правило «один аргумент — одно возвращенное значение», то придется заниматься дополнительными упражнениями:

return getUsername()
.then(function (username) {
    return [username, getUser(username)];
})
.spread(function (username, user) {
})

Читая этот код, само собой напрашивается еще одно требование к библиотеке:

Мы хотим легко выполнять несколько параллельных запросов и передавать любое количество аргументов в колбэки. «Flowy» это умеет без каких-либо дополнительных усилий со стороны разработчика благодаря своей архитектуре.

Итак, «Flowy» — это легковесная библиотека по управлению асинхронным потоком выполнения программы, позволяющая легко решать повседневные вопросы разработчиков под Node.js и хорошо зарекомендовавшая себя в production-окружении.

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

Полезные источники:
Алексей Масленников @geeqie
карма
8,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

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

  • –1
    В ноде же есть волокна, зачем эти велосипеды?

    github.com/nin-jin/node-jin#sod
    • –1
      В ноде нет волокон. Есть плагин для реализации волокон, который является сугубо экспериментальным, сложным для понимания и практически невозможным для отладки.
      • 0
        они ставятся через npm как и любые другие модули. используются через require как и любые другие модули. какая разница в ядре они или «плагином»?

        и в чём же заключается его «экспериментальность»? по моим наблюдениям работает исправно.

        опять же, что сложного для понимания в схеме «запускаем поток и когда надо ставим его на паузу»? это простая и естественная схема, в отличие от «городим кучу функций и через прямую кишку прокидываем между ними переменные»

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

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

          console.time( 'serial' )
              console.log( get().statusCode )
              console.log( get().statusCode )
          console.timeEnd( 'serial' )
          
          console.time( 'parallel' )
              var resp1= get()
              var resp2= get()
              console.log( resp1.statusCode )
              console.log( resp2.statusCode )
          console.timeEnd( 'parallel' )
          

          Чем в этом примере отличаются последовательные вызовы от параллельных? И это пример из документации библиотеки по твоей же ссылке.

          С «приятными бонусами» в виде отлова ошибок тоже спорно. Если ошибку можно обработать, то и во «Flowy» (и в любом подобном инструменте) ее можно поймать на последнем шаге. А она вылетела, пытаясь убить весь процесс, то это, видимо, ошибка, которую нужно чинить, а не отлавливать в волокнах — существует же лог консоли и утилиты наподобие forever, в конце-концов.

          Прогрессивные технологии — это замечательно. Волокна, в частности, — это очень мощный в умелых руках инструмент. Но это пока что не традиционное средство, к которому привык разработчик на Node.js (да что уж там, любой разработчик), а незнакомые инструменты — это первый шаг к беспорядку в проекте и ошибкам в коде.
          • 0
            > Во-первых, сложно то, что мы «ставим поток на паузу».

            и что же в этом сложного? особенно в сравнении с асинхронной лапшой.

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

            прежде чем говорить эти глупости не мешало бы самому попробовать с ними поработать.

            > Опять же, по причине того, что код начинает выглядеть «синхронно», появляются дополнительные проблемы с параллельным выполнением асинхронных запросов, которые уже не вписываются в эту уютную концепцию и добавляют ложку дегтя в эту «прогрессивную» бочку:

            нет там таких проблем. в моей либе используется автоматическое распарралеливание. волокно не останавливается, пока нам не потребуется работа с результатом. аналогичный код на fibers/promise выглядел бы так:

            console.time( 'serial' )
            console.log( get().wait().statusCode )
            console.log( get().wait().statusCode )
            console.timeEnd( 'serial' )

            console.time( 'parallel' )
            var resp1= get()
            var resp2= get()
            console.log( resp1.wait().statusCode )
            console.log( resp2.wait().statusCode )
            console.timeEnd( 'parallel' )

            > С «приятными бонусами» в виде отлова ошибок тоже спорно. Если ошибку можно обработать, то и во «Flowy» (и в любом подобном инструменте) ее можно поймать на последнем шаге. А она вылетела, пытаясь убить весь процесс, то это, видимо, ошибка, которую нужно чинить, а не отлавливать в волокнах — существует же лог консоли и утилиты наподобие forever, в конце-концов.

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

            > Прогрессивные технологии — это замечательно. Волокна, в частности, — это очень мощный в умелых руках инструмент. Но это пока что не традиционное средство, к которому привык разработчик на Node.js (да что уж там, любой разработчик), а незнакомые инструменты — это первый шаг к беспорядку в проекте и ошибкам в коде.

            можно подумать Flowy или любая другая либа являются традиционными и к ним привыкли разработчики) да и вообще, ратовать за явную асинхронность в то время как работа с нею является самой большой болью разработчиков на ноде — это что-то из практик BDSM)

            всякие такие библиотеки-выравниватели реализуют лишь один вид потока — последовательное выполнение. однако, реальная жизнь интересней, в ней нужно часть потока выполнять в зависимости от условия, в ней нужно некоторые ветви выполнять циклично. и тому подобное. в js есть все основные операторы контроля потока, зачем их переизобретать на своём асинхронном dsl, если ест возможность дожидаться завершения асинхронных операций не теряя контекста?
            • 0
              особенно в сравнении с асинхронной лапшой
              Работая с Node.js, люди ожидают увидеть именно асинхронный код.

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

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

              это не годится для продакшена совершенно
              Отчего же? Если не нужно писать сверхотказоустойчивую автономную систему, то вполне годится. Я еще раз подчеркну, есть лог консоли приложения и утилиты автоматического перезапуска упавшего инстанса.

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

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

              всякие такие библиотеки-выравниватели реализуют лишь один вид потока — последовательное выполнение.
              Все понятно: «статью не чилал, но осуждаю».

              зачем их переизобретать на своём асинхронном dsl, если ест возможность дожидаться завершения асинхронных операций не теряя контекста?
              Программа, может, и не потеряет контекст, но разработчик, читающий код, сойдет с ума. Write-only код — это как раз то, что не нужно для продакшена, а не вылетевший иксепшн из-за бага, который тут же пофиксят следом.
              • 0
                > Работая с Node.js, люди ожидают увидеть именно асинхронный код.

                ага, особенно используя require и *Sync методы) не знаю как насчёт людей, а я предпочитаю простой, ясный и понятный синхронный код, а не костыли типа Flowy и ко

                > И ты никогда не узнаешь, читая код, было ли тебе возвращено просто immediate значение, или же прилитело что-то из колбека.

                в этом-то вся и соль. мне совершенно не важно откуда там оно прилетело — я работаю с простым апи: запрашиваю данные и потом с ними работаю.

                > это уже проблемы стороннего кода — зачем моей либе с ним разбираться?

                очевидно затем, что из-за него падает твоё приложение. и ты никак не можешь эту ошибку перехватить и продолжить выполнение.

                > Отчего же? Если не нужно писать сверхотказоустойчивую автономную систему, то вполне годится. Я еще раз подчеркну, есть лог консоли приложения и утилиты автоматического перезапуска упавшего инстанса.

                быстро поднятое упавшим не считается?)) смотри как бы сервер у тебя в один прекрасный момент не ушёл в бесконечную перезагрузку.

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

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

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

                из многочисленых статей описывающих очередной велосипед выстраивания колбэков в цепочки на подобии синхронного кода.

                > Все понятно: «статью не чилал, но осуждаю».

                а скажешь нет? как, например, пропустить один шаг?

                > Программа, может, и не потеряет контекст, но разработчик, читающий код, сойдет с ума. Write-only код — это как раз то, что не нужно для продакшена, а не вылетевший иксепшн из-за бага, который тут же пофиксят следом.

                на этом закончим наш спор. похоже у тебя неизлечимая деформация мозга, если такой код ты считаешь write-only:

                function leaveMessage( username, text ){
                ....var user= model.users.findOne( username )
                ....if( !user ) throw new Error( 'user not found' )

                ....var message= model.messages.create( user, text )
                ....model.notifications.create( message )
                }

                а такой простым и понятным:

                function leaveMessage(username, text, callback) {
                ....Flowy(
                ........function() {
                ............model.users.findOne(username, this.slot());
                ........},
                ........function(err, user) {
                ............if (!user) throw new Error('user not found');
                ............model.messages.create(user, text, this.slot());
                ........},
                ........function(err, message) {
                ............model.notifications.create(message, this.slot());
                ........},
                ........callback //any error will be automatically propagated to this point
                ....);
                }

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.