diff --git a/packages/asset-swapper/src/utils/quote_requestor.ts b/packages/asset-swapper/src/utils/quote_requestor.ts index d39e9efdcf..62cefcd702 100644 --- a/packages/asset-swapper/src/utils/quote_requestor.ts +++ b/packages/asset-swapper/src/utils/quote_requestor.ts @@ -11,6 +11,7 @@ import { constants } from '../constants'; import { MarketOperation, RfqtMakerAssetOfferings, RfqtRequestOpts } from '../types'; import { ONE_SECOND_MS } from './market_operation_utils/constants'; +import { RfqMakerBlacklist } from './rfq_maker_blacklist'; // tslint:disable-next-line: custom-no-magic-numbers 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 }), }); +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 */ @@ -334,7 +339,10 @@ export class QuoteRequestor { const result: Array<{ response: ResponseT; makerUri: string }> = []; await Promise.all( Object.keys(this._rfqtAssetOfferings).map(async url => { - if (this._makerSupportsPair(url, makerAssetData, takerAssetData)) { + if ( + this._makerSupportsPair(url, makerAssetData, takerAssetData) && + !rfqMakerBlacklist.isMakerBlacklisted(url) + ) { const requestParamsWithBigNumbers = { takerAddress: options.takerAddress, ...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount), @@ -354,6 +362,10 @@ export class QuoteRequestor { const partialLogEntry = { url, quoteType, requestParams }; const timeBeforeAwait = Date.now(); + const maxResponseTimeMs = + options.makerEndpointMaxResponseTimeMs === undefined + ? constants.DEFAULT_RFQT_REQUEST_OPTS.makerEndpointMaxResponseTimeMs! + : options.makerEndpointMaxResponseTimeMs; try { const quotePath = (() => { switch (quoteType) { @@ -368,8 +380,9 @@ export class QuoteRequestor { const response = await quoteRequestorHttpClient.get(`${url}/${quotePath}`, { headers: { '0x-api-key': options.apiKey }, params: requestParams, - timeout: options.makerEndpointMaxResponseTimeMs, + timeout: maxResponseTimeMs, }); + const latencyMs = Date.now() - timeBeforeAwait; this._infoLogger({ rfqtMakerInteraction: { ...partialLogEntry, @@ -378,12 +391,14 @@ export class QuoteRequestor { apiKey: options.apiKey, takerAddress: requestParams.takerAddress, statusCode: response.status, - latencyMs: Date.now() - timeBeforeAwait, + latencyMs, }, }, }); + rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs > maxResponseTimeMs); result.push({ response: response.data, makerUri: url }); } catch (err) { + const latencyMs = Date.now() - timeBeforeAwait; this._infoLogger({ rfqtMakerInteraction: { ...partialLogEntry, @@ -392,10 +407,11 @@ export class QuoteRequestor { apiKey: options.apiKey, takerAddress: requestParams.takerAddress, statusCode: err.response ? err.response.status : undefined, - latencyMs: Date.now() - timeBeforeAwait, + latencyMs, }, }, }); + rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs > maxResponseTimeMs); this._warningLogger( convertIfAxiosError(err), `Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${url} for API key ${ diff --git a/packages/asset-swapper/src/utils/rfq_maker_blacklist.ts b/packages/asset-swapper/src/utils/rfq_maker_blacklist.ts new file mode 100644 index 0000000000..a918050631 --- /dev/null +++ b/packages/asset-swapper/src/utils/rfq_maker_blacklist.ts @@ -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; + } + } +}