dougmoscrop / serverless-http

Use your existing middleware framework (e.g. Express, Koa) in AWS Lambda 🎉
Other
1.72k stars 166 forks source link

How to integrate if you connect to MongoDB (or whatever non-AWS database) #41

Closed rclai closed 6 years ago

rclai commented 6 years ago

Typically on an HTTP Express setup you would connect to the database before you start listening on a port. But since each request requires a brand new Lambda request, integrating MongoDb requires a connection (possibly cached) to MongoDB per Lambda request.

What is the recommended place in this library to initialize this MongoDB connection before the request is handled?

I was thinking that the MongoDB connection could be injected into the request object via an Express middleware, but I'm not sure if it's a good idea.

dougmoscrop commented 6 years ago

Thanks for the question!

The first thing I would ask is that you consider how you would do this if you weren't using this library.

Within a Lambda, you have global code, and your handler code, so you can implement a sort of eager-initialization:

const thing = require('...');

// this runs as soon as Lambda starts up a container, and will persist to some degree as Lambda re-uses containers

const getConnection = new Promise((resolve, reject) => {
  const connection = thing.connect(...);
  connection.on('connected', () => resolve(connection));
});

module.exports.handler = (event, context, callback) => {
  // this is important, if you don't set it to false, your handler will never complete because the connection is open
  context.callbackWaitsForEmptyEventLoop = false;
  getConnection.then(connection => {
     callback(null, typeof connection);
  });
});

What's missing here is managing pool sizes (you probably don't want each Lambda to open a pool of multiple connections to your db) and handling connectivity issues (reconnecting if conn lost when lambda is put to sleep).

I can't say I have any good examples of this, since I've mostly just used Dynamo and Elasticsearch via the aws-sdk (which run on HTTP and seem resilient to connection hiccups).

But my point is, this library doesn't really fix or do any kind of magic for you there. You just want to

db = new DatabaseDriver(); // handle connection and other resiliency stuff in here
app.use((req, res, next) => {
  req.context.db = db.ensureConnected();
  next();
});
// elsewhere
app.get('/things', (req, res) => {
  db.runQuery();
});
albertomr86 commented 6 years ago

Hi @rclai

To do my my bit. This is how I've been doing.

main.js (expressjs app)

import express from 'express';
import connectDatabase, { register as registerModels } from './boot/mongodb';

// Register mongoose models.
registerModels();

// Configure the app.
const app = express();
app.use(connectDatabase);

// Routes
app.use('/', require('./routes').default);

// Expose.
export default app;

I use the middleware connectDatabase because I have to query on all my endpoints. But you could use the middleware at routing level.

Middleware


import mongoose from 'mongoose';

mongoose.Promise = require('bluebird');

// Reuse the connection.
let isConnected;

/**
 * Register all models to use:
 * let model = mongoose.model(<Model registered>)
 */
export const register = () => {
  try {
    require('/path/to/models/<user>');
  }
  catch(er) {
    // Ignore any error registering the models.
    // This is more likely in Dev mode after a Hot-Reload.
  }
};

/**
 * Connect to Mongodb.
 * @param done Callback to notify the result.
 * @returns {*}
 */
export const connect = (done) => {
  if (isConnected) {
    return done();
  }

  mongoose.connect(process.env.DB_DSN, (err) => {
    if (err) {
      return done(err);
    }

    isConnected = true;
    done();
  });
};

/**
 * Middleware to connect to database before execute a controller.
 * @param req  ExpressJS Request.
 * @param res  ExpressJS Response.
 * @param next Callback to notify when its done.
 */
export default (req, res, next) => {
  connect(next);
};
rclai commented 6 years ago

Thanks for the feedback guys.

dougmoscrop commented 6 years ago

I am going to close this based on the discussion, please re-open if you need more help.

gauravsavanur07 commented 5 years ago

{ "errorMessage": "Cannot find module './api/models/Restaurant'", "errorType": "Error", "stackTrace": [ "Function.Module._load (module.js:474:25)", "Module.require (module.js:596:17)", "require (internal/module.js:11:18)", "Object. (/var/task/handler.js:15:20)", "Module._compile (module.js:652:30)", "Object.Module._extensions..js (module.js:663:10)", "Module.load (module.js:565:32)", "tryModuleLoad (module.js:505:12)", "Function.Module._load (module.js:497:3)", "Module.require (module.js:596:17)", "require (internal/module.js:11:18)" ] }

gauravsavanur07 commented 5 years ago

i am getting this error on aws lambda

gauravsavanur07 commented 5 years ago

module.exports.hello = async (event) => { return { statusCode: 200, body: JSON.stringify({ message: 'Go Serverless v1.0! Your function executed successfully!', input: event, }), };

// Use this code if you don't use the http event with the LAMBDA-PROXY integration // return { message: 'Go Serverless v1.0! Your function executed successfully!', event }; }; const connectToDatabase = require('./db');

const Restaurant = require('./api/models/Restaurant');

module.exports.create = (event, context, callback) => { context.callbackWaitsForEmptyEventLoop = false;

connectToDatabase() .then(() => { Restaurant.create(JSON.parse(event.body)) .then(Restaurant => callback(null, { statusCode: 200, body: JSON.stringify(Restaurant) })) .catch(err => callback(null, { statusCode: err.statusCode || 500, headers: { 'Content-Type': 'text/plain' }, body: 'Could not create the Restaurant.' })); }); };

module.exports.getOne = (event, context, callback) => { context.callbackWaitsForEmptyEventLoop = false;

connectToDatabase() .then(() => { Restaurant.findById(event.pathParameters.id) .then(Restaurant => callback(null, { statusCode: 200, body: JSON.stringify(Restaurant) })) .catch(err => callback(null, { statusCode: err.statusCode || 500, headers: { 'Content-Type': 'text/plain' }, body: 'Could not fetch the Restaurant.' })); }); };

module.exports.getAll = (event, context, callback) => { context.callbackWaitsForEmptyEventLoop = false;

connectToDatabase() .then(() => { Restaurant.find() .then(Restaurants => callback(null, { statusCode: 200, body: JSON.stringify(Restaurants) })) .catch(err => callback(null, { statusCode: err.statusCode || 500, headers: { 'Content-Type': 'text/plain' }, body: 'Could not fetch the Restaurants.' })) }); };

module.exports.update = (event, context, callback) => { context.callbackWaitsForEmptyEventLoop = false;

connectToDatabase() .then(() => { Restaurant.findByIdAndUpdate(event.pathParameters.id, JSON.parse(event.body), { new: true }) .then(Restaurant => callback(null, { statusCode: 200, body: JSON.stringify(Restaurant) })) .catch(err => callback(null, { statusCode: err.statusCode || 500, headers: { 'Content-Type': 'text/plain' }, body: 'Could not fetch the Restaurants.' })); }); };

module.exports.delete = (event, context, callback) => { context.callbackWaitsForEmptyEventLoop = false;

connectToDatabase() .then(() => { Restaurant.findByIdAndRemove(event.pathParameters.id) .then(Restaurant => callback(null, { statusCode: 200, body: JSON.stringify({ message: 'Removed Restaurant with id: ' + Restaurant._id, Restaurant: Restaurant }) })) .catch(err => callback(null, { statusCode: err.statusCode || 500, headers: { 'Content-Type': 'text/plain' }, body: 'Could not fetch the Restaurants.' })); }); };