【问题标题】:Converting AWS Lambda function to use promises?将 AWS Lambda 函数转换为使用 Promise?
【发布时间】:2017-03-19 14:39:12
【问题描述】:

我正在编写一个使用 AWS Lambda 定期执行的简单 HTTP 'ping' 函数。它使用四个异步函数:http.get、S3.getObject、S3.putObject 和 nodemailer.sendMail。每个似乎都有稍微不同的回调模型。

在阅读了有关 Promise 的内容后,我花了太多时间尝试将以下代码转换为使用 Q Promise,结果惨遭失败。

为了我自己的教育以及希望其他人的教育,我希望有人可以帮助我将其转换为使用承诺(不一定是 Q):

'use strict';

var http = require('http');
var nodemailer = require('nodemailer');
var AWS = require('aws-sdk');
var s3 = new AWS.S3( { params: { Bucket: 'my-bucket' } } );

exports.handler = (event, context, callback) => {
  var lastStatus;

  var options = {
    host: event.server.host,
    port: event.server.port ? event.server.port : 80,
    path: event.server.path ? event.server.path : '',
    method: event.server.method ? event.server.method : 'HEAD',
    timeout: 5000
  };
  var transporter = nodemailer.createTransport({
    host: event.mail.host,
    port: event.mail.port ? event.mail.port : 587,
    auth: {
      user: event.mail.user,
      pass: event.mail.pass
    }
  });

  var d = new Date();
  var UTCstring = d.toUTCString();

  // email templates 
  var downMail = {
    from: event.mail.from,
    to: event.mail.to,
    subject: 'Lambda DOWN alert: SITE (' + event.server.host + ') is DOWN',
    text: 'LambdaAlert DOWN:\r\nSITE (' + event.server.host + ') is DOWN as at ' + UTCstring + '.'
  };
  var upMail = {
    from: event.mail.from,
    to: event.mail.to,
    subject: 'Lambda UP alert: SITE (' + event.server.host + ') is UP',
    text: 'LambdaAlert UP:\r\nSITE (' + event.server.host + ') is UP as at ' + UTCstring + '.'
  };

  // Run async chain to ensure that S3 calls execute in proper order
  s3.getObject( { Key: 'lastPingStatus' }, (err, data) => {
    // get last status from S3
    if (err) { lastStatus = "UP"; } else {
      lastStatus = data.Body.toString();
      console.log("Last observed status: " + lastStatus);
    }
    http_request(options, lastStatus);
  });

  function http_request(requestOptions, lastStatus) {
    var req = http.request(requestOptions, function(res) {
      if (res.statusCode == 200) {
        if (lastStatus == "DOWN") {
          console.log('Email up notice sending...');
          transporter.sendMail(upMail, function(error, info) {
            if (error) {
              console.log("ERROR: " + error);
              callback(null, "ERROR: " + error);
            } else {
              console.log('No further details available.');
              callback(null, 'Up message sent');
            }
          });
        }
        s3.putObject({ Key: 'lastPingStatus', Body: 'UP', ContentType: 'text/plain' }, (error, data) => { console.log("Saved last state as UP"); });
        callback(null, 'Website is OK.');
      }
    });
    req.on('error', function(e) {
      if (lastStatus == "UP") {
        console.log('Email down notice sending...');
        transporter.sendMail(downMail, function(error, info) {
          if (error) {
            console.log("ERROR: " + error);
            callback(null, "ERROR: " + error);
          } else {
            console.log('No further details available.');
            callback(null, 'Down message sent');
          }
        });
        s3.putObject({ Key: 'lastPingStatus', Body: 'DOWN', ContentType: 'text/plain' }, (error, data) => { console.log("Saved last state as DOWN"); });
        callback(null, 'Website is DOWN.');
      }
    });
    req.end();
  }
};

编辑:第一次尝试使用 Promise 编写:

'use strict';

var http = require('http');
var nodemailer = require('nodemailer');
var AWS = require('aws-sdk');
var s3 = new AWS.S3( { params: { Bucket: 'lambda-key-storage' } } );

