Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Converting AWS Lambda function to use promises?

I am writing a simple HTTP 'ping' function that is being periodically executed using AWS Lambda. It uses four asynchronous functions: http.get, S3.getObject, S3.putObject, and nodemailer.sendMail. Each seems to have a slightly different callback model.

After reading about promises, I spent way too much time trying to convert the following code to use Q promises and failed miserably.

For my own education and hopefully that of others, I was hoping someone could help me convert this to using promises (doesn't have to be 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();
  }
};

EDIT: First attempt at writing using 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);
  });
};

This doesn't quite work. The result logs are:

called 1
called 2
Last observed status: UP
called 3
called 4
Outcome of ping was:  undefined
ReferenceError: resolve is not defined
like image 997
GuruJ Avatar asked Nov 06 '16 01:11

GuruJ


3 Answers

Converting your typical async function to a promise is pretty straight forward. I'd rather try and demonstrate how to convert it than write the code as you don't learn anything from that.

Usually with node you'll have something that looks similar to this:

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

A promise wrapping an async function generally looks something like this:

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

To pass the result down the chain to our "always then" method we need to return a promise or a value:

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

I think that using the above patterns should cater to most of the async methods and events in your code example if any particular ones are giving you trouble drop a comment in and I'll try to cover those specific examples.

Here's an attempt at converting it over to promises - I'm pretty tired so apologies about any mess or mistakes - also there's still plenty of cleanup that could be done.

Essentially what I've done is try to break down the code into tasks and wrap each of those tasks in a promise. That way we can resolve/reject and chain them as needed.

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

        });

    }
};
like image 93
Brian Avatar answered Nov 16 '22 00:11

Brian


The AWS-SDK supports native promises, for all services. Some need additional parameters to return properly, like Lambda.invoke().

You would essentially do

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

Or, you could use async/await:

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

For quick access to the actual file (and not metadata):

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

like image 29
slaughtr Avatar answered Nov 15 '22 22:11

slaughtr


In order to "promisify" callback function, imho, the easiest and cleaner way is to use bluebird. You just don't want to write glue code in order to simplify your code, it's counter productive (and it's error prone).

From the 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);
});
like image 40
Boris Charpentier Avatar answered Nov 15 '22 23:11

Boris Charpentier