Skip to content

Commit d30cd0e

Browse files
authored
fix: abort federation composition (#6723)
1 parent fd9b160 commit d30cd0e

File tree

13 files changed

+254
-117
lines changed

13 files changed

+254
-117
lines changed

packages/libraries/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
},
4747
"dependencies": {
4848
"@graphql-tools/utils": "^10.0.0",
49-
"@whatwg-node/fetch": "^0.10.1",
49+
"@whatwg-node/fetch": "^0.10.5",
5050
"async-retry": "^1.3.3",
5151
"lodash.sortby": "^4.7.0",
5252
"tiny-lru": "^8.0.2"

packages/libraries/yoga/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@
5656
"@graphql-yoga/plugin-disable-introspection": "2.7.0",
5757
"@graphql-yoga/plugin-graphql-sse": "3.7.0",
5858
"@graphql-yoga/plugin-response-cache": "3.9.0",
59-
"@whatwg-node/fetch": "0.10.1",
59+
"@whatwg-node/fetch": "0.10.5",
6060
"graphql-ws": "5.16.1",
61-
"graphql-yoga": "5.10.8",
61+
"graphql-yoga": "5.13.3",
6262
"nock": "14.0.0",
6363
"vitest": "3.0.5",
6464
"ws": "8.18.0"

packages/libraries/yoga/tests/yoga.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,7 @@ describe('subscription usage reporting', () => {
882882
:
883883
884884
event: next
885-
data: {"errors":[{"message":"Unexpected error.","locations":[{"line":1,"column":1}]}]}
885+
data: {"errors":[{"message":"Unexpected error.","locations":[{"line":1,"column":1}],"extensions":{"code":"INTERNAL_SERVER_ERROR"}}]}
886886
887887
event: complete
888888
data:

packages/migrations/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"@types/bcryptjs": "2.4.6",
2222
"@types/node": "22.10.5",
2323
"@types/pg": "8.11.10",
24-
"@whatwg-node/fetch": "0.10.1",
24+
"@whatwg-node/fetch": "0.10.5",
2525
"bcryptjs": "2.4.3",
2626
"copyfiles": "2.4.1",
2727
"date-fns": "4.1.0",

packages/services/api/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@bentocache/plugin-prometheus": "0.2.0",
1717
"@date-fns/utc": "2.1.0",
1818
"@graphql-hive/core": "workspace:*",
19+
"@graphql-hive/signal": "1.0.0",
1920
"@graphql-inspector/core": "5.1.0-alpha-20231208113249-34700c8a",
2021
"@graphql-tools/merge": "9.0.24",
2122
"@hive/cdn-script": "workspace:*",
@@ -56,7 +57,7 @@
5657
"graphql-modules": "3.0.0",
5758
"graphql-parse-resolve-info": "4.13.0",
5859
"graphql-scalars": "1.24.0",
59-
"graphql-yoga": "5.10.8",
60+
"graphql-yoga": "5.13.3",
6061
"ioredis": "5.4.2",
6162
"ioredis-mock": "8.9.0",
6263
"lodash": "4.17.21",

packages/services/api/src/modules/schema/providers/orchestrators/federation.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { CONTEXT, Inject, Injectable, Scope } from 'graphql-modules';
2+
import { abortSignalAny } from '@graphql-hive/signal';
23
import type { ContractsInputType, SchemaBuilderApi } from '@hive/schema';
34
import { traceFn } from '@hive/service-common';
45
import { createTRPCProxyClient, httpLink } from '@trpc/client';
@@ -19,6 +20,7 @@ export class FederationOrchestrator implements Orchestrator {
1920
type = ProjectType.FEDERATION;
2021
private logger: Logger;
2122
private schemaService;
23+
private incomingRequestAbortSignal: AbortSignal;
2224

2325
constructor(
2426
logger: Logger,
@@ -37,6 +39,7 @@ export class FederationOrchestrator implements Orchestrator {
3739
}),
3840
],
3941
});
42+
this.incomingRequestAbortSignal = context.request.signal;
4043
}
4144