exports.handler = (event, context, callback) => {
  var lastStatus;

  var options = {
    host: event.server.host,
    port: event.server.port ? event.server.port : 80,
    path: event.server.path ? event.server.path : '',
    method: event.server.method ? event.server.method : 'HEAD',
    timeout: 5000
  };
  var transporter = nodemailer.createTransport({
    host: event.mail.host,
    port: event.mail.port ? event.mail.port : 587,
    auth: {
      user: event.mail.user,
      pass: event.mail.pass
    }
  });

  var d = new Date();
  var UTCstring = d.toUTCString();

  // email templates 
  var downMail = {
    from: event.mail.from,
    to: event.mail.to,
    subject: 'Lambda DOWN alert: SITE (' + event.server.host + ') is DOWN',
    text: 'LambdaAlert DOWN:\r\nSITE (' + event.server.host + ') is DOWN as at ' + UTCstring + '.'
  };
  var upMail = {
    from: event.mail.from,
    to: event.mail.to,
    subject: 'Lambda UP alert: SITE (' + event.server.host + ') is UP',
    text: 'LambdaAlert UP:\r\nSITE (' + event.server.host + ') is UP as at ' + UTCstring + '.'
  };

  var myProm = new Promise(function(resolve, reject) {
    console.log("called 1");
    s3.getObject( { Key: 'lastPingStatus' }, (err, data) => {
      // get last status from S3
      if (err) { 
        resolve("UP"); 
      } else {
        resolve(data.Body.toString());
      }
    });
  })
  .then(function(lastStatus) {
    console.log("called 2");
    console.log("Last observed status: " + lastStatus);
    var req = http.request(options, function(res) {
      resolve(res.statusCode);
    });
    req.on('error', function(e) {
      reject(e);
    });
    req.end();
    return "??";
  })
  .then(function(statusCode) {
    console.log("called 3");
    if (statusCode == 200) {
      if (lastStatus == "DOWN") {
        console.log('Email up notice sending...');
        resolve("upTrigger");
      } else {
        resolve("upNoTrigger");
      }
      s3.putObject({ Key: 'lastPingStatus', Body: 'UP', ContentType: 'text/plain' }, (err, data) => { console.log("Saved last state as UP"); });
      callback(null, 'Website is OK.');
    }
  })
  .catch(function(err){
    console.log("called 3 - error");
    // Send mail notifying of error
    if (lastStatus == "UP") {
      console.log('Email down notice sending...');
      resolve("downTrigger");
      s3.putObject({ Key: 'lastPingStatus', Body: 'DOWN', ContentType: 'text/plain' }, (error, data) => { console.log("Saved last state as DOWN"); });
      callback(null, 'Website is DOWN.');
      return("downTrigger");
    } else {
      return "downNoTrigger";
    }
  })
  .then(function(trigger) {
    console.log("called 4");
    if (trigger == "upTrigger") {
      transporter.sendMail(upMail, (error, info) => {
        if (error) {
          console.log("ERROR: " + error);
          callback(null, "ERROR: " + error);
        } else {
          console.log('Up message sent.');
          callback(null, 'Up message sent');
        }
      });
    } else if (trigger == "downTrigger") {
      transporter.sendMail(downMail, (error, info) => {
        if (error) {
          console.log("ERROR: " + error);
          callback(null, "ERROR: " + error);
        } else {
          console.log('Down message sent.');
          callback(null, 'Down message sent');
        }
      });
    }
    console.log("Outcome of ping was: ", trigger);
  });
};

这不太行。结果日志为:

called 1
called 2
Last observed status: UP
called 3
called 4
Outcome of ping was:  undefined
ReferenceError: resolve is not defined

【问题讨论】:

  • 大提示:2 个阶段,(1) promisify 在最低级别 - s3.getObject()、s3.putObject()、transporter.sendMail() 和 http.request(),( 2) 使用这四种方法的承诺版本构建应用程序流。

标签: node.js amazon-s3 promise aws-lambda


【解决方案1】:

将典型的异步函数转换为 Promise 非常简单。我宁愿尝试演示如何转换它,也不愿编写代码,因为您不会从中学到任何东西。

通常使用 node 你会得到类似这样的东西:

  doSomethingAsync(callback);

    function doSomethingAsync(callback){
        var err, result;
        // Do some work
        ... 

        callback(err, result);
    }
    function callback(err, result){
        if(err){
            // Handle error
        } else{
            // Success so do something with result
        }
    }

包装异步函数的 Promise 通常看起来像这样:

var myProm = new Promise(function(resolve, reject){
     doSomethingAsync(function(err, result){       
        if(err){
            reject(err);
        } else{
            resolve(result)
        } 
     });
})
.then(function(result){
  // Success so do something with result
  console.log("Success:", result)
})
.catch(function(err){
 // Handle error
  console.log("Error: ", err);
})
.then(function(result){
   // Where's my result? - result == undefined as we didn't return anything up the chain
  console.log("I always execute but result is gone", result)
})

要将结果沿链传递给我们的“always then”方法,我们需要返回一个承诺或一个值:

var myProm = new Promise(function(resolve, reject){
         doSomethingAsync(function(err, result){       
            if(err){
                reject(err);
            } else{
                resolve(result)
            } 
         });
    })
    .then(function(result){
      // Success so do something with result
      console.log("Success:", result)
      return result;
    })
    .catch(function(err){
     // Handle error
      console.log("Error: ", err);
      return err;
    })
    .then(function(result){
      // The err/result now gets passed down the chain :)
      console.log("Oh there it is", result)
    })

