Skip to content

Commit 6cfd31b

Browse files
committed
收紧 LimiterFileSystem 的 429 自动重试范围,避免写操作被盲目重放
1 parent 144dc25 commit 6cfd31b

2 files changed

Lines changed: 127 additions & 14 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { afterEach, describe, expect, it, vi } from "vitest";
2+
import type FileSystem from "./filesystem";
3+
import type { FileInfo, FileReader, FileWriter } from "./filesystem";
4+
import LimiterFileSystem from "./limiter";
5+
6+
function createFs(): FileSystem {
7+
return {
8+
verify: vi.fn(async () => {}),
9+
open: vi.fn(async () => {
10+
const reader: FileReader = {
11+
read: vi.fn(async () => "content"),
12+
};
13+
return reader;
14+
}),
15+
openDir: vi.fn(async () => createFs()),
16+
create: vi.fn(async () => {
17+
const writer: FileWriter = {
18+
write: vi.fn(async () => {}),
19+
};
20+
return writer;
21+
}),
22+
createDir: vi.fn(async () => {}),
23+
delete: vi.fn(async () => {}),
24+
list: vi.fn(async () => []),
25+
getDirUrl: vi.fn(async () => "url"),
26+
};
27+
}
28+
29+
const file: FileInfo = {
30+
name: "test.user.js",
31+
path: "/test.user.js",
32+
size: 1,
33+
digest: "digest",
34+
createtime: 1,
35+
updatetime: 1,
36+
};
37+
38+
describe("LimiterFileSystem", () => {
39+
afterEach(() => {
40+
vi.useRealTimers();
41+
vi.restoreAllMocks();
42+
});
43+
44+
it("should retry list on 429", async () => {
45+
vi.useFakeTimers();
46+
const fs = createFs();
47+
vi.mocked(fs.list).mockRejectedValueOnce(new Error("429 Too Many Requests")).mockResolvedValueOnce([]);
48+
const limiter = new LimiterFileSystem(fs);
49+
50+
const promise = limiter.list();
51+
await vi.runOnlyPendingTimersAsync();
52+
53+
await expect(promise).resolves.toEqual([]);
54+
expect(fs.list).toHaveBeenCalledTimes(2);
55+
});
56+
57+
it("should not retry delete on 429", async () => {
58+
const fs = createFs();
59+
vi.mocked(fs.delete).mockRejectedValueOnce(new Error("429 Too Many Requests"));
60+
const limiter = new LimiterFileSystem(fs);
61+
62+
await expect(limiter.delete("/test.user.js")).rejects.toThrow("429 Too Many Requests");
63+
expect(fs.delete).toHaveBeenCalledTimes(1);
64+
});
65+
66+
it("should not retry createDir on 429", async () => {
67+
const fs = createFs();
68+
vi.mocked(fs.createDir).mockRejectedValueOnce(new Error("429 Too Many Requests"));
69+
const limiter = new LimiterFileSystem(fs);
70+
71+
await expect(limiter.createDir("/dir")).rejects.toThrow("429 Too Many Requests");
72+
expect(fs.createDir).toHaveBeenCalledTimes(1);
73+
});
74+
75+
it("should not retry writer.write on 429", async () => {
76+
const fs = createFs();
77+
const write = vi.fn(async () => {});
78+
vi.mocked(fs.create).mockResolvedValueOnce({
79+
write,
80+
});
81+
write.mockRejectedValueOnce(new Error("429 Too Many Requests"));
82+
const limiter = new LimiterFileSystem(fs);
83+
const writer = await limiter.create(file.path);
84+
85+
await expect(writer.write("content")).rejects.toThrow("429 Too Many Requests");
86+
expect(write).toHaveBeenCalledTimes(1);
87+
});
88+
89+
it("should retry reader.read on 429", async () => {
90+
vi.useFakeTimers();
91+
const fs = createFs();
92+
const read = vi.fn(async () => "content");
93+
vi.mocked(fs.open).mockResolvedValueOnce({
94+
read,
95+
});
96+
read.mockRejectedValueOnce(new Error("429 Too Many Requests"));
97+
read.mockResolvedValueOnce("content");
98+
const limiter = new LimiterFileSystem(fs);
99+
const reader = await limiter.open(file);
100+
101+
const promise = reader.read("string");
102+
await vi.runOnlyPendingTimersAsync();
103+
104+
await expect(promise).resolves.toBe("content");
105+
expect(read).toHaveBeenCalledTimes(2);
106+
});
107+
});

packages/filesystem/limiter.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type FileSystem from "./filesystem";
22
import type { FileInfo, FileReader, FileWriter } from "./filesystem";
33

