转换AWS Lambda函数以使用promises?

时间:2016-11-06 01:22:13

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

我正在写一个简单的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();
  }
};

编辑:首次尝试使用promises编写:

'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

5 个答案:

答案 0 :(得分:9)

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

通常,对于节点,您会看到类似于此的内容:

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

将结果传递给我们的"始终然后"我们需要返回一个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)
      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();

        });

    }
};

答案 1 :(得分:5)

为了宣传"回调函数,imho,最简单,最简洁的方法是使用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);
});

答案 2 :(得分:5)

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

答案 3 :(得分:0)

在阅读了屠宰者的答案之后,我决定这样做,以便在按下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链接可能会有点像意大利面条,而异步等待只会帮助将所有内容隐藏成更容易理解的内容

答案 4 :(得分:0)

使用已承诺的节点http来调用aws lambda中的外部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();
    });
}