Archived
fix(app): 广播桥 —— 转发进来的不再 NOTIFY 回去,治两台后端互桥回弹的广播风暴
「自己发的自己不收」只挡一跳:收到别人的就 publish 到本地,而桥自己也订阅着这个 broadcaster, 那条转发被当成「自己推的」再 NOTIFY 一遍(from 换成自己)→ 对面不是它发的、再转发……两台接力 就是永动机。实测两台后端连同一个库,一次 POST /api/gov/process/validate 引出 45 秒 19 万条广播, 工作台三把查询被打成连续重取(用户看到的是「疯狂轮询三个接口」)。 转发进来的那条打记号(WeakSet,publish 同步派发),「本地 → 库」那一侧认出记号就不发; `bridgeBroadcaster` 第三参 `self` 可注入(同进程两座桥才模拟得出两台机器)。 2 测:转发不再 NOTIFY 出去 · 两台互桥一条广播各到一次就停(假频道封顶 50 条,让用例失败而不是撑爆进程)。 2026-08-26 修的,代码一直留在工作树没入库;CLAUDE.md 快照已记。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
This commit is contained in:
@@ -87,6 +87,61 @@ describe('bridgeBroadcaster', () => {
|
||||
expect(got).toEqual([]);
|
||||
});
|
||||
|
||||
it('**转发别人的那一条不再 NOTIFY 出去** —— 转发不是「自己推的」', async () => {
|
||||
const fake = fakePool();
|
||||
const bus = createBroadcaster();
|
||||
bridgeBroadcaster(fake.pool as never, bus);
|
||||
await settle();
|
||||
|
||||
fake.deliver({ kind: 'system', id: 'gov', reason: 'POST /api/gov/process/validate', from: '别的机器' });
|
||||
await settle();
|
||||
/*
|
||||
* 从前这里会 NOTIFY 一条(from 换成自己):对面收到不是它发的、再转发、再 NOTIFY……
|
||||
* 两台后端连同一个库时一条广播就此永动 —— 实测 45 秒 19 万条,工作台三把查询被打成连续重取。
|
||||
*/
|
||||
expect(fake.notified).toEqual([]);
|
||||
});
|
||||
|
||||
it('两台后端互桥,一条广播各到一次就停 —— 不来回弹', async () => {
|
||||
/** 两个假池共用一条「频道」:谁 NOTIFY 了,另一个当场收到(自己也收到,和真库一样) */
|
||||
const a = fakePool();
|
||||
const b = fakePool();
|
||||
const busA = createBroadcaster();
|
||||
const busB = createBroadcaster();
|
||||
const relay = (from: typeof a, to: typeof b) => {
|
||||
const origQuery = from.pool.query;
|
||||
from.pool.query = async (sql: string, params: unknown[]) => {
|
||||
const r = await origQuery(sql, params);
|
||||
// 回弹起来是无穷的:封顶 50 条,让用例「失败」而不是把测试进程撑爆
|
||||
if (from.notified.length > 50) return r;
|
||||
// 真库会把这条送给所有 LISTEN 的连接,包括发它的那个进程自己
|
||||
const wire = JSON.parse(String(params[1])) as Record<string, unknown>;
|
||||
from.deliver(wire);
|
||||
to.deliver(wire);
|
||||
return r;
|
||||
};
|
||||
};
|
||||
// 两台机器两个身份(真环境里 SELF 按进程生成;测试里显式给,不然同进程两座桥同名)
|
||||
bridgeBroadcaster(a.pool as never, busA, 'A');
|
||||
bridgeBroadcaster(b.pool as never, busB, 'B');
|
||||
await settle();
|
||||
relay(a, b);
|
||||
relay(b, a);
|
||||
|
||||
const seenA: string[] = [];
|
||||
const seenB: string[] = [];
|
||||
busA.subscribe((t) => seenA.push(t.reason));
|
||||
busB.subscribe((t) => seenB.push(t.reason));
|
||||
busA.publish({ kind: 'system', id: 'gov', reason: 'x' });
|
||||
for (let i = 0; i < 20; i += 1) await settle();
|
||||
|
||||
// A 本地一次、B 转发一次;NOTIFY 只有 A 发的那一条 —— B 转发的不再发回去
|
||||
expect(seenA).toEqual(['x']);
|
||||
expect(seenB).toEqual(['x']);
|
||||
expect(a.notified).toHaveLength(1);
|
||||
expect(b.notified).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('载荷坏了只丢这一条,连接照旧活着', async () => {
|
||||
const fake = fakePool();
|
||||
const bus = createBroadcaster();
|
||||
|
||||
Reference in New Issue
Block a user