@apollo/gateway 2.0.6-rc.0 → 2.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/config.d.ts +1 -1
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +2 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/datasources/RemoteGraphQLDataSource.d.ts.map +1 -1
  6. package/dist/datasources/RemoteGraphQLDataSource.js +2 -3
  7. package/dist/datasources/RemoteGraphQLDataSource.js.map +1 -1
  8. package/dist/datasources/types.d.ts +1 -0
  9. package/dist/datasources/types.d.ts.map +1 -1
  10. package/dist/executeQueryPlan.d.ts +2 -2
  11. package/dist/executeQueryPlan.d.ts.map +1 -1
  12. package/dist/executeQueryPlan.js +24 -16
  13. package/dist/executeQueryPlan.js.map +1 -1
  14. package/dist/index.d.ts +8 -8
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +27 -62
  17. package/dist/index.js.map +1 -1
  18. package/dist/logger.d.ts +3 -0
  19. package/dist/logger.d.ts.map +1 -0
  20. package/dist/logger.js +15 -0
  21. package/dist/logger.js.map +1 -0
  22. package/dist/operationContext.d.ts.map +1 -1
  23. package/dist/operationContext.js +3 -7
  24. package/dist/operationContext.js.map +1 -1
  25. package/dist/supergraphManagers/IntrospectAndCompose/index.d.ts +1 -1
  26. package/dist/supergraphManagers/IntrospectAndCompose/index.d.ts.map +1 -1
  27. package/dist/supergraphManagers/IntrospectAndCompose/loadServicesFromRemoteEndpoint.d.ts +1 -1
  28. package/dist/supergraphManagers/IntrospectAndCompose/loadServicesFromRemoteEndpoint.d.ts.map +1 -1
  29. package/dist/supergraphManagers/IntrospectAndCompose/loadServicesFromRemoteEndpoint.js.map +1 -1
  30. package/dist/supergraphManagers/UplinkSupergraphManager/index.d.ts +61 -0
  31. package/dist/supergraphManagers/UplinkSupergraphManager/index.d.ts.map +1 -0
  32. package/dist/supergraphManagers/UplinkSupergraphManager/index.js +209 -0
  33. package/dist/supergraphManagers/UplinkSupergraphManager/index.js.map +1 -0
  34. package/dist/supergraphManagers/{UplinkFetcher → UplinkSupergraphManager}/loadSupergraphSdlFromStorage.d.ts +8 -4
  35. package/dist/supergraphManagers/UplinkSupergraphManager/loadSupergraphSdlFromStorage.d.ts.map +1 -0
  36. package/dist/supergraphManagers/{UplinkFetcher → UplinkSupergraphManager}/loadSupergraphSdlFromStorage.js +20 -8
  37. package/dist/supergraphManagers/UplinkSupergraphManager/loadSupergraphSdlFromStorage.js.map +1 -0
  38. package/dist/supergraphManagers/{UplinkFetcher → UplinkSupergraphManager}/outOfBandReporter.d.ts +2 -1
  39. package/dist/supergraphManagers/UplinkSupergraphManager/outOfBandReporter.d.ts.map +1 -0
  40. package/dist/supergraphManagers/{UplinkFetcher → UplinkSupergraphManager}/outOfBandReporter.js +0 -0
  41. package/dist/supergraphManagers/UplinkSupergraphManager/outOfBandReporter.js.map +1 -0
  42. package/dist/supergraphManagers/UplinkSupergraphManager/types.d.ts +9 -0
  43. package/dist/supergraphManagers/UplinkSupergraphManager/types.d.ts.map +1 -0
  44. package/dist/supergraphManagers/UplinkSupergraphManager/types.js +5 -0
  45. package/dist/supergraphManagers/UplinkSupergraphManager/types.js.map +1 -0
  46. package/dist/supergraphManagers/index.d.ts +2 -2
  47. package/dist/supergraphManagers/index.d.ts.map +1 -1
  48. package/dist/supergraphManagers/index.js +17 -4
  49. package/dist/supergraphManagers/index.js.map +1 -1
  50. package/package.json +10 -10
  51. package/src/__tests__/executeQueryPlan.test.ts +199 -1
  52. package/src/__tests__/execution-utils.ts +5 -3
  53. package/src/__tests__/integration/abstract-types.test.ts +31 -65
  54. package/src/__tests__/integration/configuration.test.ts +2 -45
  55. package/src/__tests__/integration/managed.test.ts +292 -0
  56. package/src/__tests__/integration/networkRequests.test.ts +14 -54
  57. package/src/__tests__/integration/nockMocks.ts +7 -6
  58. package/src/config.ts +3 -1
  59. package/src/datasources/RemoteGraphQLDataSource.ts +1 -2
  60. package/src/datasources/types.ts +4 -0
  61. package/src/executeQueryPlan.ts +41 -30
  62. package/src/index.ts +33 -88
  63. package/src/logger.ts +11 -0
  64. package/src/operationContext.ts +5 -7
  65. package/src/supergraphManagers/IntrospectAndCompose/index.ts +1 -1
  66. package/src/supergraphManagers/IntrospectAndCompose/loadServicesFromRemoteEndpoint.ts +1 -1
  67. package/src/supergraphManagers/UplinkSupergraphManager/__tests__/UplinkSupergraphManager.test.ts +65 -0
  68. package/src/supergraphManagers/{UplinkFetcher → UplinkSupergraphManager}/__tests__/loadSupergraphSdlFromStorage.test.ts +51 -16
  69. package/src/supergraphManagers/{UplinkFetcher → UplinkSupergraphManager}/__tests__/tsconfig.json +0 -0
  70. package/src/supergraphManagers/UplinkSupergraphManager/index.ts +312 -0
  71. package/src/supergraphManagers/{UplinkFetcher → UplinkSupergraphManager}/loadSupergraphSdlFromStorage.ts +32 -12
  72. package/src/supergraphManagers/{UplinkFetcher → UplinkSupergraphManager}/outOfBandReporter.ts +2 -1
  73. package/src/supergraphManagers/UplinkSupergraphManager/types.ts +10 -0
  74. package/src/supergraphManagers/index.ts +2 -2
  75. package/dist/supergraphManagers/UplinkFetcher/index.d.ts +0 -35
  76. package/dist/supergraphManagers/UplinkFetcher/index.d.ts.map +0 -1
  77. package/dist/supergraphManagers/UplinkFetcher/index.js +0 -114
  78. package/dist/supergraphManagers/UplinkFetcher/index.js.map +0 -1
  79. package/dist/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.d.ts.map +0 -1
  80. package/dist/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.js.map +0 -1
  81. package/dist/supergraphManagers/UplinkFetcher/outOfBandReporter.d.ts.map +0 -1
  82. package/dist/supergraphManagers/UplinkFetcher/outOfBandReporter.js.map +0 -1
  83. package/src/supergraphManagers/UplinkFetcher/index.ts +0 -149
