fix(environment): pick best node (#5752)
This commit is contained in:
parent
75e7cea32a
commit
d6084e75a0
@ -23,7 +23,6 @@ import {
|
|||||||
SUBSCRIPTION_TIMEOUT,
|
SUBSCRIPTION_TIMEOUT,
|
||||||
useNodeBasicStatus,
|
useNodeBasicStatus,
|
||||||
useNodeSubscriptionStatus,
|
useNodeSubscriptionStatus,
|
||||||
useResponseTime,
|
|
||||||
} from './row-data';
|
} from './row-data';
|
||||||
import { BLOCK_THRESHOLD, RowData } from './row-data';
|
import { BLOCK_THRESHOLD, RowData } from './row-data';
|
||||||
import { CUSTOM_NODE_KEY } from '../../types';
|
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', () => {
|
describe('RowData', () => {
|
||||||
const props = {
|
const props = {
|
||||||
id: '0',
|
id: '0',
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { isValidUrl } from '@vegaprotocol/utils';
|
|
||||||
import { TradingRadio } from '@vegaprotocol/ui-toolkit';
|
import { TradingRadio } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { CUSTOM_NODE_KEY } from '../../types';
|
import { CUSTOM_NODE_KEY } from '../../types';
|
||||||
@ -8,6 +7,7 @@ import {
|
|||||||
} from '../../utils/__generated__/NodeCheck';
|
} from '../../utils/__generated__/NodeCheck';
|
||||||
import { LayoutCell } from './layout-cell';
|
import { LayoutCell } from './layout-cell';
|
||||||
import { useT } from '../../use-t';
|
import { useT } from '../../use-t';
|
||||||
|
import { useResponseTime } from '../../utils/time';
|
||||||
|
|
||||||
export const POLL_INTERVAL = 1000;
|
export const POLL_INTERVAL = 1000;
|
||||||
export const SUBSCRIPTION_TIMEOUT = 3000;
|
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 = ({
|
export const RowData = ({
|
||||||
id,
|
id,
|
||||||
url,
|
url,
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
getUserEnabledFeatureFlags,
|
getUserEnabledFeatureFlags,
|
||||||
setUserEnabledFeatureFlag,
|
setUserEnabledFeatureFlag,
|
||||||
} from './use-environment';
|
} from './use-environment';
|
||||||
|
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
|
||||||
|
|
||||||
const noop = () => {
|
const noop = () => {
|
||||||
/* no op*/
|
/* no op*/
|
||||||
@ -17,6 +18,10 @@ const noop = () => {
|
|||||||
|
|
||||||
jest.mock('@vegaprotocol/apollo-client');
|
jest.mock('@vegaprotocol/apollo-client');
|
||||||
jest.mock('zustand');
|
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 mockCreateClient = createClient as jest.Mock;
|
||||||
const createDefaultMockClient = () => {
|
const createDefaultMockClient = () => {
|
||||||
@ -155,6 +160,14 @@ describe('useEnvironment', () => {
|
|||||||
const fastNode = 'https://api.n01.foo.vega.xyz';
|
const fastNode = 'https://api.n01.foo.vega.xyz';
|
||||||
const fastWait = 1000;
|
const fastWait = 1000;
|
||||||
const nodes = [slowNode, fastNode];
|
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
|
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||||
global.fetch.mockImplementation(setupFetch({ hosts: nodes }));
|
global.fetch.mockImplementation(setupFetch({ hosts: nodes }));
|
||||||
|
|
||||||
@ -168,7 +181,7 @@ describe('useEnvironment', () => {
|
|||||||
statistics: {
|
statistics: {
|
||||||
chainId: 'chain-id',
|
chainId: 'chain-id',
|
||||||
blockHeight: '100',
|
blockHeight: '100',
|
||||||
vegaTime: new Date().toISOString(),
|
vegaTime: new Date(1).toISOString(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -196,7 +209,8 @@ describe('useEnvironment', () => {
|
|||||||
expect(result.current.nodes).toEqual(nodes);
|
expect(result.current.nodes).toEqual(nodes);
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.runAllTimers();
|
jest.advanceTimersByTime(2000);
|
||||||
|
// jest.runAllTimers();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(result.current.status).toEqual('success');
|
expect(result.current.status).toEqual('success');
|
||||||
|
@ -19,6 +19,9 @@ import { compileErrors } from '../utils/compile-errors';
|
|||||||
import { envSchema } from '../utils/validate-environment';
|
import { envSchema } from '../utils/validate-environment';
|
||||||
import { tomlConfigSchema } from '../utils/validate-configuration';
|
import { tomlConfigSchema } from '../utils/validate-configuration';
|
||||||
import uniq from 'lodash/uniq';
|
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 Client = ReturnType<typeof createClient>;
|
||||||
type ClientCollection = {
|
type ClientCollection = {
|
||||||
@ -38,8 +41,17 @@ export type EnvStore = Env & Actions;
|
|||||||
|
|
||||||
const VERSION = 1;
|
const VERSION = 1;
|
||||||
export const STORAGE_KEY = `vega_url_${VERSION}`;
|
export const STORAGE_KEY = `vega_url_${VERSION}`;
|
||||||
|
|
||||||
|
const QUERY_TIMEOUT = 3000;
|
||||||
const SUBSCRIPTION_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
|
* 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 findNode = async (clients: ClientCollection): Promise<string | null> => {
|
||||||
const tests = Object.entries(clients).map((args) => testNode(...args));
|
const tests = Object.entries(clients).map((args) => testNode(...args));
|
||||||
try {
|
try {
|
||||||
const url = await Promise.any(tests);
|
const nodes = await Promise.all(tests);
|
||||||
return url;
|
const responsiveNodes = nodes
|
||||||
} catch {
|
.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
|
// All tests rejected, no suitable node found
|
||||||
return null;
|
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
|
* Test a node for suitability for connection
|
||||||
*/
|
*/
|
||||||
const testNode = async (
|
const testNode = async (
|
||||||
url: string,
|
url: string,
|
||||||
client: Client
|
client: Client
|
||||||
): Promise<string | null> => {
|
): Promise<NodeTestResult> => {
|
||||||
const results = await Promise.all([
|
const results = await Promise.all([
|
||||||
// these promises will only resolve with true/false
|
testQuery(client, url),
|
||||||
testQuery(client),
|
|
||||||
testSubscription(client),
|
testSubscription(client),
|
||||||
]);
|
]);
|
||||||
if (results[0] && results[1]) {
|
return [url, ...results];
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a test query on a client
|
* Run a test query on a client
|
||||||
*/
|
*/
|
||||||
const testQuery = async (client: Client) => {
|
const testQuery = (
|
||||||
try {
|
client: Client,
|
||||||
const result = await client.query<NodeCheckQuery>({
|
url: string
|
||||||
query: NodeCheckDocument,
|
): Promise<Maybe<QueryTestResult>> => {
|
||||||
});
|
const test: Promise<Maybe<QueryTestResult>> = new Promise((resolve) =>
|
||||||
if (!result || result.error) {
|
client
|
||||||
return false;
|
.query<NodeCheckQuery>({
|
||||||
}
|
query: NodeCheckDocument,
|
||||||
return true;
|
})
|
||||||
} catch (err) {
|
.then((result) => {
|
||||||
return false;
|
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
|
* that takes longer than SUBSCRIPTION_TIMEOUT ms to respond
|
||||||
* is deemed a failure
|
* is deemed a failure
|
||||||
*/
|
*/
|
||||||
const testSubscription = (client: Client) => {
|
const testSubscription = (
|
||||||
|
client: Client
|
||||||
|
): Promise<Maybe<SubscriptionTestResult>> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const sub = client
|
const sub = client
|
||||||
.subscribe<NodeCheckTimeUpdateSubscription>({
|
.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