Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .changeset/calm-mails-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
'@graphql-hive/core': minor
'@graphql-hive/apollo': minor
'@graphql-hive/yoga': minor
---

**Persisted Documents Improvements**

Persisted documents now support specifying a mirror endpoint that will be used in case the main CDN
is unreachable. Provide an array of endpoints to the client configuration.

```ts
import { createClient } from '@graphql-hive/core'

const client = createClient({
experimental__persistedDocuments: {
cdn: {
endpoint: [
'https://cdn.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688',
'https://cdn-mirror.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688'
],
accessToken: ''
}
}
})
```

In addition to that, the underlying logic for looking up documents now uses a circuit breaker. If a
single endpoint is unreachable, further lookups on that endpoint are skipped.

The behaviour of the circuit breaker can be customized via the `circuitBreaker` configuration.

```ts
import { createClient } from '@graphql-hive/core'

const client = createClient({
experimental__persistedDocuments: {
cdn: {
endpoint: [
'https://cdn.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688',
'https://cdn-mirror.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688'
],
accessToken: ''
},
circuitBreaker: {
// open circuit if 50 percent of request result in an error
errorThresholdPercentage: 50,
// start monitoring the circuit after 10 requests
volumeThreshold: 10,
// time before the backend is tried again after the circuit is open
resetTimeout: 30_000
}
Comment on lines +45 to +52
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't circuit breaker options be under the cdn key ? Since it's the CDN's circuit breaker ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember I introduced accessToken and endpoint under cdn, so it is obvious you need to provide credentials for the CDN.

Should fetch then also be under cdn then? I don't feel the strong need to move it there or not. For me it is just fine if it is a name space for credentials.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you whish, I don't have strong opinion on it, I just found it surprising by reading the changelog :-)

Anyway, it is typed and documented, so not a big deal.

}
})
```
43 changes: 43 additions & 0 deletions .changeset/nine-worlds-slide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
'@graphql-hive/apollo': minor
---

**Supergraph Manager Improvements**

Persisted documents now support specifying a mirror endpoint that will be used in case the main CDN
is unreachable. Provide an array of endpoints to the supergraph manager configuration.

```ts
import { createSupergraphManager } from '@graphql-hive/apollo'

const supergraphManager = createSupergraphManager({
endpoint: [
'https://cdn.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688/supergraph',
'https://cdn-mirror.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688/supergraph'
],
key: ''
})
```

In addition to that, the underlying logic for looking up documents now uses a circuit breaker. If a
single endpoint is unreachable, further lookups on that endpoint are skipped.

```ts
import { createSupergraphManager } from '@graphql-hive/apollo'

const supergraphManager = createSupergraphManager({
endpoint: [
'https://cdn.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688/supergraph',
'https://cdn-mirror.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688/supergraph'
],
key: '',
circuitBreaker: {
// open circuit if 50 percent of request result in an error
errorThresholdPercentage: 50,
// start monitoring the circuit after 10 requests
volumeThreshold: 10,
// time before the backend is tried again after the circuit is open
resetTimeout: 30_000
}
})
```
25 changes: 25 additions & 0 deletions .changeset/upset-lemons-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'@graphql-hive/core': minor
---

**New CDN Artifact Fetcher**

We have a new interface for fetching CDN artifacts (such as supergraph and services) with a cache
from the CDN. This fetcher supports providing a mirror endpoint and comes with a circuit breaker
under the hood.

```ts
const supergraphFetcher = createCDNArtifactFetcher({
endpoint: [
'https://cdn.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688',
'https://cdn-mirror.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688'
],
accessKey: ''
})

supergraphFetcher.fetch()
```

---

