NodeJs延迟Promise.all()

时间:2017-11-20 00:39:41

标签: node.js aws-api-gateway aws-cli aws-sdk-js

我试图更新前一段时间使用nodejs创建的工具(我不是JS开发人员,所以我试图将代码拼凑在一起)并且最后陷入困境障碍。

新功能将采用swagger .json定义,使用'aws-sdk' SDK for JS将端点与AWS服务上的匹配API网关进行比较,然后相应地更新网关。

代码在一个小的定义文件(大约15个端点)上运行正常但是只要我给它一个更大的一个,我开始得到大量的TooManyRequestsException错误。

据我所知,这是因为我对API网关服务的调用太快,需要延迟/暂停。这就是我被困的地方

我尝试过添加;

  • 对每个承诺的延迟()
  • 在每个承诺中运行setTimeout()
  • 向Promise.all和Promise.mapSeries添加延迟

目前我的代码循环遍历定义中的每个端点,然后将每个promise的响应添加到promise数组中:

promises.push(getMethodResponse(resourceMethod, value, apiName, resourcePath)); 

循环结束后,我运行:

        return Promise.all(promises)
        .catch((err) => {
            winston.error(err);
        })

我尝试过使用mapSeries(没有运气)。

看起来(getMethodResponse promise)中的函数是立即运行的,因此,无论我添加什么类型的延迟,它们都只是执行。我怀疑是我需要使(getMethodResponse)返回一个函数,然后使用mapSeries,但我也无法使其工作。

我试过的代码: 将getMethodResponse包裹在此:

return function(value){}

然后在循环之后添加它(并且在循环内 - 没有区别):

 Promise.mapSeries(function (promises) {
 return 'a'();
 }).then(function (results) {
 console.log('result', results);
 });

还尝试了许多其他建议:

Here

Here

有什么建议吗?

修改

作为请求,还有一些额外的代码可以解决这个问题。

当前使用一小组端点的代码(在Swagger文件中):

module.exports = (apiName, externalUrl) => {

return getSwaggerFromHttp(externalUrl)
    .then((swagger) => {
        let paths = swagger.paths;
        let resourcePath = '';
        let resourceMethod = '';
        let promises = [];

        _.each(paths, function (value, key) {
            resourcePath = key;
            _.each(value, function (value, key) {
                resourceMethod = key;
                let statusList = [];
                _.each(value.responses, function (value, key) {
                    if (key >= 200 && key <= 204) {
                        statusList.push(key)
                    }
                });
                _.each(statusList, function (value, key) { //Only for 200-201 range  

                    //Working with small set 
                    promises.push(getMethodResponse(resourceMethod, value, apiName, resourcePath))
                });             
            });
        });

        //Working with small set
        return Promise.all(promises)
        .catch((err) => {
            winston.error(err);
        })
    })
    .catch((err) => {
        winston.error(err);
    });

};

我已经尝试添加它来代替返回Promise.all():

            Promise.map(promises, function() {
            // Promise.map awaits for returned promises as well.
            console.log('X');
        },{concurrency: 5})
        .then(function() {
            return console.log("y");
        });

这样做的结果是这样的(每个端点都是一样的,有很多):

  

错误:TooManyRequestsException:请求太多   X   错误:TooManyRequestsException:请求太多   X   错误:TooManyRequestsException:请求太多

AWS SDK在每个promise中被调用3次,其功能是(从getMethodResponse()函数启动):

