镜像自地址
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 rateLimitChain: Promise<void> = Promise.resolve();
|
||||||
private lastRequestStartAt = 0;
|
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 {
|
private isCancelled(): boolean {
|
||||||
return this.state === ExecutorState.CANCELLED;
|
return this.state === ExecutorState.CANCELLED;
|
||||||
}
|
}
|
||||||
@ -110,6 +116,8 @@ export class BatchExecutor {
|
|||||||
this.tasks = [];
|
this.tasks = [];
|
||||||
this.rateLimitChain = Promise.resolve();
|
this.rateLimitChain = Promise.resolve();
|
||||||
this.lastRequestStartAt = 0;
|
this.lastRequestStartAt = 0;
|
||||||
|
this.adaptiveIntervalFactor = 1;
|
||||||
|
this.adaptiveSuccessStreak = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 准备所有重命名任务
|
// 准备所有重命名任务
|
||||||
@ -262,6 +270,7 @@ export class BatchExecutor {
|
|||||||
const result = await this.adapter.renameFile(task.file.id, task.newName);
|
const result = await this.adapter.renameFile(task.file.id, task.newName);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
this.noteSuccess();
|
||||||
this.results.success.push({
|
this.results.success.push({
|
||||||
fileId: task.file.id,
|
fileId: task.file.id,
|
||||||
original: task.file.name,
|
original: task.file.name,
|
||||||
@ -270,6 +279,7 @@ export class BatchExecutor {
|
|||||||
});
|
});
|
||||||
this.emitProgress(task, 'success');
|
this.emitProgress(task, 'success');
|
||||||
} else {
|
} else {
|
||||||
|
this.noteBackoff(result.error);
|
||||||
const errorMessage = result.error?.message || 'Unknown error';
|
const errorMessage = result.error?.message || 'Unknown error';
|
||||||
this.results.failed.push({
|
this.results.failed.push({
|
||||||
fileId: task.file.id,
|
fileId: task.file.id,
|
||||||
@ -280,6 +290,7 @@ export class BatchExecutor {
|
|||||||
this.emitProgress(task, 'failed', errorMessage);
|
this.emitProgress(task, 'failed', errorMessage);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.noteBackoff(error as Error);
|
||||||
const errorMessage = (error as Error).message;
|
const errorMessage = (error as Error).message;
|
||||||
this.results.failed.push({
|
this.results.failed.push({
|
||||||
fileId: task.file.id,
|
fileId: task.file.id,
|
||||||
@ -304,12 +315,14 @@ export class BatchExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const effectiveInterval = interval * Math.max(1, this.adaptiveIntervalFactor);
|
||||||
|
|
||||||
const doWait = async () => {
|
const doWait = async () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
if (this.lastRequestStartAt > 0) {
|
if (this.lastRequestStartAt > 0) {
|
||||||
const delta = now - this.lastRequestStartAt;
|
const delta = now - this.lastRequestStartAt;
|
||||||
const delay = interval - delta;
|
const delay = effectiveInterval - delta;
|
||||||
if (delay > 0) {
|
if (delay > 0) {
|
||||||
await sleep(delay);
|
await sleep(delay);
|
||||||
}
|
}
|
||||||
@ -323,6 +336,74 @@ export class BatchExecutor {
|
|||||||
await chained;
|
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
|
* @private
|
||||||
|
|||||||
@ -207,6 +207,26 @@ describe('BatchExecutor', () => {
|
|||||||
expect(results.success.length + results.failed.length).toBe(files.length);
|
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 () => {
|
it('应该限制最大并发数', async () => {
|
||||||
// 让单次请求更慢,便于观测并发峰值
|
// 让单次请求更慢,便于观测并发峰值
|
||||||
adapter.renameDelay = 200;
|
adapter.renameDelay = 200;
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户