From 441b71fdab36a9eb79336d720d15d0d1cf7f0156 Mon Sep 17 00:00:00 2001 From: Valentin Cocaud Date: Mon, 18 Mar 2024 15:33:40 +0100 Subject: [PATCH] Dispose hive client on signal termination for yoga and envelop (#4109) Co-authored-by: Kamil Kisiela --- .changeset/long-frogs-deliver.md | 5 +++++ packages/libraries/client/src/apollo.ts | 10 +++++++--- packages/libraries/client/src/client.ts | 2 ++ packages/libraries/client/src/envelop.ts | 18 +++++++++++++++++- .../libraries/client/src/internal/types.ts | 10 +++++++++- packages/libraries/client/src/yoga.ts | 18 +++++++++++++++++- 6 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 .changeset/long-frogs-deliver.md diff --git a/.changeset/long-frogs-deliver.md b/.changeset/long-frogs-deliver.md new file mode 100644 index 000000000..877ac5b38 --- /dev/null +++ b/.changeset/long-frogs-deliver.md @@ -0,0 +1,5 @@ +--- +"@graphql-hive/client": minor +--- + +Automatic disposal of managed clients for Yoga and Envelop plugins. diff --git a/packages/libraries/client/src/apollo.ts b/packages/libraries/client/src/apollo.ts index c7e2d857a..93e30f0bc 100644 --- a/packages/libraries/client/src/apollo.ts +++ b/packages/libraries/client/src/apollo.ts @@ -2,7 +2,7 @@ import { createHash } from 'crypto'; import axios from 'axios'; import type { DocumentNode } from 'graphql'; import type { ApolloServerPlugin } from '@apollo/server'; -import { createHive } from './client.js'; +import { autoDisposeSymbol, createHive } from './client.js'; import type { HiveClient, HivePluginOptions, @@ -263,7 +263,9 @@ export function hiveApollo(clientOrOptions: HiveClient | HivePluginOptions): Apo if (isLegacyV0) { return { async serverWillStop() { - await hive.dispose(); + if (hive[autoDisposeSymbol]) { + await hive.dispose(); + } }, } as any; } @@ -272,7 +274,9 @@ export function hiveApollo(clientOrOptions: HiveClient | HivePluginOptions): Apo return Promise.resolve({ async serverWillStop() { - await hive.dispose(); + if (hive[autoDisposeSymbol]) { + await hive.dispose(); + } }, schemaDidLoadOrUpdate(schemaContext) { if (ctx.schema !== schemaContext.apiSchema) { diff --git a/packages/libraries/client/src/client.ts b/packages/libraries/client/src/client.ts index 76e17e1e1..7242c860e 100644 --- a/packages/libraries/client/src/client.ts +++ b/packages/libraries/client/src/client.ts @@ -169,6 +169,7 @@ export function createHive(options: HivePluginOptions): HiveClient { return { [hiveClientSymbol]: true, + [autoDisposeSymbol]: options.autoDispose ?? true, info, reportSchema, collectUsage, @@ -177,6 +178,7 @@ export function createHive(options: HivePluginOptions): HiveClient { } export const hiveClientSymbol: unique symbol = Symbol('hive-client'); +export const autoDisposeSymbol: unique symbol = Symbol('hive-auto-dispose'); function createPrinter(values: string[]) { const maxLen = Math.max(...values.map(v => v.length)) + 4; diff --git a/packages/libraries/client/src/envelop.ts b/packages/libraries/client/src/envelop.ts index 9e1d02365..1ac7e16c3 100644 --- a/packages/libraries/client/src/envelop.ts +++ b/packages/libraries/client/src/envelop.ts @@ -1,5 +1,5 @@ import type { Plugin } from '@envelop/types'; -import { createHive } from './client.js'; +import { autoDisposeSymbol, createHive } from './client.js'; import type { HiveClient, HivePluginOptions } from './internal/types.js'; import { isHiveClient } from './internal/utils.js'; @@ -18,6 +18,22 @@ export function useHive(clientOrOptions: HiveClient | HivePluginOptions): Plugin void hive.info(); + if (hive[autoDisposeSymbol]) { + if (global.process) { + const signals = Array.isArray(hive[autoDisposeSymbol]) + ? hive[autoDisposeSymbol] + : ['SIGINT', 'SIGTERM']; + for (const signal of signals) { + process.once(signal, () => hive.dispose()); + } + } else { + console.error( + 'It seems that GraphQL Hive is not being executed in Node.js. ' + + 'Please attempt manual client disposal and use autoDispose: false option.', + ); + } + } + return { onSchemaChange({ schema }) { hive.reportSchema({ schema }); diff --git a/packages/libraries/client/src/internal/types.ts b/packages/libraries/client/src/internal/types.ts index a909f3a40..618b911c0 100644 --- a/packages/libraries/client/src/internal/types.ts +++ b/packages/libraries/client/src/internal/types.ts @@ -1,10 +1,11 @@ import type { ExecutionArgs } from 'graphql'; -import type { hiveClientSymbol } from '../client.js'; +import type { autoDisposeSymbol, hiveClientSymbol } from '../client.js'; import type { AgentOptions } from './agent.js'; import type { SchemaReporter } from './reporting.js'; export interface HiveClient { [hiveClientSymbol]: true; + [autoDisposeSymbol]: boolean | NodeJS.Signals[]; info(): Promise; reportSchema: SchemaReporter['report']; collectUsage(): CollectUsageCallback; @@ -189,6 +190,13 @@ export type HivePluginOptions = OptionalWhenFalse< * Disabled by default */ reporting?: HiveReportingPluginOptions | false; + /** + * Automatically dispose the client when the process is terminated + * + * Apollo: Enabled by default + * Yoga / Envelop: Enabled by default for SIGINT and SIGTERM signals + */ + autoDispose?: boolean | NodeJS.Signals[]; }, 'enabled', 'token' diff --git a/packages/libraries/client/src/yoga.ts b/packages/libraries/client/src/yoga.ts index 3fe6881b6..076447600 100644 --- a/packages/libraries/client/src/yoga.ts +++ b/packages/libraries/client/src/yoga.ts @@ -1,7 +1,7 @@ import { DocumentNode, ExecutionArgs, GraphQLSchema, Kind, parse } from 'graphql'; import type { GraphQLParams, Plugin } from 'graphql-yoga'; import LRU from 'tiny-lru'; -import { createHive } from './client.js'; +import { autoDisposeSymbol, createHive } from './client.js'; import type { CollectUsageCallback, HiveClient, HivePluginOptions } from './internal/types.js'; import { isHiveClient } from './internal/utils.js'; @@ -27,6 +27,22 @@ export function useHive(clientOrOptions: HiveClient | HivePluginOptions): Plugin void hive.info(); + if (hive[autoDisposeSymbol]) { + if (global.process) { + const signals = Array.isArray(hive[autoDisposeSymbol]) + ? hive[autoDisposeSymbol] + : ['SIGINT', 'SIGTERM']; + for (const signal of signals) { + process.once(signal, () => hive.dispose()); + } + } else { + console.error( + 'It seems that GraphQL Hive is not being executed in Node.js. ' + + 'Please attempt manual client disposal and use autoDispose: false option.', + ); + } + } + const parsedDocumentCache = LRU(10_000); let latestSchema: GraphQLSchema | null = null; const cache = new WeakMap();