@@ -22,6 +22,15 @@ import {
22
22
  nockBeforeEach,
23
23
  } from '../../../__tests__/nockAssertions';
24
24
 
25
+ const logger = {
26
+ warn: jest.fn(),
27
+ debug: jest.fn(),
28
+ error: jest.fn(),
29
+ info: jest.fn(),
30
+ };
31
+
32
+ const requestTimeoutMs = 100;
33
+
25
34
  describe('loadSupergraphSdlFromStorage', () => {
26
35
  beforeEach(nockBeforeEach);
27
36
  afterEach(nockAfterEach);
@@ -34,7 +43,9 @@ describe('loadSupergraphSdlFromStorage', () => {
34
43
  endpoint: mockCloudConfigUrl1,
35
44
  errorReportingEndpoint: undefined,
36
45
  fetcher,
46
+ requestTimeoutMs,
37
47
  compositionId: null,
48
+ logger,
38
49
  });
39
50
  expect(result).toMatchObject({
40
51
  id: 'originalId-1234',
@@ -66,10 +77,11 @@ describe('loadSupergraphSdlFromStorage', () => {
66
77
  endpoints: [mockCloudConfigUrl1, mockCloudConfigUrl2],
67
78
  errorReportingEndpoint: undefined,
68
79
  fetcher,
80
+ requestTimeoutMs,
69
81
  compositionId: 'originalId-1234',
70
82
  maxRetries: 1,
71
83
  roundRobinSeed: 0,
72
- earliestFetchTime: null,
84
+ logger,
73
85
  });
74
86
 
75
87
  expect(result).toMatchObject({
@@ -92,10 +104,11 @@ describe('loadSupergraphSdlFromStorage', () => {
92
104
  endpoints: [mockCloudConfigUrl1, mockCloudConfigUrl2],
93
105
  errorReportingEndpoint: undefined,
94
106
  fetcher,
107
+ requestTimeoutMs,
95
108
  compositionId: 'originalId-1234',
96
109
  maxRetries: 1,
97
110
  roundRobinSeed: 0,
98
- earliestFetchTime: null,
111
+ logger,
99
112
  }),
100
113
  ).rejects.toThrowError(
101
114
  new UplinkFetcherError(
@@ -115,7 +128,9 @@ describe('loadSupergraphSdlFromStorage', () => {
115
128
  endpoint: mockCloudConfigUrl1,
116
129
  errorReportingEndpoint: mockOutOfBandReporterUrl,
117
130
  fetcher,
131
+ requestTimeoutMs,
118
132
  compositionId: null,
133
+ logger,
119
134
  }),
120
135
  ).rejects.toThrowError(
121
136
  new UplinkFetcherError(
@@ -140,7 +155,9 @@ describe('loadSupergraphSdlFromStorage', () => {
140
155
  endpoint: mockCloudConfigUrl1,
141
156
  errorReportingEndpoint: mockOutOfBandReporterUrl,
142
157
  fetcher,
158
+ requestTimeoutMs,
143
159
  compositionId: null,
160
+ logger,
144
161
  }),
145
162
  ).rejects.toThrowError(
146
163
  new UplinkFetcherError(
@@ -160,7 +177,9 @@ describe('loadSupergraphSdlFromStorage', () => {
160
177
  endpoint: mockCloudConfigUrl1,
161
178
  errorReportingEndpoint: mockOutOfBandReporterUrl,
162
179
  fetcher,
180
+ requestTimeoutMs,
163
181
  compositionId: null,
182
+ logger,
164
183
  }),
165
184
  ).rejects.toThrowError(
166
185
  new UplinkFetcherError(
@@ -181,7 +200,9 @@ describe('loadSupergraphSdlFromStorage', () => {
181
200
  endpoint: mockCloudConfigUrl1,
182
201
  errorReportingEndpoint: mockOutOfBandReporterUrl,
183
202
  fetcher,
203
+ requestTimeoutMs,
184
204
  compositionId: null,
205
+ logger,
185
206
  }),
186
207
  ).rejects.toThrowError(
187
208
  new UplinkFetcherError(
@@ -200,7 +221,9 @@ describe('loadSupergraphSdlFromStorage', () => {
200
221
  endpoint: mockCloudConfigUrl1,
201
222
  errorReportingEndpoint: mockOutOfBandReporterUrl,
202
223
  fetcher,
224
+ requestTimeoutMs,
203
225
  compositionId: null,
226
+ logger,
204
227
  }),
205
228
  ).rejects.toThrowError(
206
229
  new UplinkFetcherError(
@@ -220,7 +243,9 @@ describe('loadSupergraphSdlFromStorage', () => {
220
243
  endpoint: mockCloudConfigUrl1,
221
244
  errorReportingEndpoint: mockOutOfBandReporterUrl,
222
245
  fetcher,
246
+ requestTimeoutMs,
223
247
  compositionId: null,
248
+ logger,
224
249
  }),
225
250
  ).rejects.toThrowError(
226
251
  new UplinkFetcherError(
@@ -240,7 +265,9 @@ describe('loadSupergraphSdlFromStorage', () => {
240
265
  endpoint: mockCloudConfigUrl1,
241
266
  errorReportingEndpoint: mockOutOfBandReporterUrl,
242
267
  fetcher,
268
+ requestTimeoutMs,
243
269
  compositionId: null,
270
+ logger,
244
271
  }),
245
272
  ).rejects.toThrowError(
246
273
  new UplinkFetcherError(
@@ -260,7 +287,9 @@ describe('loadSupergraphSdlFromStorage', () => {
260
287
  endpoint: mockCloudConfigUrl1,
261
288
  errorReportingEndpoint: mockOutOfBandReporterUrl,
262
289
  fetcher,
290
+ requestTimeoutMs,
263
291
  compositionId: null,
292
+ logger,
264
293
  }),
265
294
  ).rejects.toThrowError(
266
295
  new UplinkFetcherError(
@@ -281,7 +310,9 @@ describe('loadSupergraphSdlFromStorage', () => {
281
310
  endpoint: mockCloudConfigUrl1,
282
311
  errorReportingEndpoint: mockOutOfBandReporterUrl,
283
312
  fetcher,
313
+ requestTimeoutMs,
284
314
  compositionId: null,
315
+ logger,
285
316
  }),
286
317
  ).rejects.toThrowError(
287
318
  new UplinkFetcherError(
@@ -301,7 +332,9 @@ describe('loadSupergraphSdlFromStorage', () => {
301
332
  endpoint: mockCloudConfigUrl1,
302
333
  errorReportingEndpoint: mockOutOfBandReporterUrl,
303
334
  fetcher,
335
+ requestTimeoutMs,
304
336
  compositionId: null,
337
+ logger,
305
338
  }),
306
339
  ).rejects.toThrowError(
307
340
  new UplinkFetcherError(
@@ -321,7 +354,9 @@ describe('loadSupergraphSdlFromStorage', () => {
321
354
  endpoint: mockCloudConfigUrl1,
322
355
  errorReportingEndpoint: mockOutOfBandReporterUrl,
323
356
  fetcher,
357
+ requestTimeoutMs,
324
358
  compositionId: null,
359
+ logger,
325
360
  }),
326
361
  ).rejects.toThrowError(
327
362
  new UplinkFetcherError(
@@ -341,7 +376,9 @@ describe('loadSupergraphSdlFromStorage', () => {
341
376
  endpoint: mockCloudConfigUrl1,
342
377
  errorReportingEndpoint: mockOutOfBandReporterUrl,
343
378
  fetcher,
379
+ requestTimeoutMs,
344
380
  compositionId: null,
381
+ logger,
345
382
  }),
346
383
  ).rejects.toThrowError(
347
384
  new UplinkFetcherError(
@@ -359,7 +396,9 @@ describe('loadSupergraphSdlFromStorage', () => {
359
396
  endpoint: mockCloudConfigUrl1,
360
397
  errorReportingEndpoint: mockOutOfBandReporterUrl,
361
398
  fetcher,
399
+ requestTimeoutMs,
362
400
  compositionId: 'id-1234',
401
+ logger,
363
402
  });
364
403
  expect(result).toBeNull();
365
404
  });
@@ -382,20 +421,20 @@ describe('loadSupergraphSdlFromUplinks', () => {
382
421
  calls++;
383
422
  return fetcher(...args);
384
423
  },
424
+ requestTimeoutMs,
385
425
  compositionId: 'id-1234',
386
426
  maxRetries: 5,
387
427
  roundRobinSeed: 0,
388
- earliestFetchTime: null,
428
+ logger,
389
429
  });
390
430
 
391
431
  expect(result).toBeNull();
392
432
  expect(calls).toBe(1);
393
433
  });
394
434
 
395
- it('Waits the correct time before retrying', async () => {
396
- const timeoutSpy = jest.spyOn(global, 'setTimeout');
397
-
435
+ it('Retries on error', async () => {
398
436
  mockSupergraphSdlRequest('originalId-1234', mockCloudConfigUrl1).reply(500);
437
+ const supergraphSdl = getTestingSupergraphSdl();
399
438
  mockSupergraphSdlRequestIfAfter(
400
439
  'originalId-1234',
401
440
  mockCloudConfigUrl2,
@@ -406,29 +445,25 @@ describe('loadSupergraphSdlFromUplinks', () => {
406
445
  routerConfig: {
407
446
  __typename: 'RouterConfigResult',
408
447
  id: 'originalId-1234',
409
- supergraphSdl: getTestingSupergraphSdl(),
448
+ supergraphSdl,
410
449
  },
411
450
  },
412
451
  }),
413
452
  );
414
453
 
415
- await loadSupergraphSdlFromUplinks({
454
+ const result = await loadSupergraphSdlFromUplinks({
416
455
  graphRef,
417
456
  apiKey,
418
457
  endpoints: [mockCloudConfigUrl1, mockCloudConfigUrl2],
419
458
  errorReportingEndpoint: undefined,
420
- fetcher: fetcher,
459
+ fetcher,
460
+ requestTimeoutMs,
421
461
  compositionId: 'originalId-1234',
422
462
  maxRetries: 1,
423
463
  roundRobinSeed: 0,
424
- earliestFetchTime: new Date(Date.now() + 1000),
464
+ logger,
425
465
  });
426
466
 
427
- // test if setTimeout was called with a value in range to deal with time jitter
428
- const setTimeoutCall = timeoutSpy.mock.calls[1][1];
429
- expect(setTimeoutCall).toBeLessThanOrEqual(1000);
430
- expect(setTimeoutCall).toBeGreaterThanOrEqual(900);
431
-
432
- timeoutSpy.mockRestore();
467
+ expect(result?.supergraphSdl).toEqual(supergraphSdl);
433
468
  });
434
469
  });
@@ -0,0 +1,312 @@
1
+ import * as makeFetchHappen from 'make-fetch-happen';
2
+ import type { Logger } from '@apollo/utils.logger';
3
+ import resolvable, { Resolvable } from '@josephg/resolvable';
4
+ import { SupergraphManager, SupergraphSdlHookOptions } from '../../config';
5
+ import {
6
+ SubgraphHealthCheckFunction,
7
+ SupergraphSdlUpdateFunction,
8
+ } from '../..';
9
+ import { getDefaultLogger } from '../../logger';
10
+ import { loadSupergraphSdlFromUplinks } from './loadSupergraphSdlFromStorage';
11
+ import type { AbortableFetcher } from './types';
12
+
13
+ export type FailureToFetchSupergraphSdlFunctionParams = {
14
+ error: Error;
15
+ graphRef: string;
16
+ logger: Logger;
17
+ fetchCount: number;
18
+ };
19
+
20
+ export type FailureToFetchSupergraphSdlDuringInit = ({
21
+ error,
22
+ graphRef,
23
+ logger,
24
+ fetchCount,
25
+ }: FailureToFetchSupergraphSdlFunctionParams) => Promise<string>;
26
+
27
+ export type FailureToFetchSupergraphSdlAfterInit = ({
28
+ error,
29
+ graphRef,
30
+ logger,
31
+ fetchCount,
32
+ mostRecentSuccessfulFetchAt,
33
+ }:
34
+ | FailureToFetchSupergraphSdlFunctionParams & {
35
+ mostRecentSuccessfulFetchAt?: Date;
36
+ }) => Promise<string | null>;
37
+
38
+ type State =
39
+ | { phase: 'constructed' }
40
+ | { phase: 'initialized' }
41
+ | {
42
+ phase: 'polling';
43
+ pollingPromise?: Promise<void>;
44
+ nextFetchPromise?: Resolvable<void>;
45
+ }
46
+ | { phase: 'stopped' };
47
+
48
+ export class UplinkSupergraphManager implements SupergraphManager {
49
+ public static readonly DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
50
+ public static readonly MIN_POLL_INTERVAL_MS = 10_000;
51
+
52
+ public static readonly DEFAULT_UPLINK_ENDPOINTS = [
53
+ 'https://uplink.api.apollographql.com/',
54
+ 'https://aws.uplink.api.apollographql.com/',
55
+ ];
56
+
57
+ public readonly uplinkEndpoints: string[] =
58
+ UplinkSupergraphManager.getUplinkEndpoints();
59
+ private apiKey: string;
60
+ private graphRef: string;
61
+ private fetcher: AbortableFetcher = makeFetchHappen.defaults();
62
+ private maxRetries: number;
63
+ private requestTimeoutMs: number =
64
+ UplinkSupergraphManager.DEFAULT_REQUEST_TIMEOUT_MS;
65
+ private initialMaxRetries: number;
66
+ private pollIntervalMs: number = UplinkSupergraphManager.MIN_POLL_INTERVAL_MS;
67
+ private logger: Logger;
68
+ private update?: SupergraphSdlUpdateFunction;
69
+ private shouldRunSubgraphHealthcheck: boolean = false;
70
+ private healthCheck?: SubgraphHealthCheckFunction;
71
+ private onFailureToFetchSupergraphSdlDuringInit?: FailureToFetchSupergraphSdlDuringInit;
72
+ private onFailureToFetchSupergraphSdlAfterInit?: FailureToFetchSupergraphSdlAfterInit;
73
+ private timerRef: NodeJS.Timeout | null = null;
74
+ private state: State;
75
+ private errorReportingEndpoint: string | undefined =
76
+ process.env.APOLLO_OUT_OF_BAND_REPORTER_ENDPOINT ?? undefined;
77
+ private compositionId?: string;
78
+ private fetchCount: number = 0;
79
+ private mostRecentSuccessfulFetchAt?: Date;
80
+
81
+ constructor({
82
+ apiKey,
83
+ graphRef,
84
+ debug,
85
+ logger,
86
+ uplinkEndpoints,
87
+ fallbackPollIntervalInMs,
88
+ maxRetries,
89
+ initialMaxRetries,
90
+ shouldRunSubgraphHealthcheck,
91
+ onFailureToFetchSupergraphSdlDuringInit,
92
+ onFailureToFetchSupergraphSdlAfterInit,
93
+ }: {
94
+ apiKey: string;
95
+ graphRef: string;
96
+ debug?: boolean;
97
+ logger?: Logger;
98
+ uplinkEndpoints?: string[];
99
+ fallbackPollIntervalInMs?: number;
100
+ maxRetries?: number;
101
+ initialMaxRetries?: number;
102
+ shouldRunSubgraphHealthcheck?: boolean;
103
+ onFailureToFetchSupergraphSdlDuringInit?: FailureToFetchSupergraphSdlDuringInit;
104
+ onFailureToFetchSupergraphSdlAfterInit?: FailureToFetchSupergraphSdlAfterInit;
105
+ }) {
106
+ this.apiKey = apiKey;
107
+ this.graphRef = graphRef;
108
+ this.logger = logger ?? getDefaultLogger(debug);
109
+
110
+ this.uplinkEndpoints = uplinkEndpoints ?? this.uplinkEndpoints;
111
+ // If the user didn't pass a `maxRetries`, default to trying each endpoint
112
+ // 3 times (minus 1 for the initial request) since we round-robin through
113
+ // each URL on failure
114
+ this.maxRetries = maxRetries ?? this.uplinkEndpoints.length * 3 - 1;
115
+ this.initialMaxRetries = initialMaxRetries ?? this.maxRetries;
116
+
117
+ this.pollIntervalMs = fallbackPollIntervalInMs ?? this.pollIntervalMs;
118
+ if (this.pollIntervalMs < UplinkSupergraphManager.MIN_POLL_INTERVAL_MS) {
119
+ this.logger.warn(
120
+ 'Polling Apollo services at a frequency of less than once per 10 seconds (10000) is disallowed. Instead, the minimum allowed pollInterval of 10000 will be used. Please reconfigure your `fallbackPollIntervalInMs` accordingly. If this is problematic for your team, please contact support.',
121
+ );
122
+ this.pollIntervalMs = UplinkSupergraphManager.MIN_POLL_INTERVAL_MS;
123
+ }
124
+
125
+ this.shouldRunSubgraphHealthcheck =
126
+ shouldRunSubgraphHealthcheck ?? this.shouldRunSubgraphHealthcheck;
127
+ this.onFailureToFetchSupergraphSdlDuringInit =
128
+ onFailureToFetchSupergraphSdlDuringInit;
129
+ this.onFailureToFetchSupergraphSdlAfterInit =
130
+ onFailureToFetchSupergraphSdlAfterInit;
131
+
132
+ this.state = { phase: 'constructed' };
133
+ }
134
+
135
+ public async initialize({ update, healthCheck }: SupergraphSdlHookOptions) {
136
+ this.update = update;
137
+
138
+ if (this.shouldRunSubgraphHealthcheck) {
139
+ this.healthCheck = healthCheck;
140
+ }
141
+
142
+ let initialSupergraphSdl: string | null = null;
143
+ try {
144
+ initialSupergraphSdl = await this.updateSupergraphSdl(
145
+ this.initialMaxRetries,
146
+ );
147
+ if (!initialSupergraphSdl) {
148
+ throw new Error(
149
+ 'Invalid supergraph schema supplied during initialization.',
150
+ );
151
+ }
152
+ } catch (e) {
153
+ this.logUpdateFailure(e);
154
+ throw e;
155
+ }
156
+
157
+ this.state = { phase: 'initialized' };
158
+
159
+ // Start polling after we resolve the first supergraph
160
+ this.beginPolling();
161
+
162
+ return {
163
+ supergraphSdl: initialSupergraphSdl,
164
+ cleanup: async () => {
165
+ if (this.state.phase === 'polling') {
166
+ await this.state.pollingPromise;
167
+ }
168
+ this.state = { phase: 'stopped' };
169
+ if (this.timerRef) {
170
+ clearTimeout(this.timerRef);
171
+ this.timerRef = null;
172
+ }
173
+ },
174
+ };
175
+ }
176
+
177
+ public async nextFetch(): Promise<void | null> {
178
+ if (this.state.phase !== 'polling') {
179
+ return;
180
+ }
181
+ return this.state.nextFetchPromise;
182
+ }
183
+
184
+ /**
185
+ * Configuration priority order:
186
+ * 1. APOLLO_SCHEMA_CONFIG_DELIVERY_ENDPOINT environment variable
187
+ * 2. default (GCP and AWS)
188
+ */
189
+ public static getUplinkEndpoints(): string[] {
190
+ const envEndpoints =
191
+ process.env.APOLLO_SCHEMA_CONFIG_DELIVERY_ENDPOINT?.split(',');
192
+ return envEndpoints ?? UplinkSupergraphManager.DEFAULT_UPLINK_ENDPOINTS;
193
+ }
194
+
195
+ private async updateSupergraphSdl(
196
+ maxRetries: number,
197
+ ): Promise<string | null> {
198
+ let supergraphSdl;
199
+
200
+ try {
201
+ const result = await loadSupergraphSdlFromUplinks({
202
+ graphRef: this.graphRef,
203
+ apiKey: this.apiKey,
204
+ endpoints: this.uplinkEndpoints,
205
+ errorReportingEndpoint: this.errorReportingEndpoint,
206
+ fetcher: this.fetcher,
207
+ compositionId: this.compositionId ?? null,
208
+ maxRetries,
209
+ requestTimeoutMs: this.requestTimeoutMs,
210
+ roundRobinSeed: this.fetchCount++,
211
+ logger: this.logger,
212
+ });
213
+ this.mostRecentSuccessfulFetchAt = new Date();
214
+
215
+ this.logger.debug(
216
+ `Received Uplink response. Has updated SDL? ${!!result?.supergraphSdl}`,
217
+ );
218
+
219
+ if (!result) {
220
+ return null;
221
+ }
222
+
223
+ this.compositionId = result.id;
224
+
225
+ supergraphSdl = result.supergraphSdl;
226
+ if (result?.minDelaySeconds) {
227
+ this.pollIntervalMs = result.minDelaySeconds * 1000;
228
+ }
229
+ } catch (e) {
230
+ this.logger.debug(
231
+ `Error fetching supergraphSdl from Uplink during phase '${this.state.phase}'`,
232
+ );
233
+
234
+ if (
235
+ this.state.phase === 'constructed' &&
236
+ this.onFailureToFetchSupergraphSdlDuringInit
237
+ ) {
238
+ supergraphSdl = await this.onFailureToFetchSupergraphSdlDuringInit({
239
+ error: e,
240
+ graphRef: this.graphRef,
241
+ logger: this.logger,
242
+ fetchCount: this.fetchCount,
243
+ });
244
+ } else if (
245
+ this.state.phase === 'polling' &&
246
+ this.onFailureToFetchSupergraphSdlAfterInit
247
+ ) {
248
+ supergraphSdl = await this.onFailureToFetchSupergraphSdlAfterInit({
249
+ error: e,
250
+ graphRef: this.graphRef,
251
+ logger: this.logger,
252
+ fetchCount: this.fetchCount,
253
+ mostRecentSuccessfulFetchAt: this.mostRecentSuccessfulFetchAt,
254
+ });
255
+
256
+ // This is really an error, but we'll let the caller decide what to do with it
257
+ if (!supergraphSdl) {
258
+ return null;
259
+ }
260
+ } else {
261
+ throw e;
262
+ }
263
+ }
264
+
265
+ // the healthCheck fn is only assigned if it's enabled in the config
266
+ await this.healthCheck?.(supergraphSdl);
267
+ return supergraphSdl;
268
+ }
269
+
270
+ private beginPolling() {
271
+ this.state = { phase: 'polling' };
272
+ this.poll();
273
+ }
274
+
275
+ private poll() {
276
+ if (this.state.phase !== 'polling') {
277
+ this.logger.debug(`Stopped polling Uplink [phase: ${this.state.phase}]`);
278
+ return;
279
+ }
280
+
281
+ this.state.nextFetchPromise = resolvable();
282
+
283
+ this.logger.debug(
284
+ `Will poll Uplink after ${this.pollIntervalMs}ms [phase: ${this.state.phase}]`,
285
+ );
286
+ this.timerRef = setTimeout(async () => {
287
+ if (this.state.phase === 'polling') {
288
+ const pollingPromise = resolvable();
289
+ this.state.pollingPromise = pollingPromise;
290
+ try {
291
+ const supergraphSdl = await this.updateSupergraphSdl(this.maxRetries);
292
+ if (supergraphSdl) {
293
+ this.update?.(supergraphSdl);
294
+ }
295
+ } catch (e) {
296
+ this.logUpdateFailure(e);
297
+ }
298
+ pollingPromise.resolve();
299
+ this.state.nextFetchPromise?.resolve();
300
+ }
301
+
302
+ this.poll();
303
+ }, this.pollIntervalMs);
304
+ }
305
+
306
+ private logUpdateFailure(e: any) {
307
+ this.logger.error(
308
+ 'UplinkSupergraphManager failed to update supergraph with the following error: ' +
309
+ (e.message ?? e),
310
+ );
311
+ }
312
+ }
@@ -1,13 +1,15 @@
1
1
  import { GraphQLError } from 'graphql';
2
2
  import retry from 'async-retry';
3
+ import { AbortController } from "node-abort-controller";
3
4
  import { SupergraphSdlUpdate } from '../../config';
4
5
  import { submitOutOfBandReportIfConfigured } from './outOfBandReporter';
5
6
  import { SupergraphSdlQuery } from '../../__generated__/graphqlTypes';
7
+ import type { FetcherResponse } from '@apollo/utils.fetcher';
8
+ import type { Logger } from '@apollo/utils.logger';
6
9
  import type {
7
- Fetcher,
8
- FetcherResponse,
9
- FetcherRequestInit,
10
- } from '@apollo/utils.fetcher';
10
+ AbortableFetcher as Fetcher,
11
+ AbortableFetcherRequestInit as FetcherRequestInit,
12
+ } from './types';
11
13
 
12
14
  // Magic /* GraphQL */ comment below is for codegen, do not remove
13
15
  export const SUPERGRAPH_SDL_QUERY = /* GraphQL */`#graphql
@@ -60,8 +62,9 @@ export async function loadSupergraphSdlFromUplinks({
60
62
  fetcher,
61
63
  compositionId,
62
64
  maxRetries,
65
+ requestTimeoutMs,
63
66
  roundRobinSeed,
64
- earliestFetchTime,
67
+ logger,
65
68
  }: {
66
69
  graphRef: string;
67
70
  apiKey: string;
@@ -70,8 +73,9 @@ export async function loadSupergraphSdlFromUplinks({
70
73
  fetcher: Fetcher;
71
74
  compositionId: string | null;
72
75
  maxRetries: number,
76
+ requestTimeoutMs: number,
73
77
  roundRobinSeed: number,
74
- earliestFetchTime: Date | null
78
+ logger: Logger,
75
79
  }) : Promise<SupergraphSdlUpdate | null> {
76
80
  // This Promise resolves with either an updated supergraph or null if no change.
77
81
  // This Promise can reject in the case that none of the retries are successful,
@@ -84,17 +88,18 @@ export async function loadSupergraphSdlFromUplinks({
84
88
  endpoint: endpoints[roundRobinSeed++ % endpoints.length],
85
89
  errorReportingEndpoint,
86
90
  fetcher,
91
+ requestTimeoutMs,
87
92
  compositionId,
93
+ logger,
88
94
  }),
89
95
  {
90
96
  retries: maxRetries,
91
- onRetry: async () => {
92
- const delayMS = earliestFetchTime ? earliestFetchTime.getTime() - Date.now(): 0;
93
- if (delayMS > 0) await new Promise(resolve => setTimeout(resolve, delayMS));
94
- }
97
+ maxTimeout: 60_000,
98
+ onRetry(e, attempt) {
99
+ logger.debug(`Unable to fetch supergraph SDL (attempt ${attempt}), waiting before retry: ${e}`);
100
+ },
95
101
  },
96
102
  );
97
-
98
103
  }
99
104
 
100
105
  export async function loadSupergraphSdlFromStorage({
@@ -103,14 +108,18 @@ export async function loadSupergraphSdlFromStorage({
103
108
  endpoint,
104
109
  errorReportingEndpoint,
105
110
  fetcher,
111
+ requestTimeoutMs,
106
112
  compositionId,
113
+ logger,
107
114
  }: {
108
115
  graphRef: string;
109
116
  apiKey: string;
110
117
  endpoint: string;
111
118
  errorReportingEndpoint?: string;
112
119
  fetcher: Fetcher;
120
+ requestTimeoutMs: number;
113
121
  compositionId: string | null;
122
+ logger: Logger;
114
123
  }) : Promise<SupergraphSdlUpdate | null> {
115
124
  const requestBody = JSON.stringify({
116
125
  query: SUPERGRAPH_SDL_QUERY,
@@ -121,6 +130,12 @@ export async function loadSupergraphSdlFromStorage({
121
130
  },
122
131
  })
123
132
 
133
+ const controller = new AbortController();
134
+ const signal = setTimeout(() => {
135
+ logger.debug(`Aborting request due to timeout`);
136
+ controller.abort();
137
+ }, requestTimeoutMs);
138
+
124
139
  const requestDetails: FetcherRequestInit = {
125
140
  method: 'POST',
126
141
  body: requestBody,
@@ -130,8 +145,11 @@ export async function loadSupergraphSdlFromStorage({
130
145
  'user-agent': `${name}/${version}`,
131
146
  'content-type': 'application/json',
132
147
  },
148
+ signal: controller.signal,
133
149
  };
134
150
 
151
+ logger.debug(`🔧 Fetching ${graphRef} supergraph schema from ${endpoint} ifAfterId ${compositionId}`);
152
+
135
153
  const startTime = new Date();
136
154
  let result: FetcherResponse;
137
155
  try {
@@ -150,6 +168,8 @@ export async function loadSupergraphSdlFromStorage({
150
168
  });
151
169
 
152
170
  throw new UplinkFetcherError(fetchErrorMsg + (e.message ?? e));
171
+ } finally {
172
+ clearTimeout(signal);
153
173
  }
154
174
 
155
175
  const endTime = new Date();
@@ -192,7 +212,7 @@ export async function loadSupergraphSdlFromStorage({
192
212
  minDelaySeconds,
193
213
  // messages,
194
214
  } = routerConfig;
195
- return { id, supergraphSdl: supergraphSdl!, minDelaySeconds };
215
+ return { id, supergraphSdl, minDelaySeconds };
196
216
  } else if (routerConfig.__typename === 'FetchError') {
197
217
  // FetchError case
198
218
  const { code, message } = routerConfig;
@@ -1,10 +1,11 @@
1
- import { Fetcher, FetcherResponse } from '@apollo/utils.fetcher';
1
+ import { FetcherResponse } from '@apollo/utils.fetcher';
2
2
  import { GraphQLError } from 'graphql';
3
3
  import {
4
4
  ErrorCode,
5
5
  OobReportMutation,
6
6
  OobReportMutationVariables,
7
7
  } from '../../__generated__/graphqlTypes';
8
+ import { type AbortableFetcher as Fetcher } from './types';
8
9
 
9
10
  // Magic /* GraphQL */ comment below is for codegen, do not remove
10
11
  export const OUT_OF_BAND_REPORTER_QUERY = /* GraphQL */`#graphql
@@ -0,0 +1,10 @@
1
+ import type { AbortSignal } from 'node-abort-controller';
2
+ import type { Fetcher, FetcherRequestInit } from '@apollo/utils.fetcher';
3
+
4
+ export interface AbortableFetcherRequestInit extends FetcherRequestInit {
5
+ signal?: AbortSignal | null | undefined;
6
+ };
7
+
8
+ export interface AbortableFetcher extends Fetcher {
9
+ init?: AbortableFetcherRequestInit;
10
+ };