So I'm following the example here https://www.mongodb.com/blog/post/optimizing-aws-lambda-performance-with-mongodb-atlas-and-nodejs, to optimize my lambda functions.
I've tried two approaches and tested them locally using serverless-offline and both don't seem to work.
First Approach
// endpoint file
import {connectToDatabase} from "lib/dbUtils.js";
let cachedDb = null;
export function post(event, context, callback) {
let response;
context.callbackWaitsForEmptyEventLoop = false;
connectToDatabase()
.then(//do other stuff
// lib/dbUtils.js
export async function connectToDatabase() {
if (cachedDb && cachedDb.serverConfig.isConnected()) {
console.log(" using cached db instance");
return cachedDb;
}
cachedDb = await mongoose.createConnection(
process.env.DB_URL,
async err => {
if (err) {
throw err;
}
}
);
return cachedDb;
}
Second Approach
global.cachedDb = null;
export function post(event, context, callback) {
let response;
context.callbackWaitsForEmptyEventLoop = false;
connectToDatabase()
.then(connection => createUser(event.body, connection))
// lib/dbUtils.js
export async function connectToDatabase() {
// eslint-disable-next-line
if (global.cachedDb && global.cachedDb.serverConfig.isConnected()) {
// eslint-disable-next-line
console.log(" using cached db instance");
// eslint-disable-next-line
return global.cachedDb;
}
// eslint-disable-next-line
global.cachedDb = await mongoose.createConnection(
process.env.DB_URL,
async err => {
if (err) {
throw err;
}
}
);
// eslint-disable-next-line
return global.cachedDb;
}
In both cases the using cached db instance console log does not run.
Why does this not work? Is this because of serverless-offline?
The answer is simple: serverless-offline doesn't simulate the full AWS. Use the AWS console to to make a real Lambda
The MongoDB Atlas guide is OK, but it's also worth checking the official AWS Lambda documentation describing the context option in each lambda:
callbackWaitsForEmptyEventLoop – Set to false to send the response right away when the callback executes, instead of waiting for the Node.js event loop to be empty. If false, any outstanding events will continue to run during the next invocation.
It's possible to run your code on a real Lambda and see using cached db instance on the console. Since MongoDB's JavaScript code is fairly poor, I've written out my own version below:
var MongoClient = require("mongodb").MongoClient
let db = null
var log = console.log.bind(console)
var print = function(object) {
return JSON.stringify(object, null, 2)
}
// Use your own credentials (and better yet, put them in environment variables)
const password = `notactuallyapassword`
const uri = `mongodb+srv://lambdauser:${password}#fakedomain.mongodb.net/test?retryWrites=true`
exports.handler = function(event, context, callback) {
log(`Calling MongoDB Atlas from AWS Lambda with event: ${print(event)}`)
var document = JSON.parse(JSON.stringify(event))
const databaseName = "myDatabase",
collectionName = "documents"
// See https://www.mongodb.com/blog/post/optimizing-aws-lambda-performance-with-mongodb-atlas-and-nodejs
// and https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html#nodejs-prog-model-context-properties
context.callbackWaitsForEmptyEventLoop = false
return createDoc(databaseName, collectionName, document)
}
async function createDoc(databaseName, collectionName, document) {
var isConnected = db && db.serverConfig.isConnected()
if (isConnected) {
log(`Already connected to database, warm start!`)
} else {
log(`Connecting to database (cold start)`)
var client = await MongoClient.connect(uri)
db = client.db(databaseName)
}
var result = await db.collection(collectionName).insertOne(document)
log(`just created an entry into the ${collectionName} collection with id: ${result.insertedId}`)
// Don't close the connection thanks to context.callbackWaitsForEmptyEventLoop = false - this will re-use the connection on the next called (if it can re-use the same Lambda container)
return result
}
Use the Test button to run the lambda above twice in the AWS Lambda console.
The first time you run it you'll see Connecting to database (cold start)
The second time you'll see Already connected to database, warm start!
See the log output section in screenshot below:
Related
I tried to make function for my project in the service. This service need to check is user exists in my database, but my function(this function is checking) inside the class return undefined.
This is my code:
const express = require("express");
const mongoDB = require('mongodb').MongoClient;
const url = "here I paste url to my databse(everything is good here)";
class CheckService {
isUserExists(username) {
mongoDB.connect(url, (error, connection) => {
if (error) {
console.log("Error", '\n', error);
throw error;
}
const query = {name: username};
const db = connection.db("users");
const result = db.collection("users").find(query).toArray(
function findUser(error, result) {
if (error) {
throw error;
}
const arr = [];
if (result.value === arr.value) {
console.log(false);
connection.close();
return false;
} else {
console.log(true);
console.log(arr);
console.log(result);
connection.close();
return true;
}
});
console.log(result);
});
}
}
module.exports = new CheckService();
I imported my service to another service:
const checkService = require('./check.service');
After this, I invoked my function from my service like this:
console.log('function:',checkService.isUserExists(username));
I expected good result, but function doesn't return, that I want, unfortunately.
Please help!
There are two problems with your function
it doesn't return anything
toArray() is a promise, so your console.log probably just prints a promise.
Probably you're looking for something like this:
class CheckService {
async isUserExists(username) {
const connection = await mongoDB.connect(url);
const query = {name: username};
const db = connection.db("users");
const result = await db.collection("users").find(query).toArray();
connection.close();
return result.length !== 0;
}
}
You'd then invoke it with something like
await new CheckService().isUserExists(username);
Though, it's worth noting that you probably don't need toArray and instead could use findOne or even count() since all you care about is existence. I probably also wouldn't instantiate a new connection each time (but that's not super relevant)
2 things wrong here. Firstly the first comment is correct. You're only logging the result and not passing back to the caller. Secondly, a quick peek at the mongo docs shows that retrieval methods are promises. You need to make the function async and add awaits where needed.
Mongo:
https://www.mongodb.com/docs/drivers/node/current/fundamentals/crud/read-operations/retrieve/
Promises:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
Here's a simple script i made:
const Nightmare = require('nightmare');
const sql = require('mssql');
const itens = getRecords();
async function getRecords(){
let itensList = [];
const cfg = {
//config here
};
sql.connect(cfg, function(err){
if(err) console.log(err);
let request = new sql.Request();
request.query("<query here>", (err, result) => {
if(err) console.log(err);
itensList = result;
});
return itensList;
});
}
async function getPrices(){
try{
console.log(itens)
}catch(e){
console.log(e);
}
}
getPrices();
Everything works, but when the getPrices() function gets called, here's what's being logged:
Promise { undefined }
What am i missing here?
request.query is being called, but itenslist is being returned before it can be assigned.
Basically, the order of what is happening is:
request.query is called, and starts running the query.
Since request.query is asynchronous, we move on to the next task - returning itenlist
request.query finishes running, and assigns itenlist the expected value after it has already been returned.
To get your desired functionality, I would recommend using callbacks (which node-mssql supports). Alternatively, you could use the await keyword. For instance:
var queryText = 'SELECT 1 AS Value';
var queryResults = await connection.query(queryText);
I use the mssql (https://www.npmjs.com/package/mssql) module for my database. Normally I use postgres databases which lead to pg (https://www.npmjs.com/package/pg).
I want to setup prepared statements for the mssql database. When using the pg module it's quite easy.
This is how I do it with pg:
I setup my databaseManager
const { Pool } = require('pg');
const db = require('../config/database.js');
const pool = new Pool(db);
function queryResponse(result, err) {
return {
result,
err
};
}
module.exports = async (text, values) => {
try {
const result = await pool.query(text, values);
return queryResponse(result.rows, null);
} catch (err) {
return queryResponse(null, err);
}
};
and whenever I want to query the database I can call this module and pass in my statement and values. An example for todo apps would be
todos.js (query file)
const db = require('../databaseManager.js');
module.exports = {
getAllTodosFromUser: values => {
const text = `
SELECT
id,
name,
is_completed AS "isCompleted"
FROM
todo
WHERE
owner_id = $1;
`;
return db(text, values);
}
};
I wanted to create an mssql equivalent. From the docs I see that the module differs from the pg module.
I changed my databaseManager to
const sql = require('mssql');
const config = require('../config/database.js');
const pool = new sql.ConnectionPool(config).connect();
module.exports = async (queryBuilder) => {
try {
const preparedStatement = await new sql.PreparedStatement(pool);
return queryBuilder(sql, preparedStatement, async (query, values) => {
await preparedStatement.prepare(query);
const result = await preparedStatement.execute(values);
await preparedStatement.unprepare();
return {
result: result.rows,
err: null
};
});
} catch (err) {
return {
result: null,
err
}
}
};
and my query file would pass in the required parameters for the preparedStatement object
const db = require('../databaseManager.js');
module.exports = {
getUserByName: username => db((dataTypes, statementConfigurator, processor) => {
statementConfigurator.input('username', dataTypes.VarChar);
const query = `
SELECT
*
FROM
person
WHERE
username = #username;
`;
return processor(query, { username });
})
};
I was hoping that this approach would return the desired result but I get the error
this.parent.acquire is not a function
and don't know if my code is wrong. If it is, how can I setup my prepared statements correctly?
Edit:
I just found out that the error comes from this line of code
await preparedStatement.prepare(query);
but I think I took it correctly from the docs
https://tediousjs.github.io/node-mssql/#prepared-statement
I thought this question deserved a little bit more explanation than the answer what OP gave. The solution is no different than what OP already answered.
The issue remains same, pool mustn't have resolved from its promise pending state. So it just has to be awaited.
module.exports = async queryBuilder => {
try {
await pool; // Waiting for pool resolve from promise pending state.
const preparedStatement = await new sql.PreparedStatement(pool);
// ..
} catch (err) {
// ..
}
};
When you try to build a prepared statement, you pass the pool as an argument to its constructor. In its constructor is the below line
this.parent = parent || globalConnection
After which when you prepare the statement the flow leads to this line which would cause the issue since at that time this.parent's value was still a promise which was yet to be resolved.
this.parent.acquire(this, (err, connection, config) => {
Building an Alexa skill that requires persistence.
I am calling the database to get the user details prior to managing the flow and I'm struggling to get Node to wait for the response from DynamoDB (using Dynasty.js to handle the db connectivity).
I've tried a whole host of different promise/callback/node "blocking"/async approaches and the best I can do is have the response (in CloudWatch logs) appear when the user quits the skill. I'd really like the user configuration to happen at the start of the process, rather than at the end!
var credentials = {
accessKeyId: process.env.MY_ACCESS_KEY_ID,
secretAccessKey: process.env.MY_SECRET_ACCESS_KEY
};
var dynasty = require('dynasty')(credentials);
var tableName = dynasty.table('dynamoTable');
const promiseFunc = new Promise((resolve,reject)=>{
var myUser = tableName.find(userId);
setTimeout(_=>{resolve(myUser)}, 2000);
});
var checkUser = async function(){
if (true) {
console.log('check, check, 1, 2, 3');
await promiseFunc.then(function(myUser) {
if (myUser) {
console.log("USER FOUND");
console.log(myUser);
} else {
console.log("no user! sigh deeply and start again");
}
})
}
console.log("do more stuff now");
}
exports.handler = function(event, context) {
userId = event.session.user.userId;
const alexa = Alexa.handler(event, context);
// only check the first time, and only once the userId is properly constructed
if (!(userId.startsWith('amzn1.ask.account.testUser')) && (checkedUser != 1)) {
checkedUser = 1;
checkUser();
}
alexa.resources = languageString;
alexa.registerHandlers(newSessionHandlers, startStateHandlers, triviaStateHandlers, helpStateHandlers, stopStateHandlers);
alexa.execute();
};
How do I get node to wait for the response from dynamoDB before the rest of the script runs...
This is an old-ish link, but according to it, you can get a promise from DynamoDB with the .promise() method. With that in mind...
function findUser(userId) {
// initialize tableName somehow
return tableName.find(userId)
}
async function checkUser() {
// initialize userId somehow
let myUser = await findUser(userId);
console.log(`found user ${myUser}`);
return myUser;
}
Finally, remember to await on your call to checkUser()...
if (/* long condition */) {
checkedUser = 1;
let myUser = await checkUser();
}
I have a question regarding an express based node.js application that is dependent on the first require(). This is my first node.js application. The first require() hits AWS ec2 parameter store to gather credentials for a database. I can not make a connection to the database until this require resolves in an asynchronous manner.
The best way I've found to do this is to export a callback and wrap the rest of the require() statements in the callback of the first require(). Is this a bad practice?
//app.js
var appConfig = require('./config/appconfig');
appConfig.fetchAppConfig(function(err, result) {
if(err) {
console.log(err);
console.error("Server failed to startup. Config parameters not available.");
}
else {
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
...
app.use(bodyParser.json());
etc
...
//appConfig.js
module.exports = function fetchAppConfig(callback) {
getCredentials(function(err, result) {
if(err) {
console.log(err);
callback(err);
} else {
awsLogin.paramStoreService(result).then(
data => {
appConfig = decodeAppConfig(data.Parameter.Value);
callback(null, appConfig);
}
).catch(
error => {
console.error(error);
callback(err);
}
)
}
})
}
Am I missing a more simple option?
Would I be better served to have this logic to pull the configuration somewhere in the deployment code?
I would define a couple of functions, one to request the credentials and another to connect to the database once the credentials are retrieved. You could use the async module's series function that will easily allow you to control the flow of your application.
From the documentation:
Run the functions in the tasks collection in series, each one running
once the previous function has completed. If any functions in the
series pass an error to its callback, no more functions are run, and
callback is immediately called with the value of the error. Otherwise,
callback receives an array of results when tasks have completed.
Here's an example:
var async = require('async');
function getCredentials(callback) {
callback(null, {
user: 'hello',
pass: 'world',
});
};
function connectToDatabase(callback, creds) {
console.log('Connecting to database => ' + JSON.stringify(creds));
callback(null, 'Done');
};
async.series([
getCredentials,
connectToDatabase,
],
function(err, results) {
console.error(err);
console.log(results);
});
Until node supports top-level awaits, below is the solution I'm using for this exact use-case.
// index.js
(async () => {
await require('./config').initialize();
require('./app');
})();
// config.js
const _ = require('lodash');
const secretKeys = ['secret1', 'secret2'];
const parameterHierarchyPrefix = `/${process.env.NODE_ENV}/app/`;
const getParamNameWithoutHierarchy = name => _.replace(name, new RegExp(`${parameterHierarchyPrefix}(.*)`), '$1');
const config = {};
config.initialize = async (callback = () => {}) => {
try {
// initialize aws sdk and ssm
const AWS = require('aws-sdk');
AWS.config.update({
region: 'us-west-2',
accessKeyId: S3_ACCESS_KEY,
secretAccessKey: S3_SECRET,
});
const ssm = new AWS.SSM();
// map secret keys to lowercase
const secretNames = _.map(secretKeys, key => `${parameterHierarchyPrefix}key`);
// this api only allows fetching 10 params per call
const secretFetchBatches = _.chunk(secretNames, 10);
// fetch secrets from aws parameter store
let secrets = {};
for (let i = 0; i < _.size(secretFetchBatches); i += 1) {
const parameters = await ssm.getParameters({
Names: secretFetchBatches[i],
WithDecryption: true,
}).promise();
secrets = {
...secrets,
..._.zipObject(_.map(parameters.Parameters, ({ Name }) => ([getParamNameWithoutHierarchy(Name)])), _.map(parameters.Parameters, 'Value')),
};
}
// write the secrets into the config object
_.each(secrets, (v, k) => constants[k] = v);
// invoke the callback
callback();
} catch (e) {
throw e
}
};
module.exports = config;