我认为使用上述模式应该可以满足代码示例中的大多数异步方法和事件,如果有任何特定的方法和事件给您带来麻烦,请在其中添加评论,我将尝试涵盖这些特定示例。

这是一个将其转换为承诺的尝试——我很累,所以对任何混乱或错误表示歉意——还有很多清理工作可以做。

我所做的基本上是尝试将代码分解为任务并将每个任务包装在一个承诺中。这样我们就可以根据需要解决/拒绝和链接它们。

'use strict';

var http = require('http');
var nodemailer = require('nodemailer');
var AWS = require('aws-sdk');
var s3 = new AWS.S3( { params: { Bucket: 'my-bucket' } } );

exports.handler = function (event, context, callback) {
    var lastStatus;

    var options = {
        host: event.server.host,
        port: event.server.port ? event.server.port : 80,
        path: event.server.path ? event.server.path : '',
        method: event.server.method ? event.server.method : 'HEAD',
        timeout: 5000
    };
    var transporter = nodemailer.createTransport({
        host: event.mail.host,
        port: event.mail.port ? event.mail.port : 587,
        auth: {
            user: event.mail.user,
            pass: event.mail.pass
        }
    });

    var d = new Date();
    var UTCstring = d.toUTCString();

    // email templates
    var downMail = {
        from: event.mail.from,
        to: event.mail.to,
        subject: 'Lambda DOWN alert: SITE (' + event.server.host + ') is DOWN',
        text: 'LambdaAlert DOWN:\r\nSITE (' + event.server.host + ') is DOWN as at ' + UTCstring + '.'
    };
    var upMail = {
        from: event.mail.from,
        to: event.mail.to,
        subject: 'Lambda UP alert: SITE (' + event.server.host + ') is UP',
        text: 'LambdaAlert UP:\r\nSITE (' + event.server.host + ') is UP as at ' + UTCstring + '.'
    };

    // Run async chain to ensure that S3 calls execute in proper order

    function getLastPingStatus(){
        return new Promise(function(resolve, reject){
            s3.getObject( { Key: 'lastPingStatus' }, function(err, data) {
                // get last status from S3
                if (err) {
                    lastStatus = "UP";
                    reject(lastStatus)
                } else {
                    lastStatus = data.Body.toString();
                    resolve(lastStatus);
                    console.log("Last observed status: " + lastStatus);
                }
            });
        })
    }
    getLastPingStatus()
        .then(httpRequest)
        .catch(httpRequest); // Otherwise a reject will throw an error

    function sendMail(mail, status){ // status = "up" or "down" -
        return new Promise(function(resolve, reject){
            transporter.sendMail(mail, function(error, info) {
                if (error) {
                    console.log("ERROR: " + error);
                    reject(null, "ERROR: " + error);
                } else {
                    console.log('No further details available.');
                    resolve(null, status + ' message sent');
                }
            });
        });
    }

    function saveStatus(up) {
        return new Promise(function (resolve, reject) {
            var saveOptions,
                message;
            // I didn't bother refactoring these as promises at they do the same thing regardless of outcome
            if(up){
                saveOptions = [{ Key: 'lastPingStatus', Body: 'UP', ContentType: 'text/plain' }, function(error, data) { console.log("Saved last state as UP"); }];
                message = 'Website is OK.';
            } else{
                saveOptions = [{ Key: 'lastPingStatus', Body: 'DOWN', ContentType: 'text/plain' }, function(error, data)  { console.log("Saved last state as DOWN"); }];
                message = 'Website is DOWN.';
            }
            s3.putObject.apply(this, saveOptions);
            callback(null, message);
        });
    }

    function httpRequest(lastStatus) {
        var requestOptions = options;
        return new Promise (function (resolve, reject){
            var req = http.request(requestOptions, function(res) {
                if (res.statusCode == 200) {
                    if (lastStatus == "DOWN") {
                        console.log('Email up notice sending...');
                        sendMail(upMail, "Up")
                            .then(resolve, reject) 
                            .then(saveStatus(true))
                            .then(callback)
                    }
                }
            });
            req.on('error', function(e) {
                if (lastStatus == "UP") {
                    console.log('Email down notice sending...');
                    sendmail(downMail, "Down")
                        .then(resolve, reject)
                        .then(saveStatus(false))
                        .then(callback)
                }
            });
            req.end();

        });

    }
};

