mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
e8d546c8da
* fix(buzz): page reconnect history instead of dropping older messages On reconnect the Buzz gateway asked the relay for a single capped page of room history and treated EOSE as a complete recovery. Any backlog larger than the per-room replay limit was never requested again, so those messages never reached handleBuzzInbound, the agent, or the session transcript, and nothing reported the loss. The room subscription now records how much history its first page returned and, when that page filled the limit, pages older history through the relay until it is exhausted. Each page is dispatched through the existing bounded replay queue and the next page waits for queue capacity, so recovery stays memory bounded. A room whose backlog cannot be paged past a single timestamp is now reported instead of dropped silently. Membership tracking moves to room-membership-tracker.ts because buzz-bus.ts was already at the 700 line ceiling. * fix(buzz): hold dispatch capacity while a history page is in flight Catch-up asked the replay queue whether capacity existed and then issued the relay query, but nothing held those slots. Live room events arriving while the query was in flight could consume them, so admitting the page afterwards could report overflow, which closes the dispatch queue and drops every queued message before forcing a reconnect. A busy room with slow inbound handlers could repeat that instead of finishing recovery. Capacity is now reserved rather than sampled. reserveCapacity resolves with a reservation that owns its slots until released, page events are admitted through that reservation, and the reservation is released once the page is enqueued. Live enqueues keep the full pending limit and are never rejected earlier because a reservation is outstanding. An overflow reported through a reservation means the relay exceeded the page it was asked for, so it is reported as a history error instead of tearing down the session. * fix(buzz): bound reconnect history pages * fix(buzz): preserve replay reservations * test(buzz): cover catch-up settlement paths * fix(buzz): drain saturated history ranges --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
140 lines
4.0 KiB
TypeScript
140 lines
4.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
BUZZ_REPLAY_DISPATCH_MAX_PENDING,
|
|
createBuzzReplayDispatchQueue,
|
|
} from "./replay-dispatch.js";
|
|
|
|
const REPLAY_DISPATCH_CONCURRENCY = 8;
|
|
|
|
function createBlockedQueue() {
|
|
const releases: Array<() => void> = [];
|
|
const queue = createBuzzReplayDispatchQueue({ onTaskError: () => {} });
|
|
const blockTask = () => {
|
|
let release = () => {};
|
|
const gate = new Promise<void>((resolve) => {
|
|
release = resolve;
|
|
});
|
|
releases.push(release);
|
|
return async () => {
|
|
await gate;
|
|
};
|
|
};
|
|
return { queue, releases, blockTask };
|
|
}
|
|
|
|
async function flush(): Promise<void> {
|
|
for (let index = 0; index < 5; index += 1) {
|
|
await Promise.resolve();
|
|
}
|
|
}
|
|
|
|
describe("Buzz replay dispatch capacity reservations", () => {
|
|
it("withholds a reservation while queued work occupies the pending limit", async () => {
|
|
const { queue, blockTask } = createBlockedQueue();
|
|
for (
|
|
let index = 0;
|
|
index < BUZZ_REPLAY_DISPATCH_MAX_PENDING + REPLAY_DISPATCH_CONCURRENCY;
|
|
index += 1
|
|
) {
|
|
expect(queue.enqueue(blockTask())).toBe("accepted");
|
|
}
|
|
|
|
let granted: unknown = "pending";
|
|
void queue.reserveCapacity(10).then((reservation) => {
|
|
granted = reservation;
|
|
});
|
|
await flush();
|
|
|
|
expect(granted).toBe("pending");
|
|
expect(queue.enqueue(blockTask())).toBe("overflow");
|
|
});
|
|
|
|
it("withholds reserved slots from later live events", async () => {
|
|
const { queue, releases, blockTask } = createBlockedQueue();
|
|
const pageSize = 100;
|
|
for (
|
|
let index = 0;
|
|
index < BUZZ_REPLAY_DISPATCH_MAX_PENDING + REPLAY_DISPATCH_CONCURRENCY;
|
|
index += 1
|
|
) {
|
|
queue.enqueue(blockTask());
|
|
}
|
|
|
|
const reservationPromise = queue.reserveCapacity(pageSize);
|
|
for (let index = 0; index < pageSize; index += 1) {
|
|
releases[index]?.();
|
|
}
|
|
const reservation = await reservationPromise;
|
|
expect(reservation).toBeDefined();
|
|
|
|
for (let index = 0; index < pageSize; index += 1) {
|
|
expect(queue.enqueue(blockTask())).toBe("overflow");
|
|
}
|
|
const admissions = new Set<string>();
|
|
for (let index = 0; index < pageSize; index += 1) {
|
|
admissions.add(reservation?.enqueue(blockTask()) ?? "missing");
|
|
}
|
|
|
|
expect([...admissions]).toEqual(["accepted"]);
|
|
expect(reservation?.enqueue(blockTask())).toBe("overflow");
|
|
});
|
|
|
|
it("returns unused slots when a reservation is released", async () => {
|
|
const { queue, releases, blockTask } = createBlockedQueue();
|
|
for (
|
|
let index = 0;
|
|
index < BUZZ_REPLAY_DISPATCH_MAX_PENDING + REPLAY_DISPATCH_CONCURRENCY;
|
|
index += 1
|
|
) {
|
|
queue.enqueue(blockTask());
|
|
}
|
|
|
|
const firstPromise = queue.reserveCapacity(50);
|
|
for (let index = 0; index < 50; index += 1) {
|
|
releases[index]?.();
|
|
}
|
|
const first = await firstPromise;
|
|
expect(first).toBeDefined();
|
|
|
|
let second: unknown = "pending";
|
|
void queue.reserveCapacity(50).then((reservation) => {
|
|
second = reservation;
|
|
});
|
|
await flush();
|
|
expect(second).toBe("pending");
|
|
|
|
first?.release();
|
|
await flush();
|
|
expect(second).toBeDefined();
|
|
expect(second).not.toBe("pending");
|
|
});
|
|
|
|
it("abandons waiting reservations once the queue closes", async () => {
|
|
const { queue, blockTask } = createBlockedQueue();
|
|
for (
|
|
let index = 0;
|
|
index < BUZZ_REPLAY_DISPATCH_MAX_PENDING + REPLAY_DISPATCH_CONCURRENCY;
|
|
index += 1
|
|
) {
|
|
queue.enqueue(blockTask());
|
|
}
|
|
const reservationPromise = queue.reserveCapacity(10);
|
|
|
|
void queue.close();
|
|
|
|
expect(await reservationPromise).toBeUndefined();
|
|
expect(await queue.reserveCapacity(1)).toBeUndefined();
|
|
});
|
|
|
|
it("rejects work from a held reservation after the queue closes", async () => {
|
|
const { queue, blockTask } = createBlockedQueue();
|
|
const reservation = await queue.reserveCapacity(2);
|
|
|
|
await queue.close();
|
|
|
|
expect(reservation?.enqueue(blockTask())).toBe("closed");
|
|
reservation?.release();
|
|
expect(await queue.reserveCapacity(1)).toBeUndefined();
|
|
});
|
|
});
|