4245
private createConfig(config: Project['externalComposition']): ExternalCompositionConfig {
@@ -82,18 +85,47 @@ export class FederationOrchestrator implements Orchestrator {
8285
'Composing and Validating Federated Schemas (method=%s)',
8386
config.native ? 'native' : config.external.enabled ? 'external' : 'v1',
8487
);
85-
const result = await this.schemaService.composeAndValidate.mutate({
86-
type: 'federation',
87-
schemas: schemas.map(s => ({
88-
raw: s.raw,
89-
source: s.source,
90-
url: s.url ?? null,
91-
})),
92-
external: this.createConfig(config.external),
93-
native: config.native,
94-
contracts: config.contracts,
95-
});
88+
const timeoutAbortSignal = AbortSignal.timeout(30_000);
89+
90+
const onTimeout = () => {
91+
this.logger.debug('Composition HTTP request aborted due to timeout of 30 seconds.');
92+
};
93+
timeoutAbortSignal.addEventListener('abort', onTimeout);
94+
95+
const onIncomingRequestAbort = () => {
96+
this.logger.debug('Composition HTTP request aborted due to incoming request being canceled.');
97+
};
98+
this.incomingRequestAbortSignal.addEventListener('abort', onIncomingRequestAbort);
9699

97-
return result;
100+
try {
101+
const result = await this.schemaService.composeAndValidate.mutate(
102+
{
103+
type: 'federation',
104+
schemas: schemas.map(s => ({
105+
raw: s.raw,
106+
source: s.source,
107+
url: s.url ?? null,
108+
})),
109+
external: this.createConfig(config.external),
110+
native: config.native,
111+
contracts: config.contracts,
112+
},
113+
{
114+
// We want to abort composition if the request that does the composition is aborted
115+
// We also limit the maximum time allowed for composition requests to 30 seconds to avoid
116+
//
117+
// The reason for these is a potential dead-lock.
118+
//
119+
// Note: We are using `abortSignalAny` over `AbortSignal.any` because of leak issues.
120+
// @source https://github.com/nodejs/node/issues/57584
121+
signal: abortSignalAny([this.incomingRequestAbortSignal, timeoutAbortSignal]),
122+
},
123+
);
124+
125+
return result;
126+
} finally {
127+
timeoutAbortSignal.removeEventListener('abort', onTimeout);
128+
this.incomingRequestAbortSignal.removeEventListener('abort', onIncomingRequestAbort);
129+
}
98130
}
99131
}

packages/services/api/src/modules/shared/providers/mutex.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export class Mutex {
108108

109109
// We try to acquire the lock until the retry counter is exceeded or the lock as been successfully acquired.
110110
do {
111-
logger.debug('Acquiring lock (id=%s, attempt=%n)', id, attemptCounter + 1);
111+
logger.debug('Acquiring lock (id=%s, attempt=%d)', id, attemptCounter + 1);
112112

113113
lockToAcquire = await Promise.race([
114114
// we avoid using any of the acquire settings for auto-extension, retrying, etc.
@@ -171,7 +171,7 @@ export class Mutex {
171171
}
172172

173173
let extendTimeout: NodeJS.Timeout | undefined;
174-
// we have a global timeout of 90 seconds to avoid dead-licks
174+
// we have a global timeout of 90 seconds to avoid dead-locks
175175
const globalTimeout = setTimeout(() => {
176176
logger.error('Global lock timeout exceeded (id=%s)', id);
177177
void cleanup();
@@ -188,12 +188,10 @@ export class Mutex {
188188
clearTimeout(globalTimeout);
189189

190190
extendTimeout = undefined;
191-
if (lock.expiration > new Date().getTime()) {
192-
void lock.release().catch(err => {
193-
logger.error('Error while releasing lock (id=%s)', id);
194-
console.error(err);
195-
});
196-
}
191+
void lock.release().catch(err => {
192+
logger.error('Error while releasing lock (id=%s)', id);
193+
console.error(err);
194+
});
197195
}
198196

199197
async function extendLock(isInitial = false) {

packages/services/broker-worker/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"devDependencies": {
1111
"@cloudflare/workers-types": "4.20241230.0",
1212
"@types/service-worker-mock": "2.0.4",
13-
"@whatwg-node/server": "0.9.65",
13+
"@whatwg-node/server": "0.10.3",
1414
"esbuild": "0.25.0",
1515
"itty-router": "4.2.2",
1616
"toucan-js": "4.1.0",

packages/services/cdn-worker/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"devDependencies": {
2020
"@cloudflare/workers-types": "4.20241230.0",
2121
"@types/service-worker-mock": "2.0.4",
22-
"@whatwg-node/server": "0.9.65",
22+
"@whatwg-node/server": "0.10.3",
2323
"bcryptjs": "2.4.3",
2424
"dotenv": "16.4.7",
2525
"esbuild": "0.25.0",

packages/services/demo/federation/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"dependencies": {
99
"@apollo/subgraph": "2.9.3",
1010
"graphql": "16.9.0",
11-
"graphql-yoga": "5.10.8"
11+
"graphql-yoga": "5.13.3"
1212
},
1313
"devDependencies": {
1414
"wrangler": "3.107.2"

0 commit comments

Comments
 (0)