Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(wip): instantiate sequelize once in lambda container #387

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 3 additions & 39 deletions src/server/bootstrap/index.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,11 @@
import express, { Express, Request, Response, NextFunction } from 'express'
import session from 'express-session'
import { Sequelize, SequelizeOptions } from 'sequelize-typescript'
import { Sequelize } from 'sequelize-typescript'
import SequelizeStoreFactory from 'connect-session-sequelize'
import bodyParser from 'body-parser'
import * as Sentry from '@sentry/node'
import * as Tracing from '@sentry/tracing'

// Sequelize-related imports
import {
databaseConfigType,
nodeEnvType,
} from '../../types/server/sequelize-config'
import * as sequelizeConfig from '../database/config/config'
import {
User,
Checker,
UserToChecker,
PublishedChecker,
Template,
} from '../database/models'

import minimatch from 'minimatch'
import { totp as totpFactory } from 'otplib'

Expand Down Expand Up @@ -48,26 +34,7 @@ const emailValidator = new minimatch.Minimatch(mailSuffix, {
nonegate: true,
})

export async function bootstrap(): Promise<{
app: Express
sequelize: Sequelize
}> {
// Create Sequelize instance and add models
const nodeEnv = config.get('nodeEnv') as nodeEnvType
const options: SequelizeOptions = (sequelizeConfig as databaseConfigType)[
nodeEnv
]

logger.info('Creating Sequelize instance and adding models')
const sequelize = new Sequelize(options)
sequelize.addModels([
User,
Checker,
UserToChecker,
PublishedChecker,
Template,
])

export async function bootstrap(sequelize: Sequelize): Promise<Express> {
const checker = new CheckerController({
service: new CheckerService({
logger,
Expand Down Expand Up @@ -161,10 +128,7 @@ export async function bootstrap(): Promise<{

app.use(Sentry.Handlers.errorHandler())

logger.info('Connecting to Sequelize')
await sequelize.authenticate()
return { app, sequelize }
return app
}

export { logger } from './logger'
export default bootstrap
37 changes: 37 additions & 0 deletions src/server/bootstrap/loadSequelize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Sequelize, SequelizeOptions } from 'sequelize-typescript'
import config from '../config'
import logger from './logger'
import {
databaseConfigType,
nodeEnvType,
} from '../../types/server/sequelize-config'
import * as sequelizeConfig from '../database/config/config'
import {
User,
Checker,
UserToChecker,
PublishedChecker,
Template,
} from '../database/models'

const loadSequelize = async () => {
const nodeEnv = config.get('nodeEnv') as nodeEnvType
const options: SequelizeOptions = (sequelizeConfig as databaseConfigType)[
nodeEnv
]

const sequelize = new Sequelize(options)
sequelize.addModels([
User,
Checker,
UserToChecker,
PublishedChecker,
Template,
])

logger.info('Connecting to Sequelize')
await sequelize.authenticate()
return sequelize
}

export default loadSequelize
39 changes: 39 additions & 0 deletions src/server/database/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,43 @@ const connectionConfig = {
database: parsed.database,
}

// Obtained from marcogrcr: https://github.com/sequelize/sequelize/pull/12642
// These only apply to the staging and production environment and not development because they are meant for lambda
const lambdaBestPracticesConfig = {
pool: {
/*
* Lambda functions process one request at a time but your code may issue multiple queries
* concurrently. Be wary that `sequelize` has methods that issue 2 queries concurrently
* (e.g. `Model.findAndCountAll()`). Using a value higher than 1 allows concurrent queries to
* be executed in parallel rather than serialized. Careful with executing too many queries in
* parallel per Lambda function execution since that can bring down your database with an
* excessive number of connections.
*
* Ideally you want to choose a `max` number where this holds true:
* max * EXPECTED_MAX_CONCURRENT_LAMBDA_INVOCATIONS < MAX_ALLOWED_DATABASE_CONNECTIONS * 0.8
*/
max: 2,
/*
* Set this value to 0 so connection pool eviction logic eventually cleans up all connections
* in the event of a Lambda function timeout.
*/
min: 0,
/*
* Set this value to 0 so connections are eligible for cleanup immediately after they're
* returned to the pool.
*/
idle: 0,
// Choose a small enough value that fails fast if a connection takes too long to be established.
acquire: 3000,
/*
* Ensures the connection pool attempts to be cleaned up automatically on the next Lambda
* function invocation, if the previous invocation timed out. This value is set to the default lambda timeout,
* which is 30 seconds.
*/
evict: 30000,
},
}

module.exports = {
development: {
storage: config.get('sqlitePath'),
Expand All @@ -25,12 +62,14 @@ module.exports = {
dialect: 'postgres',
seederStorage: 'sequelize',
...connectionConfig,
...lambdaBestPracticesConfig,
},
production: {
timezone: '+08:00',
logging: logger.info.bind(logger),
dialect: 'postgres',
seederStorage: 'sequelize',
...connectionConfig,
...lambdaBestPracticesConfig,
},
}
26 changes: 24 additions & 2 deletions src/server/serverless/api.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
import serverless, { Handler } from 'serverless-http'
import { Sequelize } from 'sequelize-typescript'

import bootstrap from '../bootstrap'
import logger from '../bootstrap/logger'
import loadSequelize from '../bootstrap/loadSequelize'

// This non-handler section runs
let sequelize: Sequelize | null = null

export const handler: Handler = async (event, context) => {
const { app, sequelize } = await bootstrap()
// re-use the sequelize instance across invocations to improve performance
if (!sequelize) {
sequelize = await loadSequelize()
} else {
// restart connection pool to ensure connections are not re-used across invocations
sequelize.connectionManager.initPools()

// restore `getConnection()` if it has been overwritten by `close()`
if (sequelize.connectionManager.hasOwnProperty('getConnection')) {
// @ts-ignore
// There is some jankiness in the Sequelize `connectionManager` module where `close()`
// overwrites the `getConnection()` method to throw an error.
// https://github.com/sequelize/sequelize/pull/12642#discussion_r613287823
delete sequelize.connectionManager.getConnection
}
}

const app = await bootstrap(sequelize)
const h = serverless(app)
try {
return await h(event, context)
} finally {
logger.info('Closing Sequelize connection')
await sequelize.close()
await sequelize.connectionManager.close()
}
}