3

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();
}
halfer
  • 19,824
  • 17
  • 99
  • 186
TheBeardedQuack
  • 449
  • 4
  • 15
  • `return rows;` or `return ret.result;` as the former will be undefined always no matter whether async or not – bugwheels94 Dec 13 '16 at 10:43
  • Supposed to be "return ret.result", updated question to show... My return is not undefined, it's always null as "ret.result" is initially setup to null and then the function returns before its value is set. – TheBeardedQuack Dec 13 '16 at 10:46
  • so do you want the call to be synchronous or do you want to embrace the node js async behavior and change your code behaviour and try promises or callbacks instead – bugwheels94 Dec 13 '16 at 10:46
  • I am unfamiliar with the async heavy nature of Node.js and the project is urgent, and does not feature a lot of IO heavy calls. I would like to make my MySQL calls synchronous... Hopefully in future I will be able to revisit this and optimise using the correct techniques but I'd rather it work first. – TheBeardedQuack Dec 13 '16 at 10:48
  • what is the callback parameter in your main function? Is that function supposed to be executed after the query completes – bugwheels94 Dec 13 '16 at 10:49
  • Possible duplicate of [How do I return the response from an asynchronous call?](http://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call) – Ben Fortune Dec 13 '16 at 10:50
  • As far as I can understand, in AWS the callback is basically the return... I am not able to return from the function without calling the callback first or it shouts at me :) `callback(err, result); return;` – TheBeardedQuack Dec 13 '16 at 10:51
  • @BenFortune I still can't understand how this solves the problem though. *Use sync method:* As far as I can tell the mysql library doesn't have any? *Embrace callbacks:* Doesn't this just move the problem one call higher? At some point there has to be some form of synchronisation to ensure guarantee the data has been set before it's used. `var data = null; databaseCommand("SELECT ...", function(err, res){ if(err) throw err; data = result; }); //Do maths on data //Problem has only been moved one call higher instead.` – TheBeardedQuack Dec 13 '16 at 11:04
  • There are no synchronous methods, as the data returned is returned asynchronously. To guarantee your data is available, you need to do your logic inside the callback function, or within a promise. – Ben Fortune Dec 13 '16 at 11:09
  • About `Maths will still be performed before data is set` - do your math INSIDE a callback. There is no way around – Vlad Holubiev Dec 13 '16 at 11:52

2 Answers2

8

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

cviejo
  • 4,388
  • 19
  • 30
Vlad Holubiev
  • 4,876
  • 7
  • 44
  • 59
  • Right, thanks for the pointer on not setting up the connection each time. Also I have a *LOT* of maths to do on the data returned from the database and my result to amazon is vastly different from that returned from the database. My question is how do I extract the data so I can perform the maths on it before returning... I'll modify my original question to try and show code to what about callbacks I don't understand. – TheBeardedQuack Dec 13 '16 at 11:13
  • @SatsumaBenji I updated my code example to show how to do some math with returned db response – Vlad Holubiev Dec 13 '16 at 11:18
  • I think I see why I've seen this referred to as "callback hell" almost everywhere I look... If this is the **ONLY** way to perform operations on data that can only be received asynchronously relative to the main function then for long functions this results in loads and loads of nested lambdas (or named functions). Besides this Amazon seems to complain if the main function returns without callback being called... If I nest loads of callback functions I'll be able to perform my maths but I still won't be able to send the result before the AWS-Lambda terminates... Unless this is fine? – TheBeardedQuack Dec 13 '16 at 11:28
  • @SatsumaBenji What about callback hell - there are alternatives: promises, coroutines, async/await. Lambda terminates only when it reaches timeout. Just set high enough timeout in settings and it will wait for you callback. Sorry, but this discussion turns out to be out of the scope of the original question. Please create separate posts and close this one if it helped you to get data. – Vlad Holubiev Dec 13 '16 at 11:33
  • Just as a quick test in my main I put `return;` in the first line and tested... AWS says that execution completed in 3.38ms with result null, my timeout is set to 3000ms. If I reformat my code to include a long tree of nested callbacks for the database IO's, these will be executed when they need to be... But the main function will return while the IOs are pending and haven't called the callback yet. How will I make sure that AWS waits for the callback? – TheBeardedQuack Dec 13 '16 at 12:06
  • @SatsumaBenji here is a link to AWS Docs how to use callback in Lambda function - http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html?shortFooter=true#nodejs-prog-model-handler-callback Maybe don't try to deal with all the code, try with simple example first until you understand how it works. Then add the actual business logic. – Vlad Holubiev Dec 13 '16 at 13:33
  • I did read that when I was first going through the documentation too. It says if you return without calling `callback(...)` then it'll act as if the call was made with no arguments. But what happens if it takes X number of seconds for my nested callbacks to be completed and eventually the AWS callback with the result, when in the scope of the main function it'll have already returned because `return;` will be the only statement after connecting to the database because all code will be in callbacks from then on. – TheBeardedQuack Dec 13 '16 at 14:47
  • Well, still not sure how this can tell if a callback is used or not, but I've reformatted all of the code into this style and I'm now trying to debug it. At one point I have an error and where this is handled AWS is timing out. How am I supposed to correctly return? `return callback(err, result);` or `callback(err, result); return;` or something else? Thanks – TheBeardedQuack Dec 15 '16 at 13:59
  • `return` doesn't impact on callback at all. Just remove it. The way Lambda knows when to exit is invocation of `callback()` – Vlad Holubiev Dec 15 '16 at 14:12
  • It can't be removed in some cases... For example `if(err){ callback(err); return; } doStuff(); //If no return then this will always be run correct?` – TheBeardedQuack Dec 15 '16 at 14:43
  • @SatsumaBenji correct. Also you can put `doStuff()` in `else` statement – Vlad Holubiev Dec 15 '16 at 15:35
  • yeah I know that, we all have personal preference though... I'm testing my code currently and I'm running `callback(null, retObj); return;` and AWS is timing at at 3000ms, but I am calling the AWS callback with ~1600ms remaining on the clock. – TheBeardedQuack Dec 15 '16 at 16:02
  • @SatsumaBenji This is a common issue when you connect to database. This might solve you rproblem: http://stackoverflow.com/a/39153090/2727317 – Vlad Holubiev Dec 15 '16 at 16:08
  • Unfortunately that didn't help. Using a test object, the callback works fine before I'm connected to the database, but it's a database callback then AWS times out. – TheBeardedQuack Dec 15 '16 at 16:23
  • It does appear to be something with the database maybe? I wasn't calling `connection.end();` when I do that the callback works... However queries don't work, but after the first test I get the error message `Error: Cannot enqueue Handshake after invoking quit.`, my guess is that the DB isn't opening properly after being closed on the previous execution. I am not entirely familiar with the rules of SO but this has moved away from the original question so I will mark your answer as correct and would you mind messaging for any further help with the AWS callback/db issue? Thanks – TheBeardedQuack Dec 15 '16 at 16:43
  • @SatsumaBenji have you already moved db initialization out of each function invocation? Feel free to ask next question of SO, this is much better way of discussion programming problems than DM. – Vlad Holubiev Dec 15 '16 at 19:27
3

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);
  });
};
Unglückspilz
  • 1,820
  • 19
  • 25