fix(enrich): Discogs троттлит по IP — общий лимит ~54/мин (стоп 429-шторм)

Два токена с одного IP не помогают (Discogs лимитит по IP, не по токену) —
вызывало 100% 429. Вернул общий интервал 1100мс (~54/мин). 2-й токен полезен
только на втором IP (DE-воркер).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nk
2026-06-04 09:52:39 +03:00
parent 94e7f46b39
commit dfdfb7e4ab

View File

@@ -38,7 +38,11 @@ export class DiscogsService {
process.env.DISCOGS_TOKEN ?? '', process.env.DISCOGS_TOKEN ?? '',
process.env.DISCOGS_TOKEN2 ?? '', process.env.DISCOGS_TOKEN2 ?? '',
].filter((t) => t.length > 0); ].filter((t) => t.length > 0);
private readonly slots: number[] = this.tokens.map(() => 0); // ВАЖНО: Discogs троттлит по IP (не по токену), поэтому с одного сервера
// общий лимит ~54/мин независимо от числа токенов. Токены ротируем вхолостую
// (выгода от 2-го токена — только на ВТОРОМ IP, напр. DE-воркер).
private nextSlot = 0;
private rr = 0;
private readonly minIntervalMs = 1100; private readonly minIntervalMs = 1100;
// Без токена обогащение жанрами не работает (поиск требует авторизации) // Без токена обогащение жанрами не работает (поиск требует авторизации)
@@ -46,18 +50,15 @@ export class DiscogsService {
return this.tokens.length > 0; return this.tokens.length > 0;
} }
// Резервирует слот наименее загруженного токена, ждёт его и возвращает токен
private async pickToken(): Promise<string> { private async pickToken(): Promise<string> {
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 now = Date.now();
const start = Math.max(now, this.slots[idx]); const start = Math.max(now, this.nextSlot);
this.slots[idx] = start + this.minIntervalMs; this.nextSlot = start + this.minIntervalMs; // глобальный интервал (IP-лимит)
const wait = start - now; const wait = start - now;
if (wait > 0) await new Promise((r) => setTimeout(r, wait)); if (wait > 0) await new Promise((r) => setTimeout(r, wait));
return this.tokens[idx]; const token = this.tokens[this.rr % this.tokens.length];
this.rr++;
return token;
} }
async lookup(artist: string, song: string): Promise<DiscogsResult | null> { async lookup(artist: string, song: string): Promise<DiscogsResult | null> {