TheBeardedQuack
TheBeardedQuack

Reputation: 449

How to return data from AWS Lambda node.js asynchronous function?

I'm working on some scripts to be hosted on the AWS Lambda service. I chose Node.js because I'm okay with JS but I've not learned Python or Java. However it's proving to be a nightmare because I need to query a MySQL database and I just cannot figure out how to get the result out of the function correctly.

So basically I have something like this (I've cut some stuff out but you should get the idea), and this is all I want to do. I want to be able to query the MySQL database and when it has an answer simply return it or throw if there was an error.

var mysql = require("mysql");     //npm installed module "mysql"
var err = require("./errors.js"); //internally requires npm installed module "errors"

var main = function(event, context, callback){
  try{
    //Confidential
    var data = null;
    //Confidential
    try{
      data = databaseCommand("SELECT * FROM `<table>` WHERE <field> = <value>");
    }
    catch(e){
      if(e instanceof err.Database)
        //Return state that indicates "Internal server error".
      else
        throw e;
      return
    }
    //Do maths on data
    //Return "OK" and result
  }
  catch(e){
    //Return "Unkown error"
  }
};
var databaseCommand = function(cmdString){
  if(typeof cmdString !== "string") throw new err.InputInvalidType({explanation: "'cmdString' is of type '" + typeof cmdString + "', expected type 'string'"});

  var connection = mysql.createConnection({
    host: process.env.db_host,
    port: process.env.db_port || 3306,
    user: process.env.db_username,
    password: process.env.db_password,
    database: process.env.db_database
  });

  var ret = {
    error: null,
    result: null
  };

  //I cut out the connection.connect() because it can be implied and I'm confused enough
  connection.query(cmdString, function(error, rows){
    if(error)
      ret.error = error;
    else
      ret.result = rows;
  });
  connection.end();

  if(ret.error)
    throw new err.Database();
  return ret.result;
};

But for those who are versed in Node.js obviously this doesn't work because the call to connection.query is asynchronous, so my databaseCommand function always returns null (and doesn't throw) and causes errors in my main function.

Please help me understand how I can perform a basic synchronous request like this.

EDIT

I've seen "solutions" on using async methods that show something like (I probably have this wrong) the following changes, but I don't see how this is any different.

var mysql = require("mysql");     //npm installed module "mysql"
var err = require("./errors.js"); //internally requires npm installed module "errors"

var main = function(event, context, callback){
  try{
    //Confidential
    var data = null;
    //Confidential
    try{
      databaseCommand("SELECT * FROM `<table>` WHERE <field> = <value>", function(err, result){
        if(err)
          throw err;
        data = result;
      });
      //This function will still return before data is set
      //Maths will still be performed before data is set
    }
    catch(e){
      if(e instanceof err.Database)
        //Return state that indicates "Internal server error".
      else
        throw e;
      return
    }
    //Do maths on data
    //Return result
  }
  catch(e){
    //Return "Unkown error"
  }
}
var databaseCommand = function(cmdString, callback){
  if(typeof cmdString !== "string") throw new err.InputInvalidType({explanation: "'cmdString' is of type '" + typeof cmdString + "', expected type 'string'"});

  var connection = mysql.createConnection({
    host: process.env.db_host,
    port: process.env.db_port || 3306,
    user: process.env.db_username,
    password: process.env.db_password,
    database: process.env.db_database
  });

  var ret = {
    error: null,
    result: null
  };

  //I cut out the connection.connect() because it can be implied and I'm confused enough
  connection.query(cmdString, function(error, rows){
    if(error)
      callback(err, null);
    else
      callback(null, rows);
  });
  connection.end();
}

Upvotes: 3

Views: 9272

Answers (2)

Vlad Holubiev
Vlad Holubiev

Reputation: 5154

Looks like you are missing basic concepts of callbacks in JavaScript.

You must utilize callback argument you are provided with in main function. That's how you tell Lambda you have some results to return.

Here is a bit simplified version of your code to give you an idea:

var mysql = require("mysql"); //npm installed module "mysql"
var err = require("./errors.js"); //internally requires npm installed module "errors"

var connection;

var main = function(event, context, callback) {
    databaseCommand("SELECT * FROM `<table>` WHERE <field> = <value>", (error, rows) => {
        if (error) return callback(error);

        var results = doSomeMathWithRows(rows);

        callback(null, results);
    });
};

var databaseCommand = function(cmdString, callback) {
    if (typeof cmdString !== "string") throw new err.InputInvalidType({
        explanation: "'cmdString' is of type '" + typeof cmdString + "', expected type 'string'"
    });

    // Don't init DB connection for every request
    // Lambda functions can lose it only after freezing (see docs for details when)
    // so we create connection only on demand
    if (!connection) {
        connection = mysql.createConnection({
            host: process.env.db_host,
            port: process.env.db_port || 3306,
            user: process.env.db_username,
            password: process.env.db_password,
            database: process.env.db_database
        });
    }

    connection.query(cmdString, callback);
};

Notice a callback as a last argument of invoking databaseCommand. That means when connection.query will fetch some rows from DB it will invoke same callback Lambda provides.

Another important point is not to create DB connection on each Lambda execution. It's expensive. You can init variable one time and init it again when function was freezed. Read more about this here - https://aws.amazon.com/blogs/compute/container-reuse-in-lambda/

Let me know if it works for you, as I edited this code online without any checks. But hope you got the point. You can read more how to write a handler function here: http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html?shortFooter=true

Upvotes: 9

Ungl&#252;ckspilz
Ungl&#252;ckspilz

Reputation: 1890

You seem to be misunderstanding how asynchronous execution works as well as the AWS Lambda architecture. Here's a modified version of your code sample, with inline comments:

var mysql = require("mysql");     //npm installed module "mysql"

// This would be the exported handler function that gets invoked when your AWS Lambda function is triggered
exports.queryDbHandler = function(event, context, callback) {
  try {
    // This function executes an asynchronous operation so you need to pass in a callback that will be executed once it's done
    databaseCommand("SELECT * FROM `<table>` WHERE <field> = <value>", function onData(error, dbData) {
      // Once the callback is executed, you call the callback provided to your Lambda function. First argument is an error and the second is payload/result of the operation. First argument should be null if all went ok
      if (error) {
        callback(error);
      } else {
        let dbDataProcessed = // do something to dbData
        callback(null, dbDataProcessed);
      }
    });
  }
  catch(e) {
    // In case you had an exception in the synchronous part of the code, you still need to invoke the callback and provide an error
    callback(e);
  }
}

var databaseCommand = function(cmdString, onResultCallback){
  // Instead of throwing, it would be better to just invoke the callback and provide an error object
  if(typeof cmdString !== "string") throw new err.InputInvalidType({explanation: "'cmdString' is of type '" + typeof cmdString + "', expected type 'string'"});

  var connection = mysql.createConnection({
    host: process.env.db_host,
    port: process.env.db_port || 3306,
    user: process.env.db_username,
    password: process.env.db_password,
    database: process.env.db_database
  });

  connection.query(cmdString, function(error, rows) {
      // Once we have the data, or an error happened, invoke the callback
      onResultCallback(error, rows);
  });
};

Upvotes: 3

Related Questions