1
1
import { CONTEXT , Inject , Injectable , Scope } from 'graphql-modules' ;
2
+ import { abortSignalAny } from '@graphql-hive/signal' ;
2
3
import type { ContractsInputType , SchemaBuilderApi } from '@hive/schema' ;
3
4
import { traceFn } from '@hive/service-common' ;
4
5
import { createTRPCProxyClient , httpLink } from '@trpc/client' ;
@@ -19,6 +20,7 @@ export class FederationOrchestrator implements Orchestrator {
19
20
type = ProjectType . FEDERATION ;
20
21
private logger : Logger ;
21
22
private schemaService ;
23
+ private incomingRequestAbortSignal : AbortSignal ;
22
24
23
25
constructor (
24
26
logger : Logger ,
@@ -37,6 +39,7 @@ export class FederationOrchestrator implements Orchestrator {
37
39
} ) ,
38
40
] ,
39
41
} ) ;
42
+ this . incomingRequestAbortSignal = context . request . signal ;
40
43
}
41
44
42
45
private createConfig ( config : Project [ 'externalComposition' ] ) : ExternalCompositionConfig {
@@ -82,18 +85,47 @@ export class FederationOrchestrator implements Orchestrator {
82
85
'Composing and Validating Federated Schemas (method=%s)' ,
83
86
config . native ? 'native' : config . external . enabled ? 'external' : 'v1' ,
84
87
) ;
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 ) ;
96
99
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
+ }
98
130
}
99
131
}
0 commit comments