asset-swapper: Punish latent RFQT makers

This commit is contained in:
F. Eugene Aumson
2020-09-10 16:23:37 -04:00
parent 7b0a1c3630
commit 1ce8a33937
2 changed files with 58 additions and 4 deletions

View File

@@ -11,6 +11,7 @@ import { constants } from '../constants';
import { MarketOperation, RfqtMakerAssetOfferings, RfqtRequestOpts } from '../types'; import { MarketOperation, RfqtMakerAssetOfferings, RfqtRequestOpts } from '../types';
import { ONE_SECOND_MS } from './market_operation_utils/constants'; import { ONE_SECOND_MS } from './market_operation_utils/constants';
import { RfqMakerBlacklist } from './rfq_maker_blacklist';
// tslint:disable-next-line: custom-no-magic-numbers // tslint:disable-next-line: custom-no-magic-numbers
const KEEP_ALIVE_TTL = 5 * 60 * ONE_SECOND_MS; const KEEP_ALIVE_TTL = 5 * 60 * ONE_SECOND_MS;
@@ -20,6 +21,10 @@ export const quoteRequestorHttpClient: AxiosInstance = Axios.create({
httpsAgent: new HttpsAgent({ keepAlive: true, timeout: KEEP_ALIVE_TTL }), httpsAgent: new HttpsAgent({ keepAlive: true, timeout: KEEP_ALIVE_TTL }),
}); });
const MAKER_TIMEOUT_STREAK_LENGTH = 10;
const MAKER_TIMEOUT_BLACKLIST_DURATION_MINUTES = 10;
const rfqMakerBlacklist = new RfqMakerBlacklist(MAKER_TIMEOUT_STREAK_LENGTH, MAKER_TIMEOUT_BLACKLIST_DURATION_MINUTES);
/** /**
* Request quotes from RFQ-T providers * Request quotes from RFQ-T providers
*/ */
@@ -334,7 +339,10 @@ export class QuoteRequestor {
const result: Array<{ response: ResponseT; makerUri: string }> = []; const result: Array<{ response: ResponseT; makerUri: string }> = [];
await Promise.all( await Promise.all(
Object.keys(this._rfqtAssetOfferings).map(async url => { Object.keys(this._rfqtAssetOfferings).map(async url => {
if (this._makerSupportsPair(url, makerAssetData, takerAssetData)) { if (
this._makerSupportsPair(url, makerAssetData, takerAssetData) &&
!rfqMakerBlacklist.isMakerBlacklisted(url)
) {
const requestParamsWithBigNumbers = { const requestParamsWithBigNumbers = {
takerAddress: options.takerAddress, takerAddress: options.takerAddress,
...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount), ...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount),
@@ -354,6 +362,10 @@ export class QuoteRequestor {
const partialLogEntry = { url, quoteType, requestParams }; const partialLogEntry = { url, quoteType, requestParams };
const timeBeforeAwait = Date.now(); const timeBeforeAwait = Date.now();
const maxResponseTimeMs =
options.makerEndpointMaxResponseTimeMs === undefined
? constants.DEFAULT_RFQT_REQUEST_OPTS.makerEndpointMaxResponseTimeMs!
: options.makerEndpointMaxResponseTimeMs;
try { try {
const quotePath = (() => { const quotePath = (() => {
switch (quoteType) { switch (quoteType) {
@@ -368,8 +380,9 @@ export class QuoteRequestor {
const response = await quoteRequestorHttpClient.get<ResponseT>(`${url}/${quotePath}`, { const response = await quoteRequestorHttpClient.get<ResponseT>(`${url}/${quotePath}`, {
headers: { '0x-api-key': options.apiKey }, headers: { '0x-api-key': options.apiKey },
params: requestParams, params: requestParams,
timeout: options.makerEndpointMaxResponseTimeMs, timeout: maxResponseTimeMs,
}); });
const latencyMs = Date.now() - timeBeforeAwait;
this._infoLogger({ this._infoLogger({
rfqtMakerInteraction: { rfqtMakerInteraction: {
...partialLogEntry, ...partialLogEntry,
@@ -378,12 +391,14 @@ export class QuoteRequestor {
apiKey: options.apiKey, apiKey: options.apiKey,
takerAddress: requestParams.takerAddress, takerAddress: requestParams.takerAddress,
statusCode: response.status, statusCode: response.status,
latencyMs: Date.now() - timeBeforeAwait, latencyMs,
}, },
}, },
}); });
rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs > maxResponseTimeMs);
result.push({ response: response.data, makerUri: url }); result.push({ response: response.data, makerUri: url });
} catch (err) { } catch (err) {
const latencyMs = Date.now() - timeBeforeAwait;
this._infoLogger({ this._infoLogger({
rfqtMakerInteraction: { rfqtMakerInteraction: {
...partialLogEntry, ...partialLogEntry,
@@ -392,10 +407,11 @@ export class QuoteRequestor {
apiKey: options.apiKey, apiKey: options.apiKey,
takerAddress: requestParams.takerAddress, takerAddress: requestParams.takerAddress,
statusCode: err.response ? err.response.status : undefined, statusCode: err.response ? err.response.status : undefined,
latencyMs: Date.now() - timeBeforeAwait, latencyMs,
}, },
}, },
}); });
rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs > maxResponseTimeMs);
this._warningLogger( this._warningLogger(
convertIfAxiosError(err), convertIfAxiosError(err),
`Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${url} for API key ${ `Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${url} for API key ${

View File

@@ -0,0 +1,38 @@
/**
* Tracks a maker's history of timely responses, and manages whether a given
* maker should be avoided for being too latent.
*/
export class RfqMakerBlacklist {
// tslint:disable-next-line:custom-no-magic-numbers
private static readonly _msPerMinute = 1000 * 60;
private readonly _makerTimeoutStreakLength: { [makerUrl: string]: number } = {};
private readonly _makerBlacklistedUntilDate: { [makerUrl: string]: number } = {};
constructor(private readonly _blacklistDurationMinutes: number, private readonly _timeoutStreakThreshold: number) {}
public logTimeoutOrLackThereof(makerUrl: string, didTimeout: boolean): void {
if (!this._makerTimeoutStreakLength.hasOwnProperty(makerUrl)) {
this._makerTimeoutStreakLength[makerUrl] = 0;
}
if (didTimeout) {
this._makerTimeoutStreakLength[makerUrl] += 1;
if (this._makerTimeoutStreakLength[makerUrl] > this._timeoutStreakThreshold) {
this._makerBlacklistedUntilDate[makerUrl] =
Date.now() + this._blacklistDurationMinutes * RfqMakerBlacklist._msPerMinute;
this._makerTimeoutStreakLength[makerUrl] = 0;
}
} else {
this._makerTimeoutStreakLength[makerUrl] = 0;
}
}
public isMakerBlacklisted(makerUrl: string): boolean {
if (this._makerBlacklistedUntilDate.hasOwnProperty(makerUrl)) {
if (this._makerBlacklistedUntilDate[makerUrl] > Date.now()) {
delete this._makerBlacklistedUntilDate[makerUrl];
return false;
} else {
return true;
}
} else {
return false;
}
}
}