From 4750aca415e8e94b786daf9fa405c195cf27fabe Mon Sep 17 00:00:00 2001 From: Zain Date: Wed, 18 Feb 2026 22:39:23 +0800 Subject: [PATCH] feat: add adaptive request interval --- src/core/executor.ts | 83 ++++++++++++++++++++++++++++++++++++- tests/unit/executor.test.ts | 20 +++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/src/core/executor.ts b/src/core/executor.ts index 5497948..ed885a1 100644 --- a/src/core/executor.ts +++ b/src/core/executor.ts @@ -73,6 +73,12 @@ export class BatchExecutor { private rateLimitChain: Promise = Promise.resolve(); private lastRequestStartAt = 0; + private adaptiveIntervalFactor = 1; + private adaptiveSuccessStreak = 0; + private readonly adaptiveMaxFactor = 5; + private readonly adaptiveRecoverThreshold = 5; + private readonly adaptiveStep = 0.5; + private isCancelled(): boolean { return this.state === ExecutorState.CANCELLED; } @@ -110,6 +116,8 @@ export class BatchExecutor { this.tasks = []; this.rateLimitChain = Promise.resolve(); this.lastRequestStartAt = 0; + this.adaptiveIntervalFactor = 1; + this.adaptiveSuccessStreak = 0; try { // 准备所有重命名任务 @@ -262,6 +270,7 @@ export class BatchExecutor { const result = await this.adapter.renameFile(task.file.id, task.newName); if (result.success) { + this.noteSuccess(); this.results.success.push({ fileId: task.file.id, original: task.file.name, @@ -270,6 +279,7 @@ export class BatchExecutor { }); this.emitProgress(task, 'success'); } else { + this.noteBackoff(result.error); const errorMessage = result.error?.message || 'Unknown error'; this.results.failed.push({ fileId: task.file.id, @@ -280,6 +290,7 @@ export class BatchExecutor { this.emitProgress(task, 'failed', errorMessage); } } catch (error) { + this.noteBackoff(error as Error); const errorMessage = (error as Error).message; this.results.failed.push({ fileId: task.file.id, @@ -304,12 +315,14 @@ export class BatchExecutor { return; } + const effectiveInterval = interval * Math.max(1, this.adaptiveIntervalFactor); + const doWait = async () => { const now = Date.now(); if (this.lastRequestStartAt > 0) { const delta = now - this.lastRequestStartAt; - const delay = interval - delta; + const delay = effectiveInterval - delta; if (delay > 0) { await sleep(delay); } @@ -323,6 +336,74 @@ export class BatchExecutor { await chained; } + private noteBackoff(error?: Error): void { + if (!this.isBackoffError(error)) { + return; + } + + this.adaptiveIntervalFactor = Math.min( + this.adaptiveMaxFactor, + this.adaptiveIntervalFactor + this.adaptiveStep + ); + this.adaptiveSuccessStreak = 0; + } + + private noteSuccess(): void { + if (this.adaptiveIntervalFactor <= 1) { + return; + } + + this.adaptiveSuccessStreak += 1; + + if (this.adaptiveSuccessStreak >= this.adaptiveRecoverThreshold) { + this.adaptiveIntervalFactor = Math.max( + 1, + this.adaptiveIntervalFactor - this.adaptiveStep + ); + this.adaptiveSuccessStreak = 0; + } + } + + private isBackoffError(error?: Error): boolean { + if (!error) return false; + + const anyError = error as any; + + const numericCode = typeof anyError.code === 'number' ? anyError.code : null; + if (numericCode === 429 || numericCode === 500) { + return true; + } + + const errno = typeof anyError.errno === 'number' ? anyError.errno : null; + if (errno !== null && [4, 110, 111, 112].includes(errno)) { + return true; + } + + const stringCode = typeof anyError.code === 'string' ? anyError.code : ''; + if (['TooManyRequests', 'InternalError.Timeout', 'ServiceUnavailable'].includes(stringCode)) { + return true; + } + + const message = String(anyError.message || '').toLowerCase(); + if ( + message.includes('too many') || + message.includes('toomanyrequests') || + message.includes('rate limit') || + message.includes('rate') || + message.includes('429') || + message.includes('请求过于频繁') || + message.includes('过于频繁') + ) { + return true; + } + + return false; + } + + getAdaptiveIntervalFactor(): number { + return this.adaptiveIntervalFactor; + } + /** * 发射进度事件 * @private diff --git a/tests/unit/executor.test.ts b/tests/unit/executor.test.ts index 48d8968..beb15d2 100644 --- a/tests/unit/executor.test.ts +++ b/tests/unit/executor.test.ts @@ -207,6 +207,26 @@ describe('BatchExecutor', () => { expect(results.success.length + results.failed.length).toBe(files.length); }); + it('should increase adaptive interval factor when backoff error occurs', async () => { + const backoffAdapter = new MockAdapter(); + let called = 0; + backoffAdapter.renameFile = async () => { + called++; + if (called === 1) { + return { success: false, error: new Error('TooManyRequests') } as any; + } + return { success: true, newName: 'ok' } as any; + }; + + const executor = new BatchExecutor(files, mockRule, backoffAdapter, { + requestInterval: 50, + }); + + await executor.execute(); + + expect(executor.getAdaptiveIntervalFactor()).toBeGreaterThan(1); + }); + it('应该限制最大并发数', async () => { // 让单次请求更慢,便于观测并发峰值 adapter.renameDelay = 200;