Skip to content

Commit 6deb1a5

Browse files
committed
feat: circuit breaker for supergraph fetcher
1 parent 791c025 commit 6deb1a5

File tree

4 files changed

+215
-11
lines changed

4 files changed

+215
-11
lines changed

packages/libraries/apollo/src/index.ts

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import { GraphQLError, type DocumentNode } from 'graphql';
22
import type { ApolloServerPlugin, HTTPGraphQLRequest } from '@apollo/server';
33
import {
44
autoDisposeSymbol,
5+
CDNArtifactFetcherCircuitBreakerConfiguration,
6+
createCDNArtifactFetcher,
57
createHive as createHiveClient,
6-
createSupergraphSDLFetcher,
78
HiveClient,
89
HivePluginOptions,
910
isHiveClient,
10-
SupergraphSDLFetcherOptions,
11+
joinUrl,
12+
Logger,
1113
} from '@graphql-hive/core';
1214
import { version } from './version.js';
1315

@@ -17,14 +19,62 @@ 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;
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?: CDNArtifactFetcherCircuitBreakerConfiguration;
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 pollIntervalInMs = args.pollIntervalInMs ?? 30_000;
62+
const endpoint = args.endpoint.endsWith('/supergraph')
63+
? args.endpoint
64+
: joinUrl(args.endpoint, 'supergraph');
65+
66+
const fetchSupergraph = createCDNArtifactFetcher({
67+
endpoint,
68+
accessKey: args.key,
69+
client: {
70+
name: args.name ?? '@graphql-hive/apollo',
71+
version: args.version ?? version,
72+
},
73+
logger: args.logger,
74+
fetch: args.fetchImplementation,
75+
circuitBreaker: args.circuitBreaker,
76+
});
77+
2878
let timer: ReturnType<typeof setTimeout> | null = null;
2979

