NodeJS обработка вызовов API последовательно, но приводит к переполнению стека - PullRequest
1 голос
/ 28 января 2020

ОК, поэтому у меня есть ситуация, когда я не могу просто отправить тысячи запросов на сервер API. У меня есть Node-процесс (без пользовательского интерфейса), который мне нужен для обработки каждого ответа / обновления API последовательно, ожидая завершения перед отправкой следующего запроса. Я могу сделать это более сложным, чем я думаю - не уверен. Я могу только выяснить, как сделать это с рекурсивными вызовами, но это приводит к переполнению стека, поскольку может быть тысячи записей. Общий процесс такой:

  1. получение строк из SQL таблицы с идентификаторами (результат)
  2. формулировка и отправка вызова API для получения информации идентификатора
  3. если возвращенные данные содержат данные изображения, запишите их обратно в SQL таблицу
  4. и дождитесь этого процесса, чтобы не бомбардировать сервер API тысячами запросов одновременно
  5. повторять до тех пор, пока не будет обработан последний идентификатор (может быть тысяч, больше места в стеке)

Вот пример кода (не актуально, поэтому игнорируйте синтаксические ошибки, если таковые имеются) ... ОБНОВЛЕНО: фактический работающий код с удаленными чувствительными элементами

var g_con = null;    //...yeah I know, globals are bad

//
//  [ found updating ]
//
function getSetImage(result, row, found) {

  if(row >= result.length) { //...exit on no row or last row processed
    con.end();
    return;
  }

  item = result[row];  //...next SQL row

  if((item !== undefined) && (item.autoid !== undefined)) {

    //...assemble API and send request
    //
    let url =   'https://...API header...'
              + item.autoid
              + '...API params...';

    request(url, (error, response, body) => {

      if(response.statusCode !== 200)
        throw('Server is not responding\n' + response.statusMessage);

      let imageData = JSON.parse(body);
      if((imageData.value[0]        !== undefined) &&
         (imageData.value[0].DETAIL !== undefined) &&
         (imageData.value[0].DETAIL.Value.length)   ) {

        //...post back to SQL
        //
        found++;
        console.log('\n' + item.autoid + '/['+ item.descr + '], ' + 'Found:' + found);

        qry = 'update inventory set image = "'+imageData.value[0].DETAIL.Value+'" where autoid = "'+item.autoid+'";';
        g_con.query(qry, (err) => {
          if (err) {
            console.log('ERROR:',err.message, '\nSQL:['+err.sql+']\n');
            throw err.message;
          }
        });

        row++;
        setTimeout(()=>{getSetImage(result, row, found)}, 0);   //...nested call after SQL

      } else {

        row++;
        process.stdout.write('.');                                   //...show '.' for record, but no image
        setTimeout(()=>{getSetImage(result, row, found)}, 0);   //...nested call after SQL

      }

    }); //...request callback

  }

  // } else {

  //   throw '\nERROR! result['+row+'] undefined? Images found: '+found;
  // }
}


//
//  [ main lines ]
//
(() => {

  let params = null;
  try {

    params = JSON.parse(fs.readFileSync('./config.json'));

    //...load autoids array from SQL inventory table - saving autoids
    //   autoids in INVENTRY join on par_aid's in INVENTRYIMAGES
    //
    g_con = mysql.createConnection(params.SQLConnection);
    g_con.connect((err) => {  if(err) {
                                console.log('ERROR:',err.message);
                                throw err.message;
                              }
                           });

    //...do requested query and return data or an error
    //
    let qry = 'select autoid, descr from inventory order by autoid;';
    g_con.query(qry, (err, results, flds) => {

        if (err || flds === undefined) {
          console.log('ERROR:',err.message, '\nSQL:['+err.sql+']\n');
          throw err.message;
        }

        console.log('Results length:',results.length);
        let row   = 0;
        let found = 0;
        getSetImage(results, row, found);

      });

  }

  catch (err) {
    console.log('Error parsing config parameters!');
    console.log(err);
  }

})();

Итак, вот ответ с помощью Обещаний (кроме MySQL):

