Статьи

Больше асинхронности: использование auto () для параллельных операций

Один из соломенных людей, на которого люди часто ссылаются при обсуждении событийно-ориентированного программирования, ala Node.js , — это страх, что сложное поведение на стороне сервера примет форму неуправляемо сложных, глубоко вложенных обратных вызовов.

Хотя большая часть кода, который я до сих пор писал с помощью Node, очень проста, обычно включает только один обратный вызов, мой мысленный образ Node — это поступающий запрос, который запускает серию операций с базой данных и другие асинхронные запросы, которые все объединить, каким-то темным образом, в один ответ. Я представляю себе такую ​​просьбу, как пинбол, падающий в несколько бамперов и подпрыгивающий вокруг, сбивая цели, пока они не выстрелили обратно к плавникам.

Вот пример рабочего процесса из примера приложения, которое я создаю; Я управляю набором изображений, используемых в слайд-шоу; поэтому у меня есть сущность SlideImage в MongoDB (с использованием Mongoose ), и каждый SlideImage ссылается на файл, хранящийся в GridFS Mongo .

Когда приходит время удалить SlideImage, необходимо также удалить файл GridFS. Псевдокод для такой операции в системе, не основанной на событиях, может выглядеть примерно так:

def deleteSlideImageById(db, imageId)
  err, slideImage = db.readSlideImage(imageId)
  if err ...

  err, file = db.openGridFile(slideImage.fileId)
  if err ...

  err = file.delete()
  if err ...

  err = slideImage.delete()
  if err ...

Внутри Node, где весь код управляется событиями и ориентирован на обратный вызов, мы можем улучшить псевдокод, выполнив параллельное удаление документа SlideImage и файла GridFS. Ну, в любом случае, это теория, но я обычно придерживаюсь подхода водопада , так как отслеживание после завершения всех операций будет утомительным и подвержено ошибкам, особенно в свете правильной обработки любых ошибок, которые могут возникнуть.

Введите асинхронную функцию auto () . С помощью auto () вы определяете набор задач, которые зависят друг от друга. Каждая задача — это функция, которая получает обратный вызов, и объект результатов. auto () определяет, когда каждая задача готова к запуску.

Когда задача не выполняется, она передает ошибку в функцию обратного вызова. Когда задача завершается успешно, она передает нулевое значение и значение результата в функцию обратного вызова. Последующее выполнение задач позволяет увидеть результаты предыдущих задач в объекте результата, введя имя предыдущей задачи.

Как и в случае водопада (), одиночный обратный вызов передается объекту ошибки или объекту конечного результата.

Давайте посмотрим, как все это сочетается, в пять шагов:

  • Найти документ SlideImage
  • Откройте файл GridFS
  • Удалить файл GridFS
  • Удалить документ SlideImage
  • Отправить ответ (успех или неудача) клиенту

Степень детализации здесь частично зависит от конкретных API и их обратных вызовов.

Код для этого на удивление труден:

app.delete "/api/images/pending/:id", (req, res) ->

    async.auto
      find: [(callback) ->
        schema.SlideImage.findById req.params.id, callback]
      remove: ["find", (callback, results) ->
        results.find.remove callback]
      openFile: ["find", (callback, results) ->
        new GridStore(mongoose.connection.db, results.find.file, "r").open callback]
      removeFile: ["openFile", (callback, results) ->
        results.openFile.unlink callback],
      (err) ->
        if err
          res.send "unable to delete SlideImage or File", 500
        else
          res.send result: "ok"

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

Так что поиск не имеет никаких зависимостей, и пинки всего процесса. Я думаю, что действительно важно то, насколько последовательны API-интерфейсы Node.js: базовый обратный вызов, состоящий из ошибки и результата, позволяет очень легко интегрировать код из множества разных библиотек и авторов (я думаю, что там есть некая монада ). В коде обратный вызов, созданный auto () и переданный для поиска, вполне подходит для перехода в findById. Все это с низким импедансом: не нужно писать какие-либо прокладки или адаптеры.

Более поздние задачи принимают дополнительный параметр результатов; results.find — это документ SlideImage, предоставленный задачей поиска.

Задачи удаления и openFile зависят от поиска: они будут выполняться в определенном порядке после поиска; что еще более важно, их обратные вызовы будут вызываться в непредсказуемом порядке, основанном на завершении различных асинхронных операций.

Только после того, как все задачи выполнены (или одна задача передала ошибку своему обратному вызову), вызывается последний обратный вызов; это то, что отправляет клиенту ошибку 500 или успех 200 (с ответом JSON {«result»: «ok»}.

Я думаю, что этот код и читабелен, и сжат; на самом деле, я не могу себе представить, чтобы это было намного более кратким. Мой мозг начинает действительно идти параллельно: часть моего мозга оценивает все с точки зрения Java-кода и идиом, в то время как остальное охватывает идиомы JavaScript, CoffeeScript и Node.js; Java-часть впечатлена тем, насколько эти решения JavaScript отказываются от сложных API-интерфейсов в пользу работы с определенной «формой» данных; если бы я писал что-то подобное на Java, я был бы заинтересован в гибких интерфейсах и скрытых реализациях с тонной кода для написания и тестирования.

Я не уверен, что приложение, которое я пишу, будет иметь какие-либо рабочие процессы обработки, значительно более сложные, чем этот бит, но если это произойдет, я вполне рад иметь auto () в своем наборе инструментов.