镜像自地址
https://github.com/lhly/cloud-drive-renamer.git
已同步 2026-10-20 21:57:47 +08:00
feat: add adaptive request interval
这个提交包含在:
父节点
f3a89f28e6
当前提交
4750aca415
@ -73,6 +73,12 @@ export class BatchExecutor {
|
||||
private rateLimitChain: Promise<void> = 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
|
||||
|
||||
@ -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;
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户