Skip to content

Commit f8e76e6

Browse files
cyfung1031CodFrm
andauthored
🐛 解决并发 xhr 的 session rule 数量问题 (#1353)
* 解决并发 xhr 的 session rule 数量问题 * typo * code fix * fix mock * update unit test * 改善 session rule 移除处理 及 scXhrRequests 处理 * fix mock * 处理 code review 反馈: 修复并发锁/错误回滚并改善测试 - dnr_id_controller: nextSessionRuleId 改为 while 循环反复判定上限, removeSessionRuleIdEntry 释放一次只放行一个 waiter,避免撑爆 LIMIT - dnr_id_controller: 初始化时用历史最大 rule id 更新 SESSION_RULE_ID_BEGIN - gm_api: headersSettled 在 lastError 时不释放 ruleID 避免本地/浏览器状态不一致 - gm_api: updateSessionRules addRules 失败时回滚 headerModifierMap 与 ruleId - gm_api: 修正注释 headerModifer -> headerModifier - chrome-extension-mock: onResponseStarted.addListener 暴露 callback 供测试触发 - dnr_id_controller.test: describe/it 标题改为中文,去除 Math.random - dnr_id_controller.test: 并发用例改为 "1 次释放 = 1 个 waiter 放行" 语义 - vitest.setup: 移除注释掉的 spyOn 调试代码 --------- Co-authored-by: 王一之 <yz@ggnb.top>
1 parent a62e876 commit f8e76e6

5 files changed

Lines changed: 431 additions & 29 deletions

File tree

packages/chrome-extension-mock/declarativ_net_request.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
export default class DeclarativeNetRequest {
2+
MAX_NUMBER_OF_SESSION_RULES = 5000;
3+
4+
private _sessionRules: chrome.declarativeNetRequest.Rule[] = [];
5+
26
HeaderOperation = {
37
APPEND: "append",
48
SET: "set",
@@ -30,9 +34,44 @@ export default class DeclarativeNetRequest {
3034
OTHER: "other",
3135
};
3236

33-
updateSessionRules() {
37+
updateSessionRules(arg1: any, arg2: any): Promise<void> {
38+
let options: {
39+
addRules?: chrome.declarativeNetRequest.Rule[];
40+
removeRuleIds?: number[];
41+
} = {};
42+
let callback: undefined | ((...args: any) => any) = undefined;
43+
44+
if (typeof arg1 === "function") {
45+
callback = arg1;
46+
} else if (typeof arg2 === "function") {
47+
callback = arg2;
48+
}
49+
if (typeof arg1 === "object" && arg1) options = arg1;
50+
3451
return new Promise<void>((resolve) => {
52+
const { addRules = [], removeRuleIds = [] } = options;
53+
54+
// Remove rules by ID
55+
if (removeRuleIds.length > 0) {
56+
this._sessionRules = this._sessionRules.filter((rule) => !removeRuleIds.includes(rule.id));
57+
}
58+
59+
// Add or update rules (upsert by ID)
60+
for (const newRule of addRules) {
61+
const existingIndex = this._sessionRules.findIndex((rule) => rule.id === newRule.id);
62+
if (existingIndex !== -1) {
63+
this._sessionRules[existingIndex] = newRule; // update
64+
} else {
65+
this._sessionRules.push(newRule); // add
66+
}
67+
}
68+
3569
resolve();
70+
callback?.();
3671
});
3772
}
73+
74+
getSessionRules(): Promise<chrome.declarativeNetRequest.Rule[]> {
75+
return Promise.resolve([...this._sessionRules]);
76+
}
3877
}

packages/chrome-extension-mock/web_reqeuest.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import EventEmitter from "eventemitter3";
22

33
export default class WebRequest {
44
sendHeader?: (details: chrome.webRequest.OnSendHeadersDetails) => chrome.webRequest.BlockingResponse | void;
5+
responseStarted?: (details: chrome.webRequest.OnResponseStartedDetails) => void;
56

67
onBeforeSendHeaders = {
78
addListener: (callback: any) => {
@@ -15,6 +16,12 @@ export default class WebRequest {
1516
},
1617
};
1718

19+
onResponseStarted = {
20+
addListener: (callback: any) => {
21+
this.responseStarted = callback;
22+
},
23+
};
24+
1825
onCompleted = {
1926
addListener: () => {
2027
// TODO
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import { sleep } from "@App/pkg/utils/utils";
2+
import { describe, expect, it } from "vitest";
3+
import {
4+
getSessionRuleIds,
5+
LIMIT_SESSION_RULES,
6+
nextSessionRuleId,
7+
removeSessionRuleIdEntry,
8+
} from "./dnr_id_controller";
9+
10+
describe("getSessionRuleIds", () => {
11+
it("从现有 chrome session rules 初始化", async () => {
12+
const ids = await getSessionRuleIds();
13+
expect(ids.size).lessThan(100);
14+
await nextSessionRuleId();
15+
expect(ids.size).greaterThanOrEqual(1);
16+
await nextSessionRuleId();
17+
expect(ids.size).greaterThanOrEqual(2);
18+
});
19+
});
20+
21+
describe("nextSessionRuleId", () => {
22+
it("每次调用返回唯一递增的 id", async () => {
23+
const id1 = await nextSessionRuleId();
24+
const id2 = await nextSessionRuleId();
25+
const id3 = await nextSessionRuleId();
26+
27+
expect(id2).toBeGreaterThan(id1);
28+
expect(id3).toBeGreaterThan(id2);
29+
});
30+
31+
it("跳过已存在于 session rules 中的 id", async () => {
32+
const ids = await getSessionRuleIds();
33+
const next = await nextSessionRuleId();
34+
35+
ids.add(next + 1);
36+
const skipped = await nextSessionRuleId();
37+
38+
expect(skipped).toBeGreaterThan(next + 1);
39+
});
40+
});
41+
42+
describe("removeSessionRuleIdEntry", () => {
43+
it("从追踪集合中移除指定 id", async () => {
44+
const ids = await getSessionRuleIds();
45+
const id = await nextSessionRuleId();
46+
47+
ids.add(id);
48+
removeSessionRuleIdEntry(id);
49+
50+
expect(ids.has(id)).toBe(false);
51+
});
52+
53+
it("sessionRuleIds 未初始化时为 no-op", () => {
54+
expect(() => removeSessionRuleIdEntry(10404)).not.toThrow();
55+
});
56+
57+
it("ruleId <= 10000 时抛错", () => {
58+
expect(() => removeSessionRuleIdEntry(10000)).toThrow();
59+
expect(() => removeSessionRuleIdEntry(1)).toThrow();
60+
});
61+
62+
it("回退 SESSION_RULE_ID_BEGIN 使被移除的 id 下次被复用", async () => {
63+
const ids = await getSessionRuleIds();
64+
65+
const id1 = await nextSessionRuleId();
66+
const id2 = await nextSessionRuleId();
67+
const id3 = await nextSessionRuleId();
68+
69+
ids.add(id1);
70+
ids.add(id2);
71+
ids.add(id3);
72+
73+
// 移除最新的 id,counter 回退并复用该 id
74+
removeSessionRuleIdEntry(id3);
75+
const reused = await nextSessionRuleId();
76+
expect(reused).toBe(id3);
77+
});
78+
79+
it("移除的 id 在 counter 之前时仍会回退", async () => {
80+
const ids = await getSessionRuleIds();
81+
82+
const id1 = await nextSessionRuleId();
83+
const id2 = await nextSessionRuleId();
84+
const id3 = await nextSessionRuleId();
85+
86+
ids.add(id1);
87+
ids.add(id2);
88+
ids.add(id3);
89+
90+
// 移除较早的 id,counter 回退到 id1 - 1,下次分配会得到 id1
91+
removeSessionRuleIdEntry(id1);
92+
const reused = await nextSessionRuleId();
93+
expect(reused).toBe(id1);
94+
});
95+
96+
it("移除 counter 之后的 id 不回退 counter", async () => {
97+
const id1 = await nextSessionRuleId();
98+
const id2 = await nextSessionRuleId();
99+
const id3 = await nextSessionRuleId();
100+
const _id4 = await nextSessionRuleId();
101+
const id5 = await nextSessionRuleId();
102+
const id6 = await nextSessionRuleId();
103+
104+
removeSessionRuleIdEntry(id1);
105+
removeSessionRuleIdEntry(id5);
106+
removeSessionRuleIdEntry(id3);
107+
// removeSessionRuleIdEntry(id4);
108+
removeSessionRuleIdEntry(id2);
109+
const nextBefore = await nextSessionRuleId(); // e.g. 10001
110+
removeSessionRuleIdEntry(id6);
111+
112+
expect(await nextSessionRuleId()).toBe(nextBefore + 1); // counter 未变,正常递增
113+
expect(await nextSessionRuleId()).toBe(nextBefore + 2);
114+
// expect(await nextSessionRuleId()).toBe(nextBefore + 3);
115+
expect(await nextSessionRuleId()).toBe(nextBefore + 4);
116+
expect(await nextSessionRuleId()).toBe(nextBefore + 5);
117+
});
118+
});
119+
120+
describe("nextSessionRuleId limit control", () => {
121+
it("达到上限时锁定,移除条目后解锁", async () => {
122+
const ids = await getSessionRuleIds();
123+
expect(ids.size).toBeLessThan(100);
124+
125+
const added = [];
126+
for (let w = ids.size; w < LIMIT_SESSION_RULES; w++) {
127+
const j = await nextSessionRuleId();
128+
added.push(j);
129+
ids.add(j);
130+
}
131+
expect(ids.size).toBeGreaterThan(1000);
132+
133+
const lockedPromise = nextSessionRuleId();
134+
135+
const raceResult1 = await Promise.race([lockedPromise.then(() => "resolved"), sleep(5).then(() => "pending")]);
136+
expect(raceResult1).toBe("pending");
137+
138+
// 使用固定索引而非随机,保证测试可重复
139+
const p1 = added[0];
140+
const p2 = added[6];
141+
removeSessionRuleIdEntry(p1);
142+
removeSessionRuleIdEntry(p2);
143+
144+
const raceResult2 = await Promise.race([lockedPromise.then(() => "resolved"), sleep(5).then(() => "pending")]);
145+
expect(raceResult2).toBe("resolved");
146+
147+
const id1 = await lockedPromise;
148+
const id2 = await nextSessionRuleId();
149+
expect(id1).toBe(p1);
150+
expect(id2).toBe(p2);
151+
152+
for (const k of added) {
153+
removeSessionRuleIdEntry(k);
154+
}
155+
156+
const res = await getSessionRuleIds();
157+
expect(res).toBe(ids);
158+
expect(res.size).toBeLessThan(100);
159+
});
160+
161+
it("单次移除仅放行 1 个 waiter,其余继续等待", async () => {
162+
const ids = await getSessionRuleIds();
163+
expect(ids.size).toBeLessThan(100);
164+
165+
const added = [];
166+
for (let w = ids.size; w < LIMIT_SESSION_RULES; w++) {
167+
const j = await nextSessionRuleId();
168+
added.push(j);
169+
ids.add(j);
170+
}
171+
expect(ids.size).toBeGreaterThan(1000);
172+
173+
// 在已达上限时发起多个并发调用
174+
const p1 = nextSessionRuleId();
175+
const p2 = nextSessionRuleId();
176+
const p3 = nextSessionRuleId();
177+
178+
const raceResult = await Promise.race([
179+
Promise.all([p1, p2, p3]).then(() => "resolved"),
180+
sleep(5).then(() => "pending"),
181+
]);
182+
expect(raceResult).toBe("pending");
183+
184+
// 单次释放 1 个 slot: 只应放行 1 个 waiter,剩余仍挂起
185+
removeSessionRuleIdEntry(added[0]);
186+
const firstResolved = await Promise.race([p1.then(() => "resolved"), sleep(50).then(() => "pending")]);
187+
expect(firstResolved).toBe("resolved");
188+
189+
const remainingStillPending = await Promise.race([
190+
Promise.all([p2, p3]).then(() => "resolved"),
191+
sleep(5).then(() => "pending"),
192+
]);
193+
expect(remainingStillPending).toBe("pending");
194+
195+
// 继续释放 2 个 slot,剩余 waiter 才能继续完成
196+
removeSessionRuleIdEntry(added[1]);
197+
removeSessionRuleIdEntry(added[2]);
198+
const results = await Promise.race([Promise.all([p2, p3]).then((vals) => vals), sleep(50).then(() => null)]);
199+
expect(results).not.toBeNull();
200+
201+
// 任何时候 size 都不应超过 LIMIT_SESSION_RULES
202+
expect(ids.size).toBeLessThanOrEqual(LIMIT_SESSION_RULES);
203+
204+
for (const k of added) {
205+
if (ids.has(k)) removeSessionRuleIdEntry(k);
206+
}
207+
// p1/p2/p3 分配的 id 也要清理
208+
const allocated = [await p1, ...(results as number[])];
209+
for (const k of allocated) {
210+
if (ids.has(k)) removeSessionRuleIdEntry(k);
211+
}
212+
213+
const res = await getSessionRuleIds();
214+
expect(res).toBe(ids);
215+
expect(res.size).toBeLessThan(100);
216+
});
217+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { Deferred } from "@App/pkg/utils/utils";
2+
import { deferred } from "@App/pkg/utils/utils";
3+
4+
let sessionRuleIdsPromise: Promise<Set<number>> | null = null;
5+
let sessionRuleIds: Set<number> | null = null;
6+
7+
let SESSION_RULE_ID_BEGIN = 10000;
8+
export const LIMIT_SESSION_RULES =
9+
process.env.VI_TESTING === "true" ? 1234 : chrome.declarativeNetRequest.MAX_NUMBER_OF_SESSION_RULES - 300;
10+
let lockSessionRuleCreation: Deferred<void> | null = null;
11+
12+
export const getSessionRuleIds = async (): Promise<Set<number>> => {
13+
if (!sessionRuleIdsPromise) {
14+
sessionRuleIdsPromise = chrome.declarativeNetRequest
15+
.getSessionRules()
16+
.then((rules) => {
17+
const existingRuleIds = rules.map((rule) => rule.id).filter(Boolean);
18+
// 根据历史 session rule 的最大 id 更新 SESSION_RULE_ID_BEGIN
19+
// 避免 SW 重启后从 10001 起做大量 do/while 递增扫描
20+
if (existingRuleIds.length > 0) {
21+
SESSION_RULE_ID_BEGIN = Math.max(SESSION_RULE_ID_BEGIN, ...existingRuleIds);
22+
}
23+
sessionRuleIds = new Set(existingRuleIds);
24+
return sessionRuleIds;
25+
})
26+
.catch((e) => {
27+
console.warn(e);
28+
sessionRuleIds = new Set<number>([]);
29+
return sessionRuleIds;
30+
});
31+
}
32+
const ruleIds = sessionRuleIds || (await sessionRuleIdsPromise);
33+
return ruleIds;
34+
};
35+
36+
export const removeSessionRuleIdEntry = (ruleId: number) => {
37+
if (ruleId <= 10000) {
38+
throw new Error("removeSessionRuleIdEntry cannot remove ids not created by nextSessionRuleId");
39+
}
40+
if (sessionRuleIds) {
41+
if (sessionRuleIds.delete(ruleId) === true) {
42+
if (ruleId <= SESSION_RULE_ID_BEGIN + 1) {
43+
SESSION_RULE_ID_BEGIN = ruleId - 1;
44+
}
45+
// 唤醒所有等待者: 每个等待者会在 while 循环内重新判定 size 并决定是继续分配还是再次等待
46+
// 这样 "1 次释放 => 1 个等待者通过",避免 N 个等待者同时放行再次撑爆上限
47+
lockSessionRuleCreation?.resolve();
48+
lockSessionRuleCreation = null;
49+
}
50+
}
51+
};
52+
53+
export const nextSessionRuleId = async () => {
54+
const ruleIds = await getSessionRuleIds();
55+
// 用 while 循环反复判定上限: 等待者被唤醒后如果 slot 已被其他等待者抢占,会再次进入等待
56+
while (ruleIds.size + 1 > LIMIT_SESSION_RULES) {
57+
if (!lockSessionRuleCreation) lockSessionRuleCreation = deferred<void>();
58+
await lockSessionRuleCreation.promise;
59+
}
60+
let id;
61+
do {
62+
id = ++SESSION_RULE_ID_BEGIN;
63+
} while (ruleIds.has(id));
64+
ruleIds.add(id);
65+
return id;
66+
};

0 commit comments

Comments
 (0)