Skip to content

Commit

Permalink
feat: allow overriding init fail handling (#1342)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjie committed Sep 8, 2020
1 parent 0420c95 commit b06142a
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 23 deletions.
6 changes: 4 additions & 2 deletions src/interfaces.ts
Expand Up @@ -59,9 +59,11 @@ export interface PostGraphileOptions<
// there are fatal naming conflicts in the schema). When true, PostGraphile
// will keep trying to rebuild the schema indefinitely, using an exponential
// backoff between attempts, starting at 100ms and increasing up to 30s delay
// between retries.
// between retries. When a function, the function will be called passing the
// error and the number of attempts, and it should return true to retry,
// false to permanently abort trying.
/* @middlewareOnly */
retryOnInitFail?: boolean;
retryOnInitFail?: boolean | ((error: Error, attempts: number) => boolean | Promise<boolean>);
// Connection string to use to connect to the database as a privileged user (e.g. for setting up watch fixtures, logical decoding, etc).
ownerConnectionString?: string;
// Enable GraphQL websocket transport support for subscriptions (you still need a subscriptions plugin currently)
Expand Down
Expand Up @@ -866,7 +866,7 @@ export default function createPostGraphileHttpRequestHandler(

// Overwrite entire response
returnArray = false;
results = [{ errors: [error] }];
results = [{ errors: (handleErrors as any)([error], req, res) }];

// If the status code is 500, let鈥檚 log our error.
if (res.statusCode === 500)
Expand Down
95 changes: 75 additions & 20 deletions src/postgraphile/postgraphile.ts
@@ -1,6 +1,6 @@
import { Pool, PoolConfig } from 'pg';
import { IncomingMessage, ServerResponse } from 'http';
import { GraphQLSchema } from 'graphql';
import { GraphQLSchema, GraphQLError } from 'graphql';
import { EventEmitter } from 'events';
import { createPostGraphileSchema, watchPostGraphileSchema } from 'postgraphile-core';
import createPostGraphileHttpRequestHandler from './http/createPostGraphileHttpRequestHandler';
Expand Down Expand Up @@ -43,7 +43,20 @@ export function getPostgraphileSchemaBuilder<
pgPool: Pool,
schema: string | Array<string>,
incomingOptions: PostGraphileOptions<Request, Response>,
release: null | (() => void) = null,
): PostgraphileSchemaBuilder {
let released = false;
function releaseOnce() {
if (released) {
throw new Error(
'Already released this PostGraphile schema builder; should not have attempted a second release',
);
}
released = true;
if (release) {
release();
}
}
if (incomingOptions.live && incomingOptions.subscriptions == null) {
// live implies subscriptions
incomingOptions.subscriptions = true;
Expand Down Expand Up @@ -117,20 +130,58 @@ export function getPostgraphileSchemaBuilder<
} catch (error) {
attempts++;
const delay = Math.min(100 * Math.pow(attempts, 2), 30000);
const exitOnFail = !options.retryOnInitFail;
// If we fail to build our schema, log the error and either exit or retry shortly
logSeriousError(
error,
'building the initial schema' + (attempts > 1 ? ` (attempt ${attempts})` : ''),
exitOnFail
? 'Exiting because `retryOnInitFail` is not set.'
: `We'll try again in ${delay}ms.`,
);
if (exitOnFail) {
process.exit(34);
if (typeof options.retryOnInitFail === 'function') {
const start = process.hrtime();
try {
const retry = await options.retryOnInitFail(error, attempts);
const diff = process.hrtime(start);
const dur = diff[0] * 1e3 + diff[1] * 1e-6;
if (!retry) {
releaseOnce();
throw error;
} else {
if (dur < 50) {
// retryOnInitFail didn't wait long enough; use default wait.
console.error(
`Your retryOnInitFail function should include a delay before resolving; falling back to a ${delay}ms wait (attempts = ${attempts}) to avoid overwhelming the database.`,
);
await sleep(delay);
}
}
} catch (e) {
throw Object.defineProperties(
new GraphQLError(
'Failed to initialize GraphQL schema.',
undefined,
undefined,
undefined,
undefined,
e,
),
{
status: {
value: 503,
enumerable: false,
},
},
);
}
} else {
const exitOnFail = !options.retryOnInitFail;
// If we fail to build our schema, log the error and either exit or retry shortly
logSeriousError(
error,
'building the initial schema' + (attempts > 1 ? ` (attempt ${attempts})` : ''),
exitOnFail
? 'Exiting because `retryOnInitFail` is not set.'
: `We'll try again in ${delay}ms.`,
);
if (exitOnFail) {
process.exit(34);
}
// Retry shortly
await sleep(delay);
}
// Retry shortly
await sleep(delay);
}
}
}
Expand Down Expand Up @@ -207,7 +258,7 @@ export default function postgraphile<

// Do some things with `poolOrConfig` so that in the end, we actually get a
// Postgres pool.
const pgPool = toPgPool(poolOrConfig);
const { pgPool, release } = toPgPool(poolOrConfig);

pgPool.on('error', err => {
/*
Expand All @@ -231,6 +282,7 @@ export default function postgraphile<
pgPool,
schema,
incomingOptions,
release,
);
return createPostGraphileHttpRequestHandler({
...(typeof poolOrConfig === 'string' ? { ownerConnectionString: poolOrConfig } : {}),
Expand Down Expand Up @@ -269,21 +321,24 @@ function constructorName(obj: mixed): string | null {
}

// tslint:disable-next-line no-any
function toPgPool(poolOrConfig: any): Pool {
function toPgPool(poolOrConfig: any): { pgPool: Pool; release: null | (() => void) } {
if (quacksLikePgPool(poolOrConfig)) {
// If it is already a `Pool`, just use it.
return poolOrConfig;
return { pgPool: poolOrConfig, release: null };
}

if (typeof poolOrConfig === 'string') {
// If it is a string, let us parse it to get a config to create a `Pool`.
return new Pool({ connectionString: poolOrConfig });
const pgPool = new Pool({ connectionString: poolOrConfig });
return { pgPool, release: () => pgPool.end() };
} else if (!poolOrConfig) {
// Use an empty config and let the defaults take over.
return new Pool({});
const pgPool = new Pool({});
return { pgPool, release: () => pgPool.end() };
} else if (isPlainObject(poolOrConfig)) {
// The user handed over a configuration object, pass it through
return new Pool(poolOrConfig);
const pgPool = new Pool(poolOrConfig);
return { pgPool, release: () => pgPool.end() };
} else {
throw new Error('Invalid connection string / Pool ');
}
Expand Down

0 comments on commit b06142a

Please sign in to comment.