4

I am having quite a number of issues using pg-promise with AWS lambda. I wanted to understand how do I solve these issues.

The library advises you to create one instance of the Database object and then export it from a module. There should only one instance of the object created. Something like:

const db = pgp({
  host: process.env.DATABASE_HOST,
  port: process.env.DATABASE_PORT,
  database: process.env.DATABASE_NAME,
  user: process.env.DATABASE_USERNAME,
  password: process.env.DATABASE_PASSWORD,
  poolSize: 0,
  poolIdleTimeout: 10,
});
module.exports = db;

I understand that this just an object and no connection is created here. A connection will be created lazily when you run anything on this db object such as a db.query().

Since we have put in the pool size as 0, there would only ever be one connection created. Which is what we need as at the start of every lambda function we need to create a connection and then close the connection when the lambda finishes.

The issues we are facing are:

  1. How do we release a connection? AWS lambdas re-use containers. Meaning it will call on the same node code that is already initialised and re-run the same function if a call to the lambda happens soon after a prior run. This means that the db object will be the same for the next call of the lambda. After our first lambda completes, if we call pgp.end() the documentation says the connection pool closes. It also says we cant use the pgp library in the same process after that. But the library will be used as the db object is still alive and will be used in a subsequent run.

  2. How do we retry getting a new connection?
    The other issue that AWS lambda poses, is that when you run a lambda within a VPC and your Postgres instance is also running inside a VPC, it takes time for the postgres database's DNS to resolve. So if you try to connect you might get an ENOTFOUND error. The advice from AWS is to retry acquiring the connection. Using the pg-promise how do we retry acquiring a connection?

The way I would like to implement it is:

module.exports.handler = (event, context, callback) => {
 let connection;
 try {
  connection = /*gets connection and retries if it failed the first time*/
  // run db queries and transactions.. etc.
  callback(null, result);
 } finally {
  connection.close();
 }

}
Dave Gray
  • 715
  • 5
  • 11
sethu
  • 8,181
  • 7
  • 39
  • 65
  • @setu would you plz share what was the final solution you adopted? Thanks. – mythicalcoder Jan 08 '18 at 16:47
  • 1
    @mythicalcoder - sorry for the late response. I have written an answer below for how we landed up doing this – sethu Jan 15 '18 at 11:10
  • See the most recent chat on `pg-promise` (chat link): https://gitter.im/vitaly-t/pg-promise?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge – vitaly-t Jan 19 '18 at 13:32

2 Answers2

4

This is what we landed up doing.

The jist is create a new connection before the start of every lambda and then close it before returning back from the lambda

// your lambda entry point
module.exports.handler = (event, context, callback) =>  
getConnection(async (connection) => {
    let result;
    try {
        // work with your connection
    } catch (error) {
    }
    callback(null, result);
})


// db connection 

const getConnection = async (callback) => {
const dbConnection = new DBConnection();
try {
    const connection = await dbConnection.create();
    await callback(connection);
} finally {
    dbConnection.close();
}
};

const MAX_RETRY = 3;

const options = {
// global event notification;
error: (error, e) => {
    if (e.cn) {
    // A connection-related error;
    //
    // Connections are reported back with the password hashed,
    // for safe errors logging, without exposing passwords.
    logger.error('CN:', e.cn);
    logger.error('EVENT:', error.message || error);
    }
},
};

const pgp = require('pg-promise')(options);

const connectionParams = {
host: process.env.DATABASE_HOST,
port: process.env.DATABASE_PORT,
database: process.env.DATABASE_NAME,
user: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
poolSize: 0,
poolIdleTimeout: 10,
};

const db = pgp(connectionParams);

class DBConnection {

async create() {
    let retry = 0;
    while (retry < MAX_RETRY) {
    try {
        logger.debug(`Acquiring a new DB connection Attempt: ${retry}/${MAX_RETRY}`);
        this.connection = await db.connect({ direct: true });
        break;
    } catch (error) {
        logger.error(`Error occurred while getting DB connection ${error}. Retrying ${retry}/${MAX_RETRY}`);
        retry += 1;
    }
    }

    if (!this.connection) {
    throw Error(`Unable to obtain DB connection after ${MAX_RETRY} retries`);
    }

    return this.connection;
}

close() {
    if (this.connection) {
    logger.debug('Closing DB Connection');
    this.connection.done();
    }
}
}
sethu
  • 8,181
  • 7
  • 39
  • 65
3

How do we release a connection?

You don't. Connections are communicated with the connection pool automatically. Once a query ends executing, the connection goes back into the pool, to be available for the next query that requests it.

When executing more than one query at a time you should use tasks. See Chaining Queries.

How do we retry getting a new connection?

Connection pool creates a new physical connection automatically, when it is necessary, according to the connection demand and the maximum pool size. When a connection goes down, it is automatically re-created.


If a single connection is all you've got within AWS lambda, then maybe the best pattern for you is to create and maintain a single global connection?

If that's the case, the Robust Listeners example might be of use to you. It shows how to maintain a single global connection outside of the connection pool, and how to keep it alive at all times.

But that's more like the last resort. I believe simply by using the automatic connection pool should be sufficient.

vitaly-t
  • 24,279
  • 15
  • 116
  • 138
  • Thanks for the answer. Should I be calling pgp.end() then at the end of the lambda invocation? What happens exactly during a pgp.end()? There are issues in sharing a connection across different invocations of a lambda.This is the issue with using a connection pool in a lambda. The connection pool might still have a connection in it. But the node process is in a 'frozen' state after a lambda completes. This probably means the socket between the lambda and the db is probably closed as well. – sethu Jul 04 '17 at 03:30
  • Is your issue across multiple Node.js processes? [What happens when calling `pgp.end()`](http://vitaly-t.github.io/pg-promise/module-pg-promise.html#~end) - is well documented ;) – vitaly-t Jul 04 '17 at 03:32
  • My primary issue is the DNS resolution of the DB. When my lambda spins up, it tries to execute a query, the library tries to get a connection, it can't find it yet as the DNS has not replicated yet. If it retries getting the connection again in a couple of seconds it will work. I am not sure how to go about doing that with the connection pool implementation. – sethu Jul 04 '17 at 03:36
  • If I try using a single global connection approach, I am not sure how to use transactions on that single connection object. – sethu Jul 04 '17 at 03:37
  • That is very awkward indeed, if you need multiple attempts to acquire the connection. I've been thinking about adding such feature right to the library, but you still would have to use method [connect](http://vitaly-t.github.io/pg-promise/Database.html#connect), which isn't a great solution either way. – vitaly-t Jul 04 '17 at 03:45
  • I am thinking of using the raw pg library and using the client instead of the pool. I can retry the connection using client.connect() I think. I will lose out on the transaction and query abstraction you have built which is the reason I wanted to use pg-promise. Not sure what else to do here. – sethu Jul 04 '17 at 03:53
  • Using `db.connect({direct: true})` would be identical, except you get access to all the high-level API of `pg-promise`. See the [API](http://vitaly-t.github.io/pg-promise/Database.html#connect). Creating a `Client` outside of the pool is exactly what the method in that case does. – vitaly-t Jul 04 '17 at 03:57
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/148266/discussion-between-sethu-and-vitaly-t). – sethu Jul 04 '17 at 03:57
  • [Version 6.3.1](https://github.com/vitaly-t/pg-promise/releases/tag/v.6.3.1) brought in the change that should help you ;) – vitaly-t Jul 04 '17 at 05:25