diff --git a/packages/monitor-v2/src/monitor-polymarket/common.ts b/packages/monitor-v2/src/monitor-polymarket/common.ts index e9685d5ede..0fdb6ff394 100644 --- a/packages/monitor-v2/src/monitor-polymarket/common.ts +++ b/packages/monitor-v2/src/monitor-polymarket/common.ts @@ -1,6 +1,6 @@ import { getRetryProvider, paginatedEventQuery as umaPaginatedEventQuery } from "@uma/common"; import { createHttpClient } from "@uma/toolkit"; -import { AxiosError, AxiosInstance } from "axios"; +import { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios"; export const paginatedEventQuery = umaPaginatedEventQuery; import type { Provider } from "@ethersproject/abstract-provider"; @@ -75,10 +75,12 @@ export interface MonitoringParams { fillEventsLookbackSeconds: number; fillEventsProposalGapSeconds: number; httpClient: ReturnType; + aiDeeplinkHttpClient: ReturnType; orderBookBatchSize: number; ooV2Addresses: string[]; ooV1Addresses: string[]; aiConfig?: AIConfig; + aiDeeplinkTimeout: number; } interface PolymarketMarketGraphql { question: string; @@ -712,16 +714,25 @@ export async function fetchLatestAIDeepLink( if (!params.aiConfig) { return { deeplink: undefined }; } + const startTime = Date.now(); try { const questionId = calculatePolymarketQuestionID(proposal.ancillaryData); - const response = await params.httpClient.get(params.aiConfig.apiUrl, { + const requestConfig: AxiosRequestConfig = { params: { limit: 50, search: proposal.proposalHash, last_page: false, project_id: params.aiConfig.projectId, }, - }); + }; + + requestConfig.timeout = params.aiDeeplinkTimeout; + + const response = await params.aiDeeplinkHttpClient.get( + params.aiConfig.apiUrl, + requestConfig + ); + const duration = Date.now() - startTime; const result = response.data?.elements?.find( (element) => element.data.input.timing?.expiration_timestamp === proposal.proposalExpirationTimestamp.toNumber() @@ -739,20 +750,52 @@ export async function fetchLatestAIDeepLink( status: response.status, statusText: response.statusText, }, + durationMs: duration, notificationPath: "otb-monitoring", }); return { deeplink: undefined }; } + logger.debug({ + at: "PolymarketMonitor", + message: "Successfully fetched AI deeplink", + proposalHash: proposal.proposalHash, + durationMs: duration, + }); + return { deeplink: `${params.aiConfig.resultsBaseUrl}/${result.id}`, }; } catch (error) { + const duration = Date.now() - startTime; + const axiosError = error as AxiosError; + logger.debug({ at: "PolymarketMonitor", message: "Failed to fetch AI deeplink", err: error instanceof Error ? error.message : String(error), proposalHash: proposal.proposalHash, + durationMs: duration, + errorDetails: { + code: axiosError?.code, + response: axiosError?.response + ? { + status: axiosError.response?.status, + statusText: axiosError.response?.statusText, + headers: axiosError.response?.headers, + } + : undefined, + request: axiosError?.config + ? { + url: axiosError.config?.url, + method: axiosError.config?.method, + timeout: axiosError.config?.timeout, + baseURL: axiosError.config?.baseURL, + } + : undefined, + isTimeout: + axiosError?.code === "ECONNABORTED" || (error instanceof Error && error.message?.includes("timeout")), + }, }); return { deeplink: undefined }; } @@ -902,6 +945,7 @@ export const initMonitoringParams = async ( const minTimeBetweenRequests = env.MIN_TIME_BETWEEN_REQUESTS ? Number(env.MIN_TIME_BETWEEN_REQUESTS) : 200; const httpTimeout = env.HTTP_TIMEOUT ? Number(env.HTTP_TIMEOUT) : 10_000; + const aiDeeplinkTimeout = env.AI_DEEPLINK_TIMEOUT ? Number(env.AI_DEEPLINK_TIMEOUT) : 10_000; const shouldResetTimeout = env.SHOULD_RESET_TIMEOUT !== "false"; @@ -924,6 +968,27 @@ export const initMonitoringParams = async ( }, }); + // Create a separate HTTP client for AI deeplink requests with unlimited concurrency + // This prevents AI deeplink requests from being queued behind other rate-limited requests + const aiDeeplinkHttpClient = createHttpClient({ + axios: { timeout: aiDeeplinkTimeout }, + rateLimit: { maxConcurrent: null, minTime: 0 }, // No rate limiting - unlimited concurrency + retry: { + retries: retryAttempts, + baseDelayMs: retryDelayMs, + shouldResetTimeout: false, // Don't reset timeout on retries - keep total time bounded by single timeout + retry delays + onRetry: (retryCount, err, config) => { + logger.debug({ + at: "PolymarketMonitor", + message: `ai-deeplink-retry attempt #${retryCount} for ${config?.url}`, + error: err.code || err.message, + retryCount, + timeout: config?.timeout, + }); + }, + }, + }); + const ooV2Addresses = parseEnvList(env, "OOV2_ADDRESSES", [await getAddress("OptimisticOracleV2", chainId)]); const ooV1Addresses = parseEnvList(env, "OOV1_ADDRESSES", [await getAddress("OptimisticOracle", chainId)]); @@ -947,10 +1012,12 @@ export const initMonitoringParams = async ( fillEventsLookbackSeconds, fillEventsProposalGapSeconds, httpClient, + aiDeeplinkHttpClient, orderBookBatchSize, ooV2Addresses, ooV1Addresses, aiConfig, + aiDeeplinkTimeout, }; }; diff --git a/packages/toolkit/src/http/httpClient.ts b/packages/toolkit/src/http/httpClient.ts index 0542c0f504..4f2d25e600 100644 --- a/packages/toolkit/src/http/httpClient.ts +++ b/packages/toolkit/src/http/httpClient.ts @@ -3,8 +3,8 @@ import Bottleneck from "bottleneck"; import axiosRetry, { IAxiosRetryConfig } from "axios-retry"; export interface RateLimitOptions { - /** Max requests running in parallel (default = 5) */ - maxConcurrent?: number; + /** Max requests running in parallel (default = 5). Set to null for unlimited concurrency. */ + maxConcurrent?: number | null; /** Minimum gap in ms between jobs (default = 200 → ≈5 req/s) */ minTime?: number; } @@ -47,15 +47,18 @@ export interface HttpClientOptions { * @returns An Axios instance */ export function createHttpClient(opts: HttpClientOptions = {}): AxiosInstance { - const { maxConcurrent = 5, minTime = 200 } = opts.rateLimit ?? {}; - const limiter = new Bottleneck({ maxConcurrent, minTime }); - const instance = axios.create({ timeout: 10_000, // default timeout of 10 seconds ...opts.axios, }); - instance.interceptors.request.use((cfg) => limiter.schedule(async () => cfg)); + // Only use Bottleneck if maxConcurrent is not null (null means unlimited, skip rate limiting entirely) + const maxConcurrent = opts.rateLimit?.maxConcurrent ?? 5; + if (maxConcurrent !== null) { + const minTime = opts.rateLimit?.minTime ?? 200; + const limiter = new Bottleneck({ maxConcurrent, minTime }); + instance.interceptors.request.use((cfg) => limiter.schedule(async () => cfg)); + } const { retries = 3, retryCondition, onRetry, baseDelayMs = 100, maxJitterMs = 1000, maxDelayMs = 10_000 } = opts.retry ?? {};