From d6b8be124e5ad02718f4e8a619e734994db79abb Mon Sep 17 00:00:00 2001 From: nk Date: Thu, 4 Jun 2026 10:21:37 +0300 Subject: [PATCH] =?UTF-8?q?perf(enrich):=203-=D0=B9=20IP=20Discogs=20(toke?= =?UTF-8?q?n3=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20=D1=84=D0=BE=D1=80=D1=81-?= =?UTF-8?q?IPv4=20RU)=20+=20concurrency=2012?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit token3 ходит напрямую с RU, но форсирован IPv4 (121.127.37.212) — для Discogs это 3-й IP (token1=RU-v6, token2=DE-v4, token3=RU-v4). ~162/мин потолок. Без доп. инфры. concurrency 12 чтобы задействовать 3 IP. Co-Authored-By: Claude Opus 4.8 --- docker-compose.yml | 1 + src/enrich/discogs.service.ts | 55 ++++++++++++++++---------------- src/enrich/enrichment.service.ts | 4 +-- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index c801c48..001e75f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,6 +23,7 @@ services: # Обогащение треков - DISCOGS_TOKEN=${DISCOGS_TOKEN} - DISCOGS_TOKEN2=${DISCOGS_TOKEN2} + - DISCOGS_TOKEN3=${DISCOGS_TOKEN3} - DISCOGS_PROXY=${DISCOGS_PROXY} - COVERS_DIR=/data/covers - PUBLIC_BASE_URL=${PUBLIC_BASE_URL:-http://121.127.37.212:3000} diff --git a/src/enrich/discogs.service.ts b/src/enrich/discogs.service.ts index a6d255b..714baa7 100644 --- a/src/enrich/discogs.service.ts +++ b/src/enrich/discogs.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; -import { ProxyAgent } from 'undici'; +import { ProxyAgent, Agent } from 'undici'; // Результат обогащения из Discogs export interface DiscogsResult { @@ -33,44 +33,43 @@ export class DiscogsService { private readonly logger = new Logger(DiscogsService.name); private readonly userAgent = 'radiOLA/1.0 +https://radiola.app'; - // Несколько токенов Discogs — лимит ~60/мин НА ТОКЕН. У каждого свой слот, - // выбираем наименее загруженный → суммарно N×~54/мин без 429. - private readonly tokens = [ - process.env.DISCOGS_TOKEN ?? '', - process.env.DISCOGS_TOKEN2 ?? '', - ].filter((t) => t.length > 0); - // Discogs троттлит по IP. Маршруты: [0] token1 напрямую (IP RU), - // [1] token2 через DE-прокси (IP DE) — два разных IP, у каждого свой слот - // ~54/мин → суммарно ~108/мин. Без прокси используем только маршрут 0. - private readonly proxyAgent = process.env.DISCOGS_PROXY - ? new ProxyAgent(process.env.DISCOGS_PROXY) - : null; - private readonly slots: number[] = this.tokens.map(() => 0); + // Discogs троттлит ПО IP. Делаем несколько маршрутов с РАЗНЫМИ IP, каждый со + // своим токеном и слотом ~54/мин → суммарно N×54/мин без 429: + // • token1 — напрямую (RU, IPv6 по умолчанию) + // • token2 — через DE-прокси (выход с IP DE) + // • token3 — напрямую, но форсируем IPv4 (RU, другой IP, чем IPv6) private readonly minIntervalMs = 1100; + private readonly routeList = this.buildRoutes(); + private readonly slots: number[] = this.routeList.map(() => 0); + + private buildRoutes(): { token: string; dispatcher: unknown }[] { + const routes: { token: string; dispatcher: unknown }[] = []; + const t1 = process.env.DISCOGS_TOKEN ?? ''; + const t2 = process.env.DISCOGS_TOKEN2 ?? ''; + const t3 = process.env.DISCOGS_TOKEN3 ?? ''; + const proxy = process.env.DISCOGS_PROXY ?? ''; + if (t1) routes.push({ token: t1, dispatcher: undefined }); + if (t2 && proxy) routes.push({ token: t2, dispatcher: new ProxyAgent(proxy) }); + if (t3) routes.push({ token: t3, dispatcher: new Agent({ connect: { family: 4 } }) }); + return routes; + } // Без токена обогащение жанрами не работает (поиск требует авторизации) get enabled(): boolean { - return this.tokens.length > 0; - } - - // Индексы доступных маршрутов (token2 — только если есть прокси/2-й IP) - private routes(): number[] { - return this.tokens.length >= 2 && this.proxyAgent ? [0, 1] : [0]; + return this.routeList.length > 0; } // Резервирует слот наименее загруженного маршрута, возвращает токен + dispatcher - private async pickRoute(): Promise<{ token: string; dispatcher: ProxyAgent | undefined }> { - const routes = this.routes(); - let idx = routes[0]; - for (const r of routes) if (this.slots[r] < this.slots[idx]) idx = r; + private async pickRoute(): Promise<{ token: string; dispatcher: unknown }> { + let idx = 0; + for (let i = 1; i < this.slots.length; i++) { + if (this.slots[i] < this.slots[idx]) idx = i; + } const now = Date.now(); const start = Math.max(now, this.slots[idx]); this.slots[idx] = start + this.minIntervalMs; if (start > now) await new Promise((res) => setTimeout(res, start - now)); - return { - token: this.tokens[idx], - dispatcher: idx === 1 ? (this.proxyAgent ?? undefined) : undefined, - }; + return this.routeList[idx]; } async lookup(artist: string, song: string): Promise { diff --git a/src/enrich/enrichment.service.ts b/src/enrich/enrichment.service.ts index e0dd3e6..f6a7045 100644 --- a/src/enrich/enrichment.service.ts +++ b/src/enrich/enrichment.service.ts @@ -19,8 +19,8 @@ export class EnrichmentService { private running = false; // Discogs сам себя лимитирует (rate-limiter в DiscogsService), поэтому можно // выше параллельность: обложки (iTunes, без лимита) льются быстрее. - private readonly throttleMs = 200; - private readonly concurrency = 8; + private readonly throttleMs = 150; + private readonly concurrency = 12; constructor( private readonly prisma: PrismaService,