开发者问题收集

将 AWS Lambda 函数转换为使用承诺?

2016-11-06
17723

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

在阅读了关于承诺的内容后,我花了太多时间尝试将以下代码转换为使用 Q 承诺,但失败了。

为了我自己的教育,也希望其他人的教育,我希望有人能帮助我将其转换为使用承诺(不一定是 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();
  }
};

编辑: 首次尝试使用承诺编写:

'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
3个回答

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

通常使用节点,您将获得与此类似的内容:

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

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

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

要将结果传递到我们的“始终如此”方法,我们需要返回一个承诺或一个值:

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

        });

    }
};
Brian
2016-11-06

AWS-SDK 支持所有服务的原生承诺。 有些需要其他参数才能正确返回,例如 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

slaughtr
2018-04-27

为了“promisify”回调函数,在我看来,最简单、最干净的方法是使用 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);
});
Boris Charpentier
2016-11-06