【问题标题】:NodeJS processing for API calls sequentially, but resulting in stack overflowNodeJS 顺序处理 API 调用,但导致堆栈溢出
【发布时间】:2020-05-13 10:17:18
【问题描述】:

好的,所以我有一种情况,我不能只向 API 服务器发出数千个请求。 我有一个 Node 进程(没有 UI),我需要依次处理每个 API 响应/更新,等待完成,然后再发送下一个请求。 我可能会让这比我想象的更复杂 - 不确定。我只能弄清楚如何通过递归调用来做到这一点,但这会导致堆栈溢出,因为可能有数千条记录。大致流程是这样的:

  1. 从带有 ID 的 SQL 表中获取行(结果)
  2. 制定并发送 API 调用以检索 ID 信息
  3. 如果返回的数据有图片数据,则回写到SQL表中
  4. 等待此过程,以免一次用数千个请求轰炸 API 服务器
  5. 重复直到处理完最后一个 ID(可以是数千,超过堆栈空间)

这里是示例代码(不是实际的,所以忽略语法错误,如果有的话)... 更新:删除敏感项目的实际运行代码

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);
  }

})();

所以这是使用 Promises 的答案(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()

}

【问题讨论】:

  • 好吧,如果您从异步回调中递归调用该函数(看起来就是这样),这不会导致堆栈溢出。调用异步回调时,堆栈为空且已完全展开。仅供参考,如果您在所有内容(请求库和数据库)上使用 Promise 接口,然后使用async/await,这将是一个干净的工具。您实际上可以只使用带有awaitfor 循环,并且所有内容都可以在一个传统外观for 循环中很好地序列化。
  • 这段代码真的很想用promises和async/await重写。不会有递归,而且会简单很多。如果您打算在 nodejs 中编程任何时间,那么学习如何使用 Promise 和 async/await 是完全值得的。在这段代码中,您可以切换到request-promise(尽管我个人更喜欢got() 库,因为request 系列处于维护模式,不再获得新功能)。然后,为您的数据库(内置)切换 promise 接口。
  • 我不确定这是否有帮助,但每次我遇到其中一种情况时,都是因为我认为是异步的代码中的某些内容实际上不是异步的。您是否尝试过添加 processNextTick?
  • @kgingeri 将一些东西包装在一个 processNextTick 中并看看它是否仍然爆炸似乎不到一分钟的工作。如果没有,那么你已经学到了一些东西。如果您正在处理纯粹的同步调用,那么您应该以不同的方式处理问题。祝你好运!
  • 好吧,在你这样做之前,我用async/await创建了一个结构的轮廓。希望你能从中学习。

标签: javascript node.js recursion stack


【解决方案1】:

正如我在所有 cmets 中所说的,使用 Promise 和 async/await 会简单得多。为此,您需要将所有异步操作切换到使用 Promise 的等效操作。

这是基于您发布的原始伪代码的一般大纲:

// 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() 库不再提供新功能并处于维护模式,您需要更改为其他库以获得内置的 Promise 支持。您可以使用request-promise(也处于维护模式),但我推荐使用较新的库之一,例如got(),它正在更积极地开发中。它有一些不错的功能(自动检查您的状态是否为 2xx,内置 JSON 解析等),我在上面使用过这些功能来保存代码。

  2. mysql2/promise 具有内置的 Promise 支持,您可以通过 const mysql = require('mysql2/promise'); 获得。我建议你切换到它。

  3. 由于这里是async/await 的用户,您可以在常规for 循环中循环遍历您的数据。而且,不需要递归。而且,没有堆栈堆积。

  4. promise 默认的工作方式,任何被拒绝的promise 都会自动终止这里的流程。我在几个地方使用.catch() 的唯一原因只是为了自定义日志记录和调整错误对象。然后我重新抛出,它将错误传播回给你的调用者。

  5. 您可以根据需要调整错误处理。 Promise 的通常约定是抛出一个 Error 对象(不是字符串),这通常是调用者希望看到的 Promise 是否拒绝。

  6. 可以轻松自定义此代码以记录错误并继续处理数组中的后续项目。您的原始代码似乎没有这样做,因此如果出现错误,我将其编写为中止。

【讨论】:

  • 这很有帮助。我会一行一行的消化!谢谢@jfriend00!
  • 最后一个问题(题外话):您如何跟上当前的库并找到最好的库等?你能推荐来源吗? IE。我知道我可以查看npmjs.com,但是有很多包裹!再次感谢。
  • @kgingeri - 除了研究个别主题以了解人们在谈论什么、推荐并在这里闲逛以发现人们在使用什么之外,我不知道任何其他方式。通过阅读此处,我才发现 request()request-promise() 处于维护模式,并且该帖子向我指出了我喜欢 got() 工作方式的替代列表。
  • @kgingeri - 如果你只是在搜索 npmjs 并寻找东西,我会寻找正在积极开发并被很多人使用的库,因为你可以找到很多死代码找到并使用其中一个最终意味着您将自己维护它。这仍然可能比从头开始编写自己的代码要好,但如果有人为您维护它,那就更好了。我还会查看打开的错误报告,看看有哪些类型的错误以及对它们的关注程度。
猜你喜欢
  • 2012-05-10
  • 2020-12-17
  • 1970-01-01
  • 2015-05-21
  • 2014-02-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多