//
//  [ found updating ]
//
async function getSetImage(data) {

  for(let item of data) {

    if(item && item.autoid) {

      //...assemble API and send request
      //
      let url   = g_URLHeader + g_URLPartA + item.autoid + g_URLPartB;

      let image = await got(url).json().catch(err => {
                    console.log(err);
                    err.message = 'API server is not responding';
                    throw err;
                  });

      if(image && image.value[0] && image.value[0].DETAIL &&
         image.value[0].DETAIL.Value.length       ) {
           console.log('\nFound: ['+item.autoid+' - '+item.descr
                       + '] a total of ' + g_found + ' in ' + g_count + ' rows');

          g_found++;

          //...post back to SQL
          //
          let qry = 'update inventory set image = "'
                  + image.value[0].DETAIL.Value
                  + '" where autoid = "'
                  + item.autoid+'";';
          await g_con.query(qry, (err) => {
                      if (err) {
                        console.log('ERROR:',err.message, '\nSQL:['+err.sql+']\n');
                        throw err.message;
                      }
                });

      } else {

          process.stdout.write('.');  //...show '.' for record, but no image

      }  //...if/else image.value

      g_count++;

    }  //...if item

  } //...for()

}

1 Ответ

2 голосов
/ 28 января 2020

Как я уже говорил во всех моих комментариях, было бы намного проще использовать обещания и async/await. Для этого вам нужно переключить все свои асинхронные операции на эквиваленты, использующие обещания.

Вот общая схема, основанная на исходном опубликованном вами псевдокоде:

// use got() for promise version of request
const got = require('got');

// use require("mysql2/promise" for promise version of mysql

async function getSetImage(data) {

    for (let item of data) {
        if (item && item.id) {
            let url = uriHeader + uriPartA + item.id + uriPartB;
            let image = await got(url).json().catch(err => {
                // log and modify error, then rethrow
                console.log(err);
                err.msg = 'API Server is not responding\n';
                throw err;
            });
            if (image.value && image.value.length) {
                console.log('\nFound image for ' + item.id + '\n');
                let qry = 'update inventory set image = "' + image.value + '" where id = "' + item.id + '";';
                await con.query(qry).catch(err => {
                    console.log('ERROR:', err.message, '\nSQL:[' + err.sql + ']\n');
                    throw err;
                });
            }
        } else {
            // no image data found
            process.stdout.write('.'); //...show '.' for record, but no image
        }
    }
}

//...sql query is done, returning "result" - data rows
getSetImage(result).then(() => {
    console.log("all done");
}).catch(err => {
    console.log(err);
});

Некоторые примечания об этом коде:

  1. Библиотека request() больше не получает новые функции и находится в режиме обслуживания, и вам нужно перейти на другую библиотеку, чтобы получить встроенную поддержку обещаний. Вы можете использовать request-promise (также в режиме обслуживания), но я рекомендую одну из более новых библиотек, такую ​​как got(), которая разрабатывается более активно. У него есть несколько приятных функций (автоматически проверяет статус 2xx, встроенный JSON синтаксический анализ и т. Д. c ...), которые я использовал выше для сохранения кода.

  2. mysql2/promise имеет встроенную поддержку обещаний, которую вы получаете с const mysql = require('mysql2/promise');. Я бы порекомендовал вам переключиться на него.

  3. Поскольку пользователь async/await здесь, вы можете просто l oop просматривать ваши данные в обычном for l oop. И никакой рекурсии не требуется. И нет наращивания стека.

  4. Способ, которым обещания работают по умолчанию, любые отклоненные обещания автоматически прервут поток здесь. Единственная причина, по которой я использую .catch() в нескольких местах, это просто для пользовательского ведения журнала и настройки объекта ошибки. Затем я перебрасываю сообщение, которое передает сообщение об ошибке вызывающей стороне.

  5. Вы можете настроить обработку ошибок по своему желанию. Обычное соглашение с обещаниями заключается в создании объекта Error (а не строки), и именно это часто ожидают вызывающие абоненты, если обещание отклоняется.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...