Операции по сбору файлов и сводный отчет о результатах с неблокирующим вводом-выводом - PullRequest
2 голосов
/ 24 февраля 2011

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

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

Написание этого кода на языке блокировки (например, на Ruby) чрезвычайно просто.

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

Моим первым замыслом было выполнение каждого шага последовательно.

  1. Загрузите все файлы, создав набор файлов для обработки
  2. Обработка каждого файла в коллекции
  3. Сообщить о результатах после обработки всех файлов

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

Мой второй дизайн - обрабатывать каждый файл так, как он был асинхронно найден на диске. Для аргументации давайте представим метод, который выглядит примерно так:

eachFileMatching(path, expression, callback) {
  // recursively, asynchronously traverse the file system,
  // calling callback every time a file name matches expression.
}

И потребитель этого метода выглядит примерно так:

eachFileMatching('test/', /_test.js/, function(err, testFile) {
  // read and process the content of testFile
});

Хотя этот дизайн выглядит как очень «нодальный» способ работы с IO, он страдает от 2 основных проблем (по крайней мере, в моей предположительно ошибочной реализации):

  1. Я не знаю, когда все файлы были обработаны, поэтому я не знаю, когда собирать и публиковать результаты.
  2. Поскольку чтение файлов неблокирующее и рекурсивное, я пытаюсь понять, не было ли найдено файлов.

Я надеюсь, что я просто делаю что-то не так, и есть какая-то достаточно простая стратегия, которую другие люди используют, чтобы второй подход работал.

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

Ответы [ 3 ]

1 голос
/ 27 февраля 2011

Как оказалось, самое маленькое рабочее решение, которое я смог построить, гораздо сложнее, чем я ожидал.

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

Если существует значительно другой способ решения этой проблемы, более простой и / или более эффективный, мне очень интересно услышать его. Меня действительно удивляет, что решение этого, казалось бы, простого требования потребует такого большого количества кода, но, возможно, именно поэтому кто-то изобрел блокировку io?

Сложность действительно заключается в желании удовлетворить все следующих требований:

  • Обрабатывать файлы по мере их обнаружения
  • Знать, когда поиск будет завершен
  • Узнать, не найдены ли файлы

Вот код:

/**
 * Call fileHandler with the file name and file Stat for each file found inside
 * of the provided directory.
 *
 * Call the optionally provided completeHandler with an array of files (mingled
 * with directories) and an array of Stat objects (one for each of the found
 * files.
 *
 * Following is an example of a simple usage:
 *
 *   eachFileOrDirectory('test/', function(err, file, stat) {
 *     if (err) throw err;
 *     if (!stat.isDirectory()) {
 *       console.log(">> Found file: " + file);
 *     }
 *   });
 *
 * Following is an example that waits for all files and directories to be 
 * scanned and then uses the entire result to do something:
 *
 *   eachFileOrDirectory('test/', null, function(files, stats) {
 *     if (err) throw err;
 *     var len = files.length;
 *     for (var i = 0; i < len; i++) {
 *       if (!stats[i].isDirectory()) {
 *         console.log(">> Found file: " + files[i]);
 *       }
 *     }
 *   });
 */
var eachFileOrDirectory = function(directory, fileHandler, completeHandler) {
  var filesToCheck = 0;
  var checkedFiles = [];
  var checkedStats = [];

  directory = (directory) ? directory : './';

  var fullFilePath = function(dir, file) {
    return dir.replace(/\/$/, '') + '/' + file;
  };

  var checkComplete = function() {
    if (filesToCheck == 0 && completeHandler) {
      completeHandler(null, checkedFiles, checkedStats);
    }
  };

  var onFileOrDirectory = function(fileOrDirectory) {
    filesToCheck++;
    fs.stat(fileOrDirectory, function(err, stat) {
      filesToCheck--;
      if (err) return fileHandler(err);
      checkedFiles.push(fileOrDirectory);
      checkedStats.push(stat);
      fileHandler(null, fileOrDirectory, stat);
      if (stat.isDirectory()) {
        onDirectory(fileOrDirectory);
      }
      checkComplete();
    });
  };

  var onDirectory = function(dir) {
    filesToCheck++;
    fs.readdir(dir, function(err, files) {
      filesToCheck--;
      if (err) return fileHandler(err);
      files.forEach(function(file, index) {
        file = fullFilePath(dir, file);
        onFileOrDirectory(file);
      });
      checkComplete();
    });
  }

  onFileOrDirectory(directory);
};
1 голос
/ 24 февраля 2011

Что вы подразумеваете под "читать и обрабатывать содержимое testFile"?

Я не понимаю, почему вы понятия не имеете, когда все файлы обрабатываются. Вы не используете потоки? Поток имеет несколько событий, а не только data. Если вы обработаете события end, то вы будете знать, когда закончится каждый файл.

Например, у вас может быть list имен файлов, настроить обработку для каждого файла, а затем, когда вы получите событие end, удалить имя файла из списка. Когда список пуст, все готово. Или создайте объект FileName, который содержит имя и статус завершения. Когда вы получите событие end, измените статус и уменьшите счетчик имени файла. Когда счетчик сбрасывается до нуля, все готово или если вы не уверены, что можете сканировать все объекты FileName, чтобы убедиться, что их состояние завершено.

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

... Я только что натолкнулся на этот сценарий в другом вопросе, и принятый ответ (плюс ссылка на github) хорошо это объясняет. Проверьте код, управляемый событиями зацикливания?

0 голосов
/ 25 февраля 2011

2 способа сделать это, первый и, вероятно, рассмотренный поочередно, будут выглядеть примерно так:

var files = [];
doFile(files, oncomplete);

function doFile(files, oncomplete) {
  if (files.length === 0) return oncomplete();
  var f = files.pop();
  processFile(f, function(err) {
    // Handle error if any
    doFile(files, oncomplete); // Recurse
  });
};

function processFile(file, callback) {
  // Do whatever you want to do and once 
  // done call the callback
  ...
  callback();
};

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

var files = [];
doFiles(files, oncomplete);

function doFiles(files, oncomplete) {
  var exp = files.length;
  var done = 0;
  for (var i = 0; i < exp; i++) {
    processFile(files[i], function(err) {
      // Handle errors (but still need to increment counter)
      if (++done === exp) return oncomplete();      
    });
  }
};

function processFile(file, callback) {
  // Do whatever you want to do and once 
  // done call the callback
  ...
  callback();
};

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

Tnx

Guido

...