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: handle database overload gracefully #362

Merged
merged 1 commit into from
Aug 30, 2023
Merged
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
87 changes: 58 additions & 29 deletions src/database/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import retry from 'async-retry'
import { getConfig } from '../config'
import { DbActiveConnection, DbActivePool } from '../monitoring/metrics'
import { StorageBackendError } from '../storage'
import KnexTimeoutError = knex.KnexTimeoutError

// https://github.com/knex/knex/issues/387#issuecomment-51554522
pg.types.setTypeParser(20, 'text', parseInt)
Expand Down Expand Up @@ -131,41 +132,69 @@ export class TenantConnection {
}

async transaction(instance?: Knex) {
const tnx = await retry(
async (bail) => {
try {
const pool = instance || this.pool
return await pool.transaction()
} catch (e) {
if (
e instanceof DatabaseError &&
e.code === '08P01' &&
e.message.includes('no more connections allowed')
) {
throw e
try {
const tnx = await retry(
async (bail) => {
try {
const pool = instance || this.pool
return await pool.transaction()
} catch (e) {
if (
e instanceof DatabaseError &&
e.code === '08P01' &&
e.message.includes('no more connections allowed')
) {
throw e
}

bail(e as Error)
return
}

bail(e as Error)
return
},
{
minTimeout: 50,
maxTimeout: 200,
maxRetryTime: 3000,
retries: 10,
}
},
{
minTimeout: 50,
maxTimeout: 200,
maxRetryTime: 3000,
retries: 10,
)

if (!tnx) {
throw new StorageBackendError('Could not create transaction', 500, 'transaction_failed')
}
)

if (!tnx) {
throw new StorageBackendError('Could not create transaction', 500, 'transaction_failed')
}
if (!instance && this.options.isExternalPool) {
await tnx.raw(`SELECT set_config('search_path', ?, true)`, [searchPath.join(', ')])
}

if (!instance && this.options.isExternalPool) {
await tnx.raw(`SELECT set_config('search_path', ?, true)`, [searchPath.join(', ')])
}
return tnx
} catch (e) {
if (e instanceof KnexTimeoutError) {
throw StorageBackendError.withStatusCode(
'database_timeout',
544,
'The connection to the database timed out',
e
)
}

return tnx
if (
e instanceof DatabaseError &&
[
'remaining connection slots are reserved for non-replication superuser connections',
'no more connections allowed',
].some((msg) => (e as DatabaseError).message.includes(msg))
) {
throw StorageBackendError.withStatusCode(
'too_many_connections',
429,
'Too many connections issued to the database',
e
)
}

throw e
}
}

transactionProvider(instance?: Knex): Knex.TransactionProvider {
Expand Down
7 changes: 6 additions & 1 deletion src/http/error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,18 @@

if (isRenderableError(error)) {
const renderableError = error.render()
return reply.status(renderableError.statusCode === '500' ? 500 : 400).send(renderableError)
const statusCode = error.userStatusCode
? error.userStatusCode
: renderableError.error === '500'
? 500
: 400
return reply.status(statusCode).send(renderableError)
}

// Fastify errors
if ('statusCode' in error) {
const err = error as FastifyError
return reply.status((error as any).statusCode || 500).send({

Check warning on line 34 in src/http/error-handler.ts

View workflow job for this annotation

GitHub Actions / Test / OS ubuntu-20.04 / Node 18

Unexpected any. Specify a different type
statusCode: `${err.statusCode}`,
error: err.name,
message: err.message,
Expand Down
14 changes: 14 additions & 0 deletions src/storage/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type StorageError = {
* that we want to display to our users
*/
export interface RenderableError {
userStatusCode?: number
render(): StorageError
getOriginalError(): unknown
}
Expand All @@ -38,16 +39,29 @@ export function isS3Error(error: unknown): error is S3ServiceException {
export class StorageBackendError extends Error implements RenderableError {
httpStatusCode: number
originalError: unknown
userStatusCode: number

constructor(name: string, httpStatusCode: number, message: string, originalError?: unknown) {
super(message)
this.name = name
this.httpStatusCode = httpStatusCode
this.userStatusCode = httpStatusCode === 500 ? 500 : 400
this.message = message
this.originalError = originalError
Object.setPrototypeOf(this, StorageBackendError.prototype)
}

static withStatusCode(
name: string,
statusCode: number,
message: string,
originalError?: unknown
) {
const error = new StorageBackendError(name, statusCode, message, originalError)
error.userStatusCode = statusCode
return error
}

static fromError(error?: unknown) {
let name: string
let httpStatusCode: number
Expand Down
Loading