3080
return {
@@ -38,8 +88,8 @@ export function createSupergraphManager({
3888
timer = setTimeout(async () => {
3989
try {
4090
const result = await fetchSupergraph();
41-
if (result.supergraphSdl) {
42-
hooks.update?.(result.supergraphSdl);
91+
if (result.contents) {
92+
hooks.update?.(result.contents);
4393
}
4494
} catch (error) {
4595
console.error(
@@ -53,7 +103,7 @@ export function createSupergraphManager({
53103
poll();
54104

55105
return {
56-
supergraphSdl: initialResult.supergraphSdl,
106+
supergraphSdl: initialResult.contents,
57107
cleanup: async () => {
58108
if (timer) {
59109
clearTimeout(timer);
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import CircuitBreaker from '../circuit-breaker/circuit.js';
2+
import { version } from '../version.js';
3+
import { http } from './http-client.js';
4+
import type { Logger } from './types.js';
5+
import { createHash, createHiveLogger } from './utils.js';
6+
7+
export type CDNArtifactFetcherCircuitBreakerConfiguration = {
8+
/**
9+
* Percentage after what the circuit breaker should kick in.
10+
* Default: 50
11+
*/
12+
errorThresholdPercentage: number;
13+
/**
14+
* Count of requests before starting evaluating.
15+
* Default: 5
16+
*/
17+
volumeThreshold: number;
18+
/**
19+
* After what time the circuit breaker is attempting to retry sending requests in milliseconds
20+
* Default: 30_000
21+
*/
22+
resetTimeout: number;
23+
};
24+
25+
const defaultCircuitBreakerConfiguration: CDNArtifactFetcherCircuitBreakerConfiguration = {
26+
errorThresholdPercentage: 50,
27+
volumeThreshold: 10,
28+
resetTimeout: 30_000,
29+
};
30+
31+
type CreateCDNArtifactFetcherArgs = {
32+
endpoint: string;
33+
accessKey: string;
34+
/** client meta data */
35+
client?: {
36+
name: string;
37+
version: string;
38+
};
39+
circuitBreaker?: CDNArtifactFetcherCircuitBreakerConfiguration;
40+
logger?: Logger;
41+
fetch?: typeof fetch;
42+
};
43+
44+
type CDNFetcherArgs = {
45+
logger?: Logger;
46+
fetch?: typeof fetch;
47+
};
48+
49+
type CDNFetchResult = {
50+
/** Text contents of the artifact */
51+
contents: string;
52+
/** SHA-256 Hash */
53+
hash: string;
54+
/** Schema Version ID as on Hive Console (optional) */
55+
schemaVersionId: null | string;
56+
};
57+
58+
function isRequestOk(response: Response) {
59+
return response.status === 304 || response.ok;
60+
}
61+
62+
/**
63+
* Create a handler for fetching a CDN artifact with built-in cache and circuit breaker.
64+
* It is intended for polling supergraph, schema sdl or services.
65+
*/
66+
export function createCDNArtifactFetcher(args: CreateCDNArtifactFetcherArgs) {
67+
let cacheETag: string | null = null;
68+
let cached: CDNFetchResult | null = null;
69+
const clientInfo = args.client ?? { name: 'hive-client', version };
70+
const circuitBreakerConfig = args.circuitBreaker ?? defaultCircuitBreakerConfiguration;
71+
72+
const circuitBreaker = new CircuitBreaker(
73+
function runFetch(fetchArgs?: CDNFetcherArgs) {
74+
const signal = circuitBreaker.getSignal();
75+
const logger = createHiveLogger(fetchArgs?.logger ?? args.logger ?? console, '');
76+
const fetchImplementation = fetchArgs?.fetch ?? args.fetch;
77+
78+
const headers: {
79+
[key: string]: string;
80+
} = {
81+
'X-Hive-CDN-Key': args.accessKey,
82+
'User-Agent': `${clientInfo.name}/${clientInfo.version}`,
83+
};
84+
85+
if (cacheETag) {
86+
headers['If-None-Match'] = cacheETag;
87+
}
88+
89+
return http.get(args.endpoint, {
90+
headers,
91+
isRequestOk,
92+
retry: {
93+
retries: 10,
94+
maxTimeout: 200,
95+
minTimeout: 1,
96+
},
97+
logger,
98+
fetchImplementation,
99+
signal,
100+
});
101+
},
102+
{
103+
...circuitBreakerConfig,
104+
timeout: false,
105+
autoRenewAbortController: true,
106+
},
107+
);
108+
109+
return async function fetchArtifact(fetchArgs?: CDNFetcherArgs): Promise<CDNFetchResult> {
110+
try {
111+
const response = await circuitBreaker.fire(fetchArgs);
112+
113+
if (response.status === 304) {
114+
if (cached !== null) {
115+
return cached;
116+
}
117+
throw new Error('This should never happen.');
118+
}
119+
120+
const contents = await response.text();
121+
const result: CDNFetchResult = {
122+
hash: await createHash('SHA-256').update(contents).digest('base64'),
123+
contents,
124+
schemaVersionId: response.headers.get('x-hive-schema-version-id') || null,
125+
};
126+
127+
const etag = response.headers.get('etag');
128+
if (etag) {
129+
cached = result;
130+
cacheETag = etag;
131+
}
132+
133+
return result;
134+
} catch (err) {
135+
if (err instanceof Error && 'code' in err && err.code === 'EOPENBREAKER') {
136+
if (cached) {
137+
return cached;
138+
}
139+
}
140+
141+
throw err;
142+
}
143+
};
144+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { http } from './http-client.js';
33
import type { Logger } from './types.js';
44
import { createHash, joinUrl } from './utils.js';
55

6+
/**
7+
* @deprecated Please use {createCDNArtifactFetcher} instead of createSupergraphSDLFetcher.
8+
*/
69
export interface SupergraphSDLFetcherOptions {
710
endpoint: string;
811
key: string;
@@ -12,6 +15,9 @@ export interface SupergraphSDLFetcherOptions {
1215
version?: string;
1316
}
1417

18+
/**
19+
* @deprecated Please use {createCDNArtifactFetcher} instead.
20+
*/
1521
export function createSupergraphSDLFetcher(options: SupergraphSDLFetcherOptions) {
1622
let cacheETag: string | null = null;
1723
let cached: {

packages/libraries/core/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ export { isHiveClient, isAsyncIterable, createHash, joinUrl } from './client/uti
1313
export { http, URL } from './client/http-client.js';
1414
export { createSupergraphSDLFetcher } from './client/supergraph.js';
1515
export type { SupergraphSDLFetcherOptions } from './client/supergraph.js';
16+
export {
17+
createCDNArtifactFetcher,
18+
type CDNArtifactFetcherCircuitBreakerConfiguration,
19+
} from './client/artifacts.js';

0 commit comments

Comments
 (0)