4+
const RETRYABLE_429_OPS = new Set(["verify", "open", "read", "openDir", "list", "getDirUrl"]);
5+
46
/**
57
* 速率限制器
68
* 控制并发操作数量,防止过多并发请求
@@ -21,7 +23,7 @@ export class RateLimiter {
2123
* @param fn 要执行的操作函数
2224
* @returns 操作结果
2325
*/
24-
async execute<T>(fn: () => Promise<T>): Promise<T> {
26+
async execute<T>(fn: () => Promise<T>, op = "unknown"): Promise<T> {
2527
// 如果当前运行的操作数已达到上限,则等待
2628
while (this.running >= this.maxConcurrent) {
2729
await new Promise<void>((resolve) => {
@@ -31,7 +33,7 @@ export class RateLimiter {
3133

3234
this.running++;
3335
try {
34-
return await this.executeWithRetry(fn);
36+
return await this.executeWithRetry(fn, op);
3537
} finally {
3638
this.running--;
3739
// 执行完成后,从队列中取出下一个等待的操作
@@ -47,15 +49,15 @@ export class RateLimiter {
4749
* @param fn 要执行的操作函数
4850
* @returns 操作结果
4951
*/
50-
private async executeWithRetry<T>(fn: () => Promise<T>): Promise<T> {
52+
private async executeWithRetry<T>(fn: () => Promise<T>, op: string): Promise<T> {
5153
// 最多重试 10 次
5254
for (let i = 0; i <= 10; i++) {
5355
try {
5456
return await fn();
5557
} catch (error) {
5658
// 检查错误字符串中是否包含 429
5759
const errorStr = String(error);
58-
if (errorStr.includes("429") && i < 10) {
60+
if (this.shouldRetry429(op, errorStr) && i < 10) {
5961
// 遇到 429 错误且未达到重试上限,采用指数退避策略延迟后继续重试
6062
const delay = Math.min(2000 * Math.pow(2, i), 60000);
6163
await new Promise((resolve) => setTimeout(resolve, delay));
@@ -68,6 +70,10 @@ export class RateLimiter {
6870
}
6971
throw new Error("Max retries exceeded");
7072
}
73+
74+
private shouldRetry429(op: string, errorStr: string): boolean {
75+
return errorStr.includes("429") && RETRYABLE_429_OPS.has(op);
76+
}
7177
}
7278

7379
// 文件系统限速器,防止并发请求过多达到服务器限制
@@ -83,47 +89,47 @@ export default class LimiterFileSystem implements FileSystem {
8389
}
8490

8591
verify(): Promise<void> {
86-
return this.limiter.execute(() => this.fs.verify());
92+
return this.limiter.execute(() => this.fs.verify(), "verify");
8793
}
8894

8995
async open(file: FileInfo): Promise<FileReader> {
9096
return this.limiter.execute(async () => {
9197
const reader = await this.fs.open(file);
9298
return {
93-
read: (type) => this.limiter.execute(() => reader.read(type)),
99+
read: (type) => this.limiter.execute(() => reader.read(type), "read"),
94100
};
95-
});
101+
}, "open");
96102
}
97103

98104
async openDir(path: string): Promise<FileSystem> {
99105
return this.limiter.execute(async () => {
100106
const fs = await this.fs.openDir(path);
101107
return new LimiterFileSystem(fs, this.limiter);
102-
});
108+
}, "openDir");
103109
}
104110

105111
async create(path: string): Promise<FileWriter> {
106112
return this.limiter.execute(async () => {
107113
const writer = await this.fs.create(path);
108114
return {
109-
write: (content) => this.limiter.execute(() => writer.write(content)),
115+
write: (content) => this.limiter.execute(() => writer.write(content), "write"),
110116
};
111-
});
117+
}, "create");
112118
}
113119

114120
createDir(dir: string): Promise<void> {
115-
return this.limiter.execute(() => this.fs.createDir(dir));
121+
return this.limiter.execute(() => this.fs.createDir(dir), "createDir");
116122
}
117123

118124
delete(path: string): Promise<void> {
119-
return this.limiter.execute(() => this.fs.delete(path));
125+
return this.limiter.execute(() => this.fs.delete(path), "delete");
120126
}
121127

122128
list(): Promise<FileInfo[]> {
123-
return this.limiter.execute(() => this.fs.list());
129+
return this.limiter.execute(() => this.fs.list(), "list");
124130
}
125131

126132
getDirUrl(): Promise<string> {
127-
return this.limiter.execute(() => this.fs.getDirUrl());
133+
return this.limiter.execute(() => this.fs.getDirUrl(), "getDirUrl");
128134
}
129135
}

0 commit comments

Comments
 (0)