Skip to content

Commit a6f707b

Browse files
authored
feat(js-sdk): circuit breaker and cdn mirror for CDN apis (#7287)
1 parent f02409e commit a6f707b

17 files changed

+743
-92
lines changed

.changeset/calm-mails-sneeze.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
'@graphql-hive/core': minor
3+
'@graphql-hive/apollo': minor
4+
'@graphql-hive/yoga': minor
5+
---
6+
7+
**Persisted Documents Improvements**
8+
9+
Persisted documents now support specifying a mirror endpoint that will be used in case the main CDN
10+
is unreachable. Provide an array of endpoints to the client configuration.
11+
12+
```ts
13+
import { createClient } from '@graphql-hive/core'
14+
15+
const client = createClient({
16+
experimental__persistedDocuments: {
17+
cdn: {
18+
endpoint: [
19+
'https://cdn.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688',
20+
'https://cdn-mirror.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688'
21+
],
22+
accessToken: ''
23+
}
24+
}
25+
})
26+
```
27+
28+
In addition to that, the underlying logic for looking up documents now uses a circuit breaker. If a
29+
single endpoint is unreachable, further lookups on that endpoint are skipped.
30+
31+
The behaviour of the circuit breaker can be customized via the `circuitBreaker` configuration.
32+
33+
```ts
34+
import { createClient } from '@graphql-hive/core'
35+
36+
const client = createClient({
37+
experimental__persistedDocuments: {
38+
cdn: {
39+
endpoint: [
40+
'https://cdn.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688',
41+
'https://cdn-mirror.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688'
42+
],
43+
accessToken: ''
44+
},
45+
circuitBreaker: {
46+
// open circuit if 50 percent of request result in an error
47+
errorThresholdPercentage: 50,
48+
// start monitoring the circuit after 10 requests
49+
volumeThreshold: 10,
50+
// time before the backend is tried again after the circuit is open
51+
resetTimeout: 30_000
52+
}
53+
}
54+
})
55+
```

.changeset/nine-worlds-slide.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
'@graphql-hive/apollo': minor
3+
---
4+
5+
**Supergraph Manager Improvements**
6+
7+
Persisted documents now support specifying a mirror endpoint that will be used in case the main CDN
8+
is unreachable. Provide an array of endpoints to the supergraph manager configuration.
9+
10+
```ts
11+
import { createSupergraphManager } from '@graphql-hive/apollo'
12+
13+
const supergraphManager = createSupergraphManager({
14+
endpoint: [
15+
'https://cdn.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688/supergraph',
16+
'https://cdn-mirror.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688/supergraph'
17+
],
18+
key: ''
19+
})
20+
```
21+
22+
In addition to that, the underlying logic for looking up documents now uses a circuit breaker. If a
23+
single endpoint is unreachable, further lookups on that endpoint are skipped.
24+
25+
```ts
26+
import { createSupergraphManager } from '@graphql-hive/apollo'
27+
28+
const supergraphManager = createSupergraphManager({
29+
endpoint: [
30+
'https://cdn.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688/supergraph',
31+
'https://cdn-mirror.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688/supergraph'
32+
],
33+
key: '',
34+
circuitBreaker: {
35+
// open circuit if 50 percent of request result in an error
36+
errorThresholdPercentage: 50,
37+
// start monitoring the circuit after 10 requests
38+
volumeThreshold: 10,
39+
// time before the backend is tried again after the circuit is open
40+
resetTimeout: 30_000
41+
}
42+
})
43+
```

.changeset/upset-lemons-reply.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@graphql-hive/core': minor
3+
---
4+
5+
**New CDN Artifact Fetcher**
6+
7+
We have a new interface for fetching CDN artifacts (such as supergraph and services) with a cache
8+
from the CDN. This fetcher supports providing a mirror endpoint and comes with a circuit breaker
9+
under the hood.
10+
11+
```ts
12+
const supergraphFetcher = createCDNArtifactFetcher({
13+
endpoint: [
14+
'https://cdn.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688',
15+
'https://cdn-mirror.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688'
16+
],
17+
accessKey: ''
18+
})
19+
20+
supergraphFetcher.fetch()
21+
```
22+
23+
---
24+
25+
`createSupergraphSDLFetcher` is now deprecated. Please upgrade to use `createCDNArtifactFetcher`.

packages/libraries/apollo/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
"graphql": "^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
4848
},
4949
"dependencies": {
50-
"@graphql-hive/core": "workspace:*"
50+
"@graphql-hive/core": "workspace:*",
51+
"@graphql-hive/logger": "^1.0.9"
5152
},
5253
"devDependencies": {
5354
"@apollo/server": "5.0.0",

packages/libraries/apollo/src/index.ts

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import { GraphQLError, type DocumentNode } from 'graphql';
22
import type { ApolloServerPlugin, HTTPGraphQLRequest } from '@apollo/server';
33
import {
44
autoDisposeSymbol,
5+
createCDNArtifactFetcher,
56
createHive as createHiveClient,
6-
createSupergraphSDLFetcher,
77
HiveClient,
88
HivePluginOptions,
99
isHiveClient,
10-
SupergraphSDLFetcherOptions,
10+
joinUrl,
11+
type CircuitBreakerConfiguration,
1112
} from '@graphql-hive/core';
13+
import { Logger } from '@graphql-hive/logger';
1214
import { version } from './version.js';
1315

1416
export {
@@ -17,34 +19,83 @@ export {
1719
createServicesFetcher,
1820
createSupergraphSDLFetcher,
1921
} from '@graphql-hive/core';
22+
23+
/** @deprecated Use {CreateSupergraphManagerArgs} instead */
2024
export type { SupergraphSDLFetcherOptions } from '@graphql-hive/core';
2125

22-
export function createSupergraphManager({
23-
pollIntervalInMs,
24-
...superGraphFetcherOptions
25-
}: { pollIntervalInMs?: number } & SupergraphSDLFetcherOptions) {
26-
pollIntervalInMs = pollIntervalInMs ?? 30_000;
27-
const fetchSupergraph = createSupergraphSDLFetcher(superGraphFetcherOptions);
26+
/**
27+
* Configuration for {createSupergraphManager}.
28+
*/
29+
export type CreateSupergraphManagerArgs = {
30+
/**
31+
* The artifact endpoint to poll.
32+
* E.g. `https://cdn.graphql-hive.com/<uuid>/supergraph`
33+
*/
34+
endpoint: string | [string, string];
35+
/**
36+
* The CDN access key for fetching artifact.
37+
*/
38+
key: string;
39+
logger?: Logger;
40+
/**
41+
* The supergraph poll interval in milliseconds
42+
* Default: 30_000
43+
*/
44+
pollIntervalInMs?: number;
45+
/** Circuit breaker configuration override. */
46+
circuitBreaker?: CircuitBreakerConfiguration;
47+
fetchImplementation?: typeof fetch;
48+
/**
49+
* Client name override
50+
* Default: `@graphql-hive/apollo`
51+
*/
52+
name?: string;
53+
/**
54+
* Client version override
55+
* Default: currents package version
56+
*/
57+
version?: string;
58+
};
59+
60+
export function createSupergraphManager(args: CreateSupergraphManagerArgs) {
61+
const logger = args.logger ?? new Logger({ level: false });
62+
const pollIntervalInMs = args.pollIntervalInMs ?? 30_000;
63+
let endpoints = Array.isArray(args.endpoint) ? args.endpoint : [args.endpoint];
64+
65+
const endpoint = endpoints.map(endpoint =>
66+
endpoint.endsWith('/supergraph') ? endpoint : joinUrl(endpoint, 'supergraph'),
67+
);
68+
69+
const artifactsFetcher = createCDNArtifactFetcher({
70+
endpoint: endpoint as [string, string],
71+
accessKey: args.key,
72+
client: {
73+
name: args.name ?? '@graphql-hive/apollo',
74+
version: args.version ?? version,
75+
},
76+
logger,
77+
fetch: args.fetchImplementation,
78+
circuitBreaker: args.circuitBreaker,
79+
});
80+
2881
let timer: ReturnType<typeof setTimeout> | null = null;
2982

3083
return {
3184
async initialize(hooks: { update(supergraphSdl: string): void }): Promise<{
3285
supergraphSdl: string;
3386
cleanup?: () => Promise<void>;
3487
}> {
35-
const initialResult = await fetchSupergraph();
88+
const initialResult = await artifactsFetcher.fetch();
3689

3790
function poll() {
3891
timer = setTimeout(async () => {
3992
try {
40-
const result = await fetchSupergraph();
41-
if (result.supergraphSdl) {
42-
hooks.update?.(result.supergraphSdl);
93+
const result = await artifactsFetcher.fetch();
94+
if (result.contents) {
95+
hooks.update?.(result.contents);
4396
}
4497
} catch (error) {
45-
console.error(
46-
`Failed to update supergraph: ${error instanceof Error ? error.message : error}`,
47-
);
98+
logger.error({ error }, `Failed to update supergraph.`);
4899
}
49100
poll();
50101
}, pollIntervalInMs);
@@ -53,11 +104,12 @@ export function createSupergraphManager({
53104
poll();
54105

55106
return {
56-
supergraphSdl: initialResult.supergraphSdl,
107+
supergraphSdl: initialResult.contents,
57108
cleanup: async () => {
58109
if (timer) {
59110
clearTimeout(timer);
60111
}
112+
artifactsFetcher.dispose();
61113
},
62114
};
63115
},

packages/libraries/core/src/client/agent.ts

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,15 @@
11
import CircuitBreaker from '../circuit-breaker/circuit.js';
22
import { version } from '../version.js';
3+
import {
4+
CircuitBreakerConfiguration,
5+
defaultCircuitBreakerConfiguration,
6+
} from './circuit-breaker.js';
37
import { http } from './http-client.js';
48
import type { LegacyLogger } from './types.js';
59
import { chooseLogger } from './utils.js';
610

711
type ReadOnlyResponse = Pick<Response, 'status' | 'text' | 'json' | 'statusText'>;
812

9-
export type AgentCircuitBreakerConfiguration = {
10-
/**
11-
* Percentage after what the circuit breaker should kick in.
12-
* Default: 50
13-
*/
14-
errorThresholdPercentage: number;
15-
/**
16-
* Count of requests before starting evaluating.
17-
* Default: 5
18-
*/
19-
volumeThreshold: number;
20-
/**
21-
* After what time the circuit breaker is attempting to retry sending requests in milliseconds
22-
* Default: 30_000
23-
*/
24-
resetTimeout: number;
25-
};
26-
27-
const defaultCircuitBreakerConfiguration: AgentCircuitBreakerConfiguration = {
28-
errorThresholdPercentage: 50,
29-
volumeThreshold: 10,
30-
resetTimeout: 30_000,
31-
};
32-
3313
export interface AgentOptions {
3414
enabled?: boolean;
3515
name?: string;
@@ -76,7 +56,7 @@ export interface AgentOptions {
7656
* false -> Disable
7757
* object -> use custom configuration see {AgentCircuitBreakerConfiguration}
7858
*/
79-
circuitBreaker?: boolean | AgentCircuitBreakerConfiguration;
59+
circuitBreaker?: boolean | CircuitBreakerConfiguration;
8060
/**
8161
* WHATWG Compatible fetch implementation
8262
* used by the agent to send reports
@@ -103,7 +83,7 @@ export function createAgent<TEvent>(
10383
},
10484
) {
10585
const options: Required<Omit<AgentOptions, 'fetch' | 'debug' | 'logger' | 'circuitBreaker'>> & {
106-
circuitBreaker: AgentCircuitBreakerConfiguration | null;
86+
circuitBreaker: CircuitBreakerConfiguration | null;
10787
} = {
10888
timeout: 30_000,
10989
enabled: true,

0 commit comments

Comments
 (0)