`createSupergraphSDLFetcher` is now deprecated. Please upgrade to use `createCDNArtifactFetcher`.
3 changes: 2 additions & 1 deletion packages/libraries/apollo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
"graphql": "^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
},
"dependencies": {
"@graphql-hive/core": "workspace:*"
"@graphql-hive/core": "workspace:*",
"@graphql-hive/logger": "^1.0.9"
},
"devDependencies": {
"@apollo/server": "5.0.0",
Expand Down
84 changes: 68 additions & 16 deletions packages/libraries/apollo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { GraphQLError, type DocumentNode } from 'graphql';
import type { ApolloServerPlugin, HTTPGraphQLRequest } from '@apollo/server';
import {
autoDisposeSymbol,
createCDNArtifactFetcher,
createHive as createHiveClient,
createSupergraphSDLFetcher,
HiveClient,
HivePluginOptions,
isHiveClient,
SupergraphSDLFetcherOptions,
joinUrl,
type CircuitBreakerConfiguration,
} from '@graphql-hive/core';
import { Logger } from '@graphql-hive/logger';
import { version } from './version.js';

export {
Expand All @@ -17,34 +19,83 @@ export {
createServicesFetcher,
createSupergraphSDLFetcher,
} from '@graphql-hive/core';

/** @deprecated Use {CreateSupergraphManagerArgs} instead */
export type { SupergraphSDLFetcherOptions } from '@graphql-hive/core';

export function createSupergraphManager({
pollIntervalInMs,
...superGraphFetcherOptions
}: { pollIntervalInMs?: number } & SupergraphSDLFetcherOptions) {
pollIntervalInMs = pollIntervalInMs ?? 30_000;
const fetchSupergraph = createSupergraphSDLFetcher(superGraphFetcherOptions);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I introduced a new CDNArtifactFetcher that also has a cleanup method, which was needed due to the circuit breaker.

/**
* Configuration for {createSupergraphManager}.
*/
export type CreateSupergraphManagerArgs = {
/**
* The artifact endpoint to poll.
* E.g. `https://cdn.graphql-hive.com/<uuid>/supergraph`
*/
endpoint: string | [string, string];
/**
* The CDN access key for fetching artifact.
*/
key: string;
logger?: Logger;
/**
* The supergraph poll interval in milliseconds
* Default: 30_000
*/
pollIntervalInMs?: number;
/** Circuit breaker configuration override. */
circuitBreaker?: CircuitBreakerConfiguration;
fetchImplementation?: typeof fetch;
/**
* Client name override
* Default: `@graphql-hive/apollo`
*/
name?: string;
/**
* Client version override
* Default: currents package version
*/
version?: string;
};

export function createSupergraphManager(args: CreateSupergraphManagerArgs) {
const logger = args.logger ?? new Logger({ level: false });
const pollIntervalInMs = args.pollIntervalInMs ?? 30_000;
let endpoints = Array.isArray(args.endpoint) ? args.endpoint : [args.endpoint];

const endpoint = endpoints.map(endpoint =>
endpoint.endsWith('/supergraph') ? endpoint : joinUrl(endpoint, 'supergraph'),
);

const artifactsFetcher = createCDNArtifactFetcher({
endpoint: endpoint as [string, string],
accessKey: args.key,
client: {
name: args.name ?? '@graphql-hive/apollo',
version: args.version ?? version,
},
logger,
fetch: args.fetchImplementation,
circuitBreaker: args.circuitBreaker,
});

let timer: ReturnType<typeof setTimeout> | null = null;

return {
async initialize(hooks: { update(supergraphSdl: string): void }): Promise<{
supergraphSdl: string;
cleanup?: () => Promise<void>;
}> {
const initialResult = await fetchSupergraph();
const initialResult = await artifactsFetcher.fetch();

function poll() {
timer = setTimeout(async () => {
try {
const result = await fetchSupergraph();
if (result.supergraphSdl) {
hooks.update?.(result.supergraphSdl);
const result = await artifactsFetcher.fetch();
if (result.contents) {
hooks.update?.(result.contents);
}
} catch (error) {
console.error(
`Failed to update supergraph: ${error instanceof Error ? error.message : error}`,
);
logger.error({ error }, `Failed to update supergraph.`);
}
poll();
}, pollIntervalInMs);
Expand All @@ -53,11 +104,12 @@ export function createSupergraphManager({
poll();

return {
supergraphSdl: initialResult.supergraphSdl,
supergraphSdl: initialResult.contents,
cleanup: async () => {
if (timer) {
clearTimeout(timer);
}
artifactsFetcher.dispose();
},
};
},
Expand Down
32 changes: 6 additions & 26 deletions packages/libraries/core/src/client/agent.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,15 @@
import CircuitBreaker from '../circuit-breaker/circuit.js';
import { version } from '../version.js';
import {
CircuitBreakerConfiguration,
defaultCircuitBreakerConfiguration,
} from './circuit-breaker.js';
import { http } from './http-client.js';
import type { LegacyLogger } from './types.js';
import { chooseLogger } from './utils.js';

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

export type AgentCircuitBreakerConfiguration = {
/**
* Percentage after what the circuit breaker should kick in.
* Default: 50
*/
errorThresholdPercentage: number;
/**
* Count of requests before starting evaluating.
* Default: 5
*/
volumeThreshold: number;
/**
* After what time the circuit breaker is attempting to retry sending requests in milliseconds
* Default: 30_000
*/
resetTimeout: number;
};

const defaultCircuitBreakerConfiguration: AgentCircuitBreakerConfiguration = {
errorThresholdPercentage: 50,
volumeThreshold: 10,
resetTimeout: 30_000,
};

export interface AgentOptions {
enabled?: boolean;
name?: string;
Expand Down Expand Up @@ -80,7 +60,7 @@ export interface AgentOptions {
* false -> Disable
* object -> use custom configuration see {AgentCircuitBreakerConfiguration}
*/
circuitBreaker?: boolean | AgentCircuitBreakerConfiguration;
circuitBreaker?: boolean | CircuitBreakerConfiguration;
/**
* WHATWG Compatible fetch implementation
* used by the agent to send reports
Expand All @@ -105,7 +85,7 @@ export function createAgent<TEvent>(
},
) {
const options: Required<Omit<AgentOptions, 'fetch' | 'debug' | 'logger' | 'circuitBreaker'>> & {
circuitBreaker: AgentCircuitBreakerConfiguration | null;
circuitBreaker: CircuitBreakerConfiguration | null;
} = {
timeout: 30_000,
enabled: true,
Expand Down
Loading
Loading