apigateway.getRestApisAsync()
return apigateway.getResourcesAsync(resourceParams)
apigateway.getMethodAsync(params, function (err, data) {}

典型的AWS SDK文档指出,这是进行过多连续调用(太快)时的典型行为。我过去曾遇到类似的问题,只需在被调用的代码中添加.delay(500)即可解决;

类似的东西:

    return apigateway.updateModelAsync(updateModelParams)
    .tap(() => logger.verbose(`Updated model ${updatedModel.name}`))
    .tap(() => bar.tick())
    .delay(500)

编辑#2

我想以彻底的名义,包括我的整个.js文件。

'use strict';

const AWS = require('aws-sdk');
let apigateway, lambda;
const Promise = require('bluebird');
const R = require('ramda');
const logger = require('../logger');
const config = require('../config/default');
const helpers = require('../library/helpers');
const winston = require('winston');
const request = require('request');
const _ = require('lodash');
const region = 'ap-southeast-2';
const methodLib = require('../aws/methods');

const emitter = require('../library/emitter');
emitter.on('updateRegion', (region) => {
    region = region;
    AWS.config.update({ region: region });
    apigateway = new AWS.APIGateway({ apiVersion: '2015-07-09' });
    Promise.promisifyAll(apigateway);
});

function getSwaggerFromHttp(externalUrl) {
    return new Promise((resolve, reject) => {
        request.get({
            url: externalUrl,
            header: {
                "content-type": "application/json"
            }
        }, (err, res, body) => {
            if (err) {
                winston.error(err);
                reject(err);
            }

            let result = JSON.parse(body);
            resolve(result);
        })
    });
}

/*
    Deletes a method response
*/
function deleteMethodResponse(httpMethod, resourceId, restApiId, statusCode, resourcePath) {

    let methodResponseParams = {
        httpMethod: httpMethod,
        resourceId: resourceId,
        restApiId: restApiId,
        statusCode: statusCode
    };

    return apigateway.deleteMethodResponseAsync(methodResponseParams)
        .delay(1200)
        .tap(() => logger.verbose(`Method response ${statusCode} deleted for path: ${resourcePath}`))
        .error((e) => {
            return console.log(`Error deleting Method Response ${httpMethod} not found on resource path: ${resourcePath} (resourceId: ${resourceId})`); // an error occurred
            logger.error('Error: ' + e.stack)
        });
}

/*
    Deletes an integration response
*/
function deleteIntegrationResponse(httpMethod, resourceId, restApiId, statusCode, resourcePath) {

    let methodResponseParams = {
        httpMethod: httpMethod,
        resourceId: resourceId,
        restApiId: restApiId,
        statusCode: statusCode
    };

    return apigateway.deleteIntegrationResponseAsync(methodResponseParams)
        .delay(1200)
        .tap(() => logger.verbose(`Integration response ${statusCode} deleted for path ${resourcePath}`))
        .error((e) => {
            return console.log(`Error deleting Integration Response ${httpMethod} not found on resource path: ${resourcePath} (resourceId: ${resourceId})`); // an error occurred
            logger.error('Error: ' + e.stack)
        });
}

/*
    Get Resource
*/
function getMethodResponse(httpMethod, statusCode, apiName, resourcePath) {

    let params = {
        httpMethod: httpMethod.toUpperCase(),
        resourceId: '',
        restApiId: ''
    }

    return getResourceDetails(apiName, resourcePath)
        .error((e) => {
            logger.unimportant('Error: ' + e.stack)
        }) 
        .then((result) => {
            //Only run the comparrison of models if the resourceId (from the url passed in) is found within the AWS Gateway
            if (result) {
                params.resourceId = result.resourceId
                params.restApiId = result.apiId

                var awsMethodResponses = [];
                try {
                    apigateway.getMethodAsync(params, function (err, data) {
                        if (err) {
                            if (err.statusCode == 404) {
                                return console.log(`Method ${params.httpMethod} not found on resource path: ${resourcePath} (resourceId: ${params.resourceId})`); // an error occurred
                            }
                            console.log(err, err.stack); // an error occurred
                        }
                        else {
                            if (data) {
                                _.each(data.methodResponses, function (value, key) {
                                    if (key >= 200 && key <= 204) {
                                        awsMethodResponses.push(key)
                                    }
                                });
                                awsMethodResponses = _.pull(awsMethodResponses, statusCode); //List of items not found within the Gateway - to be removed.
                                _.each(awsMethodResponses, function (value, key) {
                                    if (data.methodResponses[value].responseModels) {
                                        var existingModel = data.methodResponses[value].responseModels['application/json']; //Check if there is currently a model attached to the resource / method about to be deleted
                                        methodLib.updateResponseAssociation(params.httpMethod, params.resourceId, params.restApiId, statusCode, existingModel); //Associate this model to the same resource / method, under the new response status
                                    }
                                    deleteMethodResponse(params.httpMethod, params.resourceId, params.restApiId, value, resourcePath)
                                        .delay(1200)
                                        .done();
                                    deleteIntegrationResponse(params.httpMethod, params.resourceId, params.restApiId, value, resourcePath)
                                        .delay(1200)
                                        .done();
                                })
                            }
                        }
                    })
                        .catch(err => {
                            console.log(`Error: ${err}`);
                        });
                }
                catch (e) {
                    console.log(`getMethodAsync failed, Error: ${e}`);
                }
            }
        })
};

function getResourceDetails(apiName, resourcePath) {

    let resourceExpr = new RegExp(resourcePath + '$', 'i');

    let result = {
        apiId: '',
        resourceId: '',
        path: ''
    }

    return helpers.apiByName(apiName, AWS.config.region)
        .delay(1200)
        .then(apiId => {
            result.apiId = apiId;

            let resourceParams = {
                restApiId: apiId,
                limit: config.awsGetResourceLimit,
            };

            return apigateway.getResourcesAsync(resourceParams)

        })
        .then(R.prop('items'))
        .filter(R.pipe(R.prop('path'), R.test(resourceExpr)))
        .tap(helpers.handleNotFound('resource'))
        .then(R.head)
        .then([R.prop('path'), R.prop('id')])
        .then(returnedObj => {
            if (returnedObj.id) {
                result.path = returnedObj.path;
                result.resourceId = returnedObj.id;
                logger.unimportant(`ApiId: ${result.apiId} | ResourceId: ${result.resourceId} | Path: ${result.path}`);
                return result;
            }
        })
        .catch(err => {
            console.log(`Error: ${err} on API: ${apiName} Resource: ${resourcePath}`);
        });
};

function delay(t) {
    return new Promise(function(resolve) { 
        setTimeout(resolve, t)
    });
 }

module.exports = (apiName, externalUrl) => {

    return getSwaggerFromHttp(externalUrl)
        .then((swagger) => {
            let paths = swagger.paths;
            let resourcePath = '';
            let resourceMethod = '';
            let promises = [];

            _.each(paths, function (value, key) {
                resourcePath = key;
                _.each(value, function (value, key) {
                    resourceMethod = key;
                    let statusList = [];
                    _.each(value.responses, function (value, key) {
                        if (key >= 200 && key <= 204) {
                            statusList.push(key)
                        }
                    });
                    _.each(statusList, function (value, key) { //Only for 200-201 range  

                        promises.push(getMethodResponse(resourceMethod, value, apiName, resourcePath))

                    });             
                });
            });

            //Working with small set
            return Promise.all(promises)
            .catch((err) => {
                winston.error(err);
            })
        })
        .catch((err) => {
            winston.error(err);
        });
};

1 个答案:

答案 0 :(得分:2)

您显然对Promise.all()Promise.map()所做的事情存在误解。

所有Promise.all()所做的就是跟踪一整套承诺,告诉你它们所代表的异步操作何时完成(或者一个返回错误)。当你传递一组promises时(正如你所做的那样),所有这些异步操作都已经并行启动了。因此,如果您试图同时限制同时执行的异步操作数量,那么此时已经太晚了。因此,Promise.all()本身不会帮助您控制一次以任何方式运行的数量。

  

我之后也注意到,似乎这一行promises.push(getMethodResponse(resourceMethod, value, apiName, resourcePath))实际上是在执行promises而不是简单地将它们添加到数组中。好像上一个Promise.all()实际上并没有做多少。

是的,当您执行promises.push(getMethodResponse())时,您正在立即呼叫getMethodResponse()。这会立即启动异步操作。然后该函数返回一个promise,Promise.all()将监视该promise(以及你在数组中放入的所有其他),告诉你它们什么时候完成。所有Promise.all()都是如此。它监控您已经开始的操作。为了使飞行中的最大请求数同时保持在某个阈值以下,您不必像以前那样一次性启动异步操作。 Promise.all()并不适合你。

对于Bluebird的Promise.map()来说,你必须向它传递一系列DATA,而不是承诺。当您传递一系列承诺来表示您已经启动的异步操作时,它只能执行Promise.all()。但是,如果您传递一个数据数组和一个回调函数,然后可以为数组中的每个数据元素启动异步操作,那么当您使用concurrency选项时它可以帮助您。

您的代码非常复杂,所以我将使用一个想要读取大量URL的简单Web抓取器来说明,但出于内存考虑,一次只处理20个。

const rp = require('request-promise');
let urls = [...];    // large array of URLs to process

Promise.map(urls, function(url) {
    return rp(url).then(function(data) {
        // process scraped data here
        return someValue;
    });
}, {concurrency: 20}).then(function(results) {
   // process array of results here
}).catch(function(err) {
    // error here
});

在这个例子中,希望你可以看到一组数据项被传递到Promise.map()(不是一个promises数组)。然后,Promise.map()允许concurrency: 20管理数组处理的方式/时间,在这种情况下,它将使用Promise.map()设置确保同时处理的请求不超过20个

您使用Promise.map(promises, function() { ... }); 的努力是传递了一系列承诺,这对您没有帮助,因为承诺代表已经启动的异步操作:

TooManyRequestsException

然后,另外,你真的需要弄清楚究竟是什么导致concurrency错误,要么阅读展示这个的目标API的文档,要么通过做一大堆测试,因为可能有各种各样的可能导致这种情况的事情,并且不知道你需要控制什么,它只需要大量的猜测就可以找出可能有用的东西。 API可能检测到的最常见的事情是:

  1. 来自同一帐户或来源的同时请求。
  2. 来自同一帐户或来源的每单位时间请求(例如每秒请求数)。
  3. Promise.map()中的delay()操作可以轻松帮助您使用第一个选项,但不一定会帮助您使用第二个选项,因为您可以限制同时请求数量较少且仍然超过请求每秒限制。第二个需要一些实际的时间控制。插入{{1}}语句有时会起作用,但即使这不是一种非常直接的方法来管理它,也会导致控制不一致(某些东西有时会起作用,但不会导致其他时间)或次优控制(限制自己远远低于实际使用的东西。)

    要管理每秒请求数限制,您需要在自己的代码中使用速率限制库或实际速率限制逻辑进行一些实际时间控制。

    以下是限制每秒请求数量的方案示例:How to Manage Requests to Stay Below Rate Limiting