fix(environment): pick best node (#5752)
This commit is contained in:
parent
75e7cea32a
commit
d6084e75a0
@ -23,7 +23,6 @@ import {
|
||||
SUBSCRIPTION_TIMEOUT,
|
||||
useNodeBasicStatus,
|
||||
useNodeSubscriptionStatus,
|
||||
useResponseTime,
|
||||
} from './row-data';
|
||||
import { BLOCK_THRESHOLD, RowData } from './row-data';
|
||||
import { CUSTOM_NODE_KEY } from '../../types';
|
||||
@ -162,19 +161,6 @@ describe('useNodeBasicStatus', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('useResponseTime', () => {
|
||||
it('returns response time when url is valid', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useResponseTime('https://localhost:1234')
|
||||
);
|
||||
expect(result.current.responseTime).toBe(50);
|
||||
});
|
||||
it('does not return response time when url is invalid', () => {
|
||||
const { result } = renderHook(() => useResponseTime('nope'));
|
||||
expect(result.current.responseTime).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RowData', () => {
|
||||
const props = {
|
||||
id: '0',
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { isValidUrl } from '@vegaprotocol/utils';
|
||||
import { TradingRadio } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CUSTOM_NODE_KEY } from '../../types';
|
||||
@ -8,6 +7,7 @@ import {
|
||||
} from '../../utils/__generated__/NodeCheck';
|
||||
import { LayoutCell } from './layout-cell';
|
||||
import { useT } from '../../use-t';
|
||||
import { useResponseTime } from '../../utils/time';
|
||||
|
||||
export const POLL_INTERVAL = 1000;
|
||||
export const SUBSCRIPTION_TIMEOUT = 3000;
|
||||
@ -108,20 +108,6 @@ export const useNodeBasicStatus = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const useResponseTime = (url: string, trigger?: unknown) => {
|
||||
const [responseTime, setResponseTime] = useState<number>();
|
||||
useEffect(() => {
|
||||
if (!isValidUrl(url)) return;
|
||||
if (typeof window.performance.getEntriesByName !== 'function') return; // protection for test environment
|
||||
const requestUrl = new URL(url);
|
||||
const requests = window.performance.getEntriesByName(requestUrl.href);
|
||||
const { duration } =
|
||||
(requests.length && requests[requests.length - 1]) || {};
|
||||
setResponseTime(duration);
|
||||
}, [url, trigger]);
|
||||
return { responseTime };
|
||||
};
|
||||
|
||||
export const RowData = ({
|
||||
id,
|
||||
url,
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
getUserEnabledFeatureFlags,
|
||||
setUserEnabledFeatureFlag,
|
||||
} from './use-environment';
|
||||
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
|
||||
|
||||
const noop = () => {
|
||||
/* no op*/
|
||||
@ -17,6 +18,10 @@ const noop = () => {
|
||||
|
||||
jest.mock('@vegaprotocol/apollo-client');
|
||||
jest.mock('zustand');
|
||||
jest.mock('../utils/time');
|
||||
|
||||
const mockCanMeasureResponseTime = canMeasureResponseTime as jest.Mock;
|
||||
const mockMeasureResponseTime = measureResponseTime as jest.Mock;
|
||||
|
||||
const mockCreateClient = createClient as jest.Mock;
|
||||
const createDefaultMockClient = () => {
|
||||
@ -155,6 +160,14 @@ describe('useEnvironment', () => {
|
||||
const fastNode = 'https://api.n01.foo.vega.xyz';
|
||||
const fastWait = 1000;
|
||||
const nodes = [slowNode, fastNode];
|
||||
|
||||
mockCanMeasureResponseTime.mockImplementation(() => true);
|
||||
mockMeasureResponseTime.mockImplementation((url: string) => {
|
||||
if (url === slowNode) return slowWait;
|
||||
if (url === fastNode) return fastWait;
|
||||
return Infinity;
|
||||
});
|
||||
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(setupFetch({ hosts: nodes }));
|
||||
|
||||
@ -168,7 +181,7 @@ describe('useEnvironment', () => {
|
||||
statistics: {
|
||||
chainId: 'chain-id',
|
||||
blockHeight: '100',
|
||||
vegaTime: new Date().toISOString(),
|
||||
vegaTime: new Date(1).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -196,7 +209,8 @@ describe('useEnvironment', () => {
|
||||
expect(result.current.nodes).toEqual(nodes);
|
||||
});
|
||||
|
||||
jest.runAllTimers();
|
||||
jest.advanceTimersByTime(2000);
|
||||
// jest.runAllTimers();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.status).toEqual('success');
|
||||
|
@ -19,6 +19,9 @@ import { compileErrors } from '../utils/compile-errors';
|
||||
import { envSchema } from '../utils/validate-environment';
|
||||
import { tomlConfigSchema } from '../utils/validate-configuration';
|
||||
import uniq from 'lodash/uniq';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import first from 'lodash/first';
|
||||
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
|
||||
|
||||
type Client = ReturnType<typeof createClient>;
|
||||
type ClientCollection = {
|
||||
@ -38,8 +41,17 @@ export type EnvStore = Env & Actions;
|
||||
|
||||
const VERSION = 1;
|
||||
export const STORAGE_KEY = `vega_url_${VERSION}`;
|
||||
|
||||
const QUERY_TIMEOUT = 3000;
|
||||
const SUBSCRIPTION_TIMEOUT = 3000;
|
||||
|
||||
const raceAgainst = (timeout: number): Promise<false> =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(false);
|
||||
}, timeout);
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch and validate a vega node configuration
|
||||
*/
|
||||
@ -64,53 +76,88 @@ const fetchConfig = async (url?: string) => {
|
||||
const findNode = async (clients: ClientCollection): Promise<string | null> => {
|
||||
const tests = Object.entries(clients).map((args) => testNode(...args));
|
||||
try {
|
||||
const url = await Promise.any(tests);
|
||||
return url;
|
||||
} catch {
|
||||
const nodes = await Promise.all(tests);
|
||||
const responsiveNodes = nodes
|
||||
.filter(([, q, s]) => q && s)
|
||||
.map(([url, q]) => {
|
||||
return {
|
||||
url,
|
||||
...q,
|
||||
};
|
||||
});
|
||||
|
||||
// more recent and faster at the top
|
||||
const ordered = orderBy(
|
||||
responsiveNodes,
|
||||
[(n) => n.blockHeight, (n) => n.vegaTime, (n) => n.responseTime],
|
||||
['desc', 'desc', 'asc']
|
||||
);
|
||||
|
||||
const best = first(ordered);
|
||||
return best ? best.url : null;
|
||||
} catch (err) {
|
||||
// All tests rejected, no suitable node found
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
type Maybe<T> = T | false;
|
||||
type QueryTestResult = {
|
||||
blockHeight: number;
|
||||
vegaTime: Date;
|
||||
responseTime: number;
|
||||
};
|
||||
type SubscriptionTestResult = true;
|
||||
type NodeTestResult = [
|
||||
/** url */
|
||||
string,
|
||||
Maybe<QueryTestResult>,
|
||||
Maybe<SubscriptionTestResult>
|
||||
];
|
||||
/**
|
||||
* Test a node for suitability for connection
|
||||
*/
|
||||
const testNode = async (
|
||||
url: string,
|
||||
client: Client
|
||||
): Promise<string | null> => {
|
||||
): Promise<NodeTestResult> => {
|
||||
const results = await Promise.all([
|
||||
// these promises will only resolve with true/false
|
||||
testQuery(client),
|
||||
testQuery(client, url),
|
||||
testSubscription(client),
|
||||
]);
|
||||
if (results[0] && results[1]) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const message = `Tests failed for node: ${url}`;
|
||||
console.warn(message);
|
||||
|
||||
// throwing here will mean this tests is ignored and a different
|
||||
// node that hopefully does resolve will fulfill the Promise.any
|
||||
throw new Error(message);
|
||||
return [url, ...results];
|
||||
};
|
||||
|
||||
/**
|
||||
* Run a test query on a client
|
||||
*/
|
||||
const testQuery = async (client: Client) => {
|
||||
try {
|
||||
const result = await client.query<NodeCheckQuery>({
|
||||
query: NodeCheckDocument,
|
||||
});
|
||||
if (!result || result.error) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
const testQuery = (
|
||||
client: Client,
|
||||
url: string
|
||||
): Promise<Maybe<QueryTestResult>> => {
|
||||
const test: Promise<Maybe<QueryTestResult>> = new Promise((resolve) =>
|
||||
client
|
||||
.query<NodeCheckQuery>({
|
||||
query: NodeCheckDocument,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result && !result.error) {
|
||||
const res = {
|
||||
blockHeight: Number(result.data.statistics.blockHeight),
|
||||
vegaTime: new Date(result.data.statistics.vegaTime),
|
||||
// only after a request has been sent we can retrieve the response time
|
||||
responseTime: canMeasureResponseTime(url)
|
||||
? measureResponseTime(url) || Infinity
|
||||
: Infinity,
|
||||
} as QueryTestResult;
|
||||
resolve(res);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
})
|
||||
.catch(() => resolve(false))
|
||||
);
|
||||
return Promise.race([test, raceAgainst(QUERY_TIMEOUT)]);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -118,7 +165,9 @@ const testQuery = async (client: Client) => {
|
||||
* that takes longer than SUBSCRIPTION_TIMEOUT ms to respond
|
||||
* is deemed a failure
|
||||
*/
|
||||
const testSubscription = (client: Client) => {
|
||||
const testSubscription = (
|
||||
client: Client
|
||||
): Promise<Maybe<SubscriptionTestResult>> => {
|
||||
return new Promise((resolve) => {
|
||||
const sub = client
|
||||
.subscribe<NodeCheckTimeUpdateSubscription>({
|
||||
|
22
libs/environment/src/utils/time.spec.ts
Normal file
22
libs/environment/src/utils/time.spec.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useResponseTime } from './time';
|
||||
|
||||
const mockResponseTime = 50;
|
||||
global.performance.getEntriesByName = jest.fn().mockReturnValue([
|
||||
{
|
||||
duration: mockResponseTime,
|
||||
},
|
||||
]);
|
||||
|
||||
describe('useResponseTime', () => {
|
||||
it('returns response time when url is valid', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useResponseTime('https://localhost:1234')
|
||||
);
|
||||
expect(result.current.responseTime).toBe(50);
|
||||
});
|
||||
it('does not return response time when url is invalid', () => {
|
||||
const { result } = renderHook(() => useResponseTime('nope'));
|
||||
expect(result.current.responseTime).toBeUndefined();
|
||||
});
|
||||
});
|
25
libs/environment/src/utils/time.ts
Normal file
25
libs/environment/src/utils/time.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { isValidUrl } from '@vegaprotocol/utils';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useResponseTime = (url: string, trigger?: unknown) => {
|
||||
const [responseTime, setResponseTime] = useState<number>();
|
||||
useEffect(() => {
|
||||
if (!canMeasureResponseTime(url)) return;
|
||||
const duration = measureResponseTime(url);
|
||||
setResponseTime(duration);
|
||||
}, [url, trigger]);
|
||||
return { responseTime };
|
||||
};
|
||||
|
||||
export const canMeasureResponseTime = (url: string) => {
|
||||
if (!isValidUrl(url)) return false;
|
||||
if (typeof window.performance.getEntriesByName !== 'function') return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
export const measureResponseTime = (url: string) => {
|
||||
const requestUrl = new URL(url);
|
||||
const requests = window.performance.getEntriesByName(requestUrl.href);
|
||||
const { duration } = (requests.length && requests[requests.length - 1]) || {};
|
||||
return duration;
|
||||
};
|
Loading…
Reference in New Issue
Block a user