CodeGenieApp / serverless-express

Run Express and other Node.js frameworks on AWS Serverless technologies such as Lambda, API Gateway, Lambda@Edge, and more.
https://codegenie.codes
Apache License 2.0
5.13k stars 668 forks source link

NestJS + PG Aurora Serverless cold start #666

Closed lays147 closed 8 months ago

lays147 commented 9 months ago

Hi, I'm looking for guidance on a specific issue when using a full serverless infrastructure. I have an AWS Lambda behind an ALB that uses a Postgres Aurora Serverless, where I use Knex as the db manager.

From time to time, on the first request of the day, I get a "timeout expired" error from the connection to the database. I'm assuming that the serverless db has some cold start on itself, but the lambda explodes with this error, and does not return a proper request response to the client, where the client has an Internal Server Error response.

What I would like to know is how one can handle this error. In the async bootstrap example we have a function that simulates a connection to the database, but it only handles the “everything works” path. How can I handle the “not working” path when I have issues connecting to the database? Or any issues in the bootstrap flow?

nestApp.ts

export const nestApp = async () => {
  const logger = WinstonModule.createLogger(winstonConfig);

  if (environment === 'production') {
    logger.log('Initializing RDS connection');

    await setupRDSConn();

    logger.log('RDS connection completed');

    reInitDBConn();
  }

  const app = await NestFactory.create(AppModule, { logger });

  app.useGlobalPipes(new ValidationPipe({ transform: true }));
  setupSwagger(app);
  app.enableCors();
  return app;
};

lambda.ts

import serverlessExpress from '@vendia/serverless-express';
import { Callback, Context, Handler } from 'aws-lambda';

import { nestApp } from './nestApp';

let cachedServer: Handler;

async function bootstrap(): Promise<Handler> {
  const app = await nestApp();

  await app.init();

  const expressApp = app.getHttpAdapter().getInstance();
  return serverlessExpress({ app: expressApp });
}

export const handler: Handler = async (
  event: any,
  context: Context,
  callback: Callback,
) => {
  cachedServer = cachedServer ?? (await bootstrap());
  return cachedServer(event, context, callback);
};
brettstack commented 9 months ago

Hi @lays147 in this case you should catch the error and respond with the response format expected by ALB, which I believe is this format:

{
    statusCode,
    body,
    headers,
    multiValueHeaders,
    isBase64Encoded
  }
lays147 commented 9 months ago

@brettstack yeah, but where can I catch the error? If I return the json instead of the app on the nestApp.ts, handling the error inside the if condition, won't that be a problem for the serverless-express?

brettstack commented 9 months ago

Maybe something like this:

try {
  const app = await nestApp();

  await app.init();

  const expressApp = app.getHttpAdapter().getInstance();
  return serverlessExpress({ app: expressApp });
} catch(error) {
  return {
    statusCode,
    body,
    headers,
    multiValueHeaders,
    isBase64Encoded
  }
}
lays147 commented 9 months ago

Amazon Q gave me a hand and suggested doing in the handler func:

export const handler: Handler = async (
  event: any,
  context: Context,
  callback: Callback,
) => {
  try {
    cachedServer = cachedServer ?? (await bootstrap());
    return cachedServer(event, context, callback);
  } catch (error) {
    callback(null, {
      statusCode: 502,
      body: JSON.stringify({
        error: 'Initialization process failed.',
        message: error.message,
      }),
      headers: {
        'Content-Type': 'application/json',
      },
    });
  }
};

I'll try this and get back to this issue.