【讨论】:

  • 我已经在原始 Q 中尝试了 Promises 代码。它大部分都有效,但我认为我对何时应该解决、何时应该返回以及何时应该回调感到困惑。
  • 我已经添加了将其转换为承诺的机会 - 我很累,对错误感到抱歉。通常,最好将您的代码分解为返回承诺的小任务。一旦任务成功就解决任务,如果任务失败则拒绝它,将“返回”值作为参数传递给解决/拒绝,然后将其传递给链中的下一个 then/catch 并分配为该特定承诺的值.一旦一个承诺被解决或拒绝,它的价值是不可变的。在您的尝试中,您试图在 .then 中解决承诺,但解决和拒绝参数超出范围
【解决方案2】:

为了“承诺”回调函数,恕我直言,最简单和更干净的方法是使用bluebird。您只是不想为了简化代码而编写胶水代码,这会适得其反(而且容易出错)。

来自doc

var Promise = require("bluebird");
var readFile = Promise.promisify(require("fs").readFile);

readFile("myfile.js", "utf8").then(function(contents) {
    return eval(contents);
}).then(function(result) {
    console.log("The result of evaluating myfile.js", result);
}).catch(SyntaxError, function(e) {
    console.log("File had syntax error", e);
//Catch any other error
}).catch(function(e) {
    console.log("Error reading file", e);
});

【讨论】:

    【解决方案3】:

    The AWS-SDK supports native promises, for all services. 有些需要额外的参数才能正确返回,比如 Lambda.invoke()。

    你基本上会这样做

    s3.putObject({ Key: 'key', Bucket: 'bucket' }).promise()
        .then(data => {
            // this is the same as the data callback parameter
        })
        .catch(error => {
            // handle your error
        })
    

    或者,您可以使用async/await

    const file = await s3.getObject(params).promise()
    // do things with the result
    

    为了快速访问实际文件(而不是元数据):

    const file = JSON.parse(await s3.getObject(params).promise().then(res => res.Body));
    

    https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/using-promises.html

    【讨论】:

    • 虽然 Brian 的回答非常好并且解释性很好,但这让我找到了实现链接的最佳方式
    【解决方案4】:

    在阅读了 slaughtr 的回答后,我决定这样做,以便在按下 AWS IoT 按钮时保存一些数据:

    var AWS = require("aws-sdk");
    var iot = new AWS.Iot();
    
    exports.handler = (event, context, callback) => {
      iot.listThings({
        attributeName: 'dsn',
        attributeValue: event.serialNumber,
        maxResults: 1
      })
      .promise()
      .then(response => {
        return iot.listThingGroupsForThing({thingName: response.things[0].thingName}).promise();
      })
      .then(groupsList => insertRecordIntoDDB(date, serialNumber, groupsList.thingGroups[0].groupName))
      .catch(err => console.log(err))
    };
    

    在我决定使用 async/await 进一步压缩它之后不久

    exports.handler = async (event, context, callback) => {
    var eventText = JSON.stringify(event, null, 2);
    
    var thingsList = await iot.listThings({ 
      attributeName: 'dsn', 
      attributeValue: event.serialNumber, 
      maxResults: 1
    }).promise()
    
    var groupsList = await iot.listThingGroupsForThing({
      'thingName': thingsList.things[0].thingName
    }).promise();
    insertRecordIntoDDB(date, serialNumber, groupsList.thingGroups[0].groupName)
    
    };
    

    我在异步编程方面还很陌生,所以我不确定我最喜欢什么。 Promise 链可能有点像意大利面条,而 async await 只会帮助将所有这些掩盖成更容易理解的东西

    【讨论】:

      【解决方案5】:

      在aws lambda中使用promisified节点http调用外部api。

      exports.handler = async (event) => {
          return httprequest().then((data) => {
              const response = {
                  statusCode: 200,
                  body: JSON.stringify(data),
              };
          return response;
          });
      };
      function httprequest() {
           return new Promise((resolve, reject) => {
              const options = {
                  host: 'jsonplaceholder.typicode.com',
                  path: '/todos',
                  port: 443,
                  method: 'GET'
              };
              const req = http.request(options, (res) => {
                if (res.statusCode < 200 || res.statusCode >= 300) {
                      return reject(new Error('statusCode=' + res.statusCode));
                  }
                  var body = [];
                  res.on('data', function(chunk) {
                      body.push(chunk);
                  });
                  res.on('end', function() {
                      try {
                          body = JSON.parse(Buffer.concat(body).toString());
                      } catch(e) {
                          reject(e);
                      }
                      resolve(body);
                  });
              });
              req.on('error', (e) => {
                reject(e.message);
              });
              // send the request
             req.end();
          });
      }
      

      【讨论】:

        猜你喜欢
        • 2021-04-23
        • 2019-10-20
        • 2020-03-01
        • 2022-01-28
        • 2017-01-05
        • 2021-09-21
        • 2016-03-02
        • 1970-01-01
        • 2017-09-09
        相关资源
        最近更新 更多