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
38 changes: 0 additions & 38 deletions .changeset/beige-teams-spend.md

This file was deleted.

3 changes: 0 additions & 3 deletions packages/libraries/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,11 @@
"graphql": "^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
},
"dependencies": {
"@graphql-hive/signal": "^2.0.0",
"@graphql-tools/utils": "^10.0.0",
"@whatwg-node/fetch": "^0.10.6",
"async-retry": "^1.3.3",
"js-md5": "0.8.3",
"lodash.sortby": "^4.7.0",
"opossum": "^9.0.0",
"tiny-lru": "^8.0.2"
},
"devDependencies": {
Expand All @@ -60,7 +58,6 @@
"@types/async-retry": "1.4.8",
"@types/js-md5": "0.8.0",
"@types/lodash.sortby": "4.7.9",
"@types/opossum": "8.1.9",
"graphql": "16.9.0",
"nock": "14.0.10",
"tslib": "2.8.1",
Expand Down
52 changes: 0 additions & 52 deletions packages/libraries/core/playground/agent-circuit-breaker.ts

This file was deleted.

158 changes: 21 additions & 137 deletions packages/libraries/core/src/client/agent.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,10 @@
import { fetch as defaultFetch } from '@whatwg-node/fetch';
import { version } from '../version.js';
import { http } from './http-client.js';
import type { Logger } from './types.js';
import { CircuitBreakerInterface, createHiveLogger, loadCircuitBreaker } from './utils.js';
import { createHiveLogger } 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 @@ -74,14 +49,7 @@ export interface AgentOptions {
* WHATWG Compatible fetch implementation
* used by the agent to send reports
*/
fetch?: typeof defaultFetch;
/**
* Circuit Breaker Configuration.
* true -> Use default configuration
* false -> Disable
* object -> use custom configuration see {AgentCircuitBreakerConfiguration}
*/
circuitBreaker?: boolean | AgentCircuitBreakerConfiguration;
fetch?: typeof fetch;
}

export function createAgent<TEvent>(
Expand All @@ -100,9 +68,7 @@ export function createAgent<TEvent>(
headers?(): Record<string, string>;
},
) {
const options: Required<Omit<AgentOptions, 'fetch' | 'circuitBreaker' | 'logger' | 'debug'>> & {
circuitBreaker: null | AgentCircuitBreakerConfiguration;
} = {
const options: Required<Omit<AgentOptions, 'fetch' | 'debug' | 'logger'>> = {
timeout: 30_000,
enabled: true,
minTimeout: 200,
Expand All @@ -112,18 +78,9 @@ export function createAgent<TEvent>(
name: 'hive-client',
version,
...pluginOptions,
circuitBreaker:
pluginOptions.circuitBreaker == null || pluginOptions.circuitBreaker === true
? defaultCircuitBreakerConfiguration
: pluginOptions.circuitBreaker === false
? null
: pluginOptions.circuitBreaker,
};

const logger = createHiveLogger(pluginOptions.logger ?? console, '[agent]');

const logger = createHiveLogger(pluginOptions.logger ?? console, '[agent]', pluginOptions.debug);
const enabled = options.enabled !== false;

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

function schedule() {
Expand Down Expand Up @@ -174,27 +131,6 @@ export function createAgent<TEvent>(
return send({ throwOnError: true, skipSchedule: true });
}

async function sendHTTPCall(buffer: string | Buffer<ArrayBufferLike>): Promise<Response> {
const signal = breaker.getSignal();
return await http.post(options.endpoint, buffer, {
headers: {
accept: 'application/json',
'content-type': 'application/json',
Authorization: `Bearer ${options.token}`,
'User-Agent': `${options.name}/${options.version}`,
...headers(),
},
timeout: options.timeout,
retry: {
retries: options.maxRetries,
factor: 2,
},
logger,
fetchImplementation: pluginOptions.fetch,
signal,
});
}

async function send(sendOptions?: {
throwOnError?: boolean;
skipSchedule: boolean;
Expand All @@ -212,7 +148,23 @@ export function createAgent<TEvent>(
data.clear();

logger.debug(`Sending report (queue ${dataToSend})`);
const response = sendFromBreaker(buffer)
const response = await http
.post(options.endpoint, buffer, {
headers: {
accept: 'application/json',
'content-type': 'application/json',
Authorization: `Bearer ${options.token}`,
'User-Agent': `${options.name}/${options.version}`,
...headers(),
},
timeout: options.timeout,
retry: {
retries: options.maxRetries,
factor: 2,
},
logger,
fetchImplementation: pluginOptions.fetch,
})
.then(res => {
logger.debug(`Report sent!`);
return res;
Expand Down Expand Up @@ -251,74 +203,6 @@ export function createAgent<TEvent>(
});
}

let breaker: CircuitBreakerInterface<
Parameters<typeof sendHTTPCall>,
ReturnType<typeof sendHTTPCall>
>;
let loadCircuitBreakerPromise: Promise<void> | null = null;
const breakerLogger = createHiveLogger(logger, '[circuit breaker]');

function noopBreaker(): typeof breaker {
return {
getSignal() {
return undefined;
},
fire: sendHTTPCall,
};
}

if (options.circuitBreaker) {
/**
* We support Cloudflare, which does not has the `events` module.
* So we lazy load opossum which has `events` as a dependency.
*/
breakerLogger.info('initialize circuit breaker');
loadCircuitBreakerPromise = loadCircuitBreaker(
CircuitBreaker => {
breakerLogger.info('started');
const realBreaker = new CircuitBreaker(sendHTTPCall, {
...options.circuitBreaker,
timeout: false,
autoRenewAbortController: true,
});

realBreaker.on('open', () =>
breakerLogger.error('circuit opened - backend seems unreachable.'),
);
realBreaker.on('halfOpen', () =>
breakerLogger.info('circuit half open - testing backend connectivity'),
);
realBreaker.on('close', () => breakerLogger.info('circuit closed - backend recovered '));

// @ts-expect-error missing definition in typedefs for `opposum`
breaker = realBreaker;
},
() => {
breakerLogger.info('circuit breaker not supported on platform');
breaker = noopBreaker();
},
);
} else {
breaker = noopBreaker();
}

async function sendFromBreaker(...args: Parameters<typeof breaker.fire>) {
if (!breaker) {
await loadCircuitBreakerPromise;
}

try {
return await breaker.fire(...args);
} catch (err: unknown) {
if (err instanceof Error && 'code' in err && err.code === 'EOPENBREAKER') {
breakerLogger.info('circuit open - sending report skipped');
return null;
}

throw err;
}
}

return {
capture,
sendImmediately,
Expand Down
18 changes: 1 addition & 17 deletions packages/libraries/core/src/client/http-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import asyncRetry from 'async-retry';
import { abortSignalAny } from '@graphql-hive/signal';
import { crypto, fetch, URL } from '@whatwg-node/fetch';
import { Logger } from './types';

Expand All @@ -22,8 +21,6 @@ interface SharedConfig {
* @default {response => response.ok}
**/
isRequestOk?: ResponseAssertFunction;
/** Optional abort signal */
signal?: AbortSignal;
}

/**
Expand Down Expand Up @@ -81,8 +78,6 @@ export async function makeFetchCall(
* @default {response => response.ok}
**/
isRequestOk?: ResponseAssertFunction;
/** Optional abort signal */
signal?: AbortSignal;
},
): Promise<Response> {
const logger = config.logger;
Expand All @@ -92,9 +87,6 @@ export async function makeFetchCall(
let maxTimeout = 2000;
let factor = 1.2;

const actionHeader =
config.method === 'POST' ? { 'x-client-action-id': crypto.randomUUID() } : undefined;

if (config.retry !== false) {
retries = config.retry?.retries ?? 5;
minTimeout = config.retry?.minTimeout ?? 200;
Expand All @@ -113,15 +105,13 @@ export async function makeFetchCall(
);

const getDuration = measureTime();
const timeoutSignal = AbortSignal.timeout(config.timeout ?? 20_000);
const signal = config.signal ? abortSignalAny([config.signal, timeoutSignal]) : timeoutSignal;
const signal = AbortSignal.timeout(config.timeout ?? 20_000);

const response = await (config.fetchImplementation ?? fetch)(endpoint, {
method: config.method,
body: config.body,
headers: {
'x-request-id': requestId,
...actionHeader,
...config.headers,
},
signal,
Expand Down Expand Up @@ -156,12 +146,6 @@ export async function makeFetchCall(
throw new Error(`Unexpected HTTP error. (x-request-id=${requestId})`, { cause: error });
});

if (config.signal?.aborted === true) {
const error = config.signal.reason ?? new Error('Request aborted externally.');
bail(error);
throw error;
}

if (isRequestOk(response)) {
logger?.debug?.(
`${config.method} ${endpoint} (x-request-id=${requestId}) succeeded with status ${response.status} ${getDuration()}.`,
Expand Down
Loading
Loading