fix: 披露 Workbench Kafka 重放耗时
This commit is contained in:
@@ -758,17 +758,34 @@ async function handleKafkaRefreshReplayWorkbenchRealtimeHttp(request, response,
|
|||||||
liveBufferLimit: refreshReplay.liveBufferLimit,
|
liveBufferLimit: refreshReplay.liveBufferLimit,
|
||||||
identityWindowLimit: refreshReplay.matchedEventLimit + refreshReplay.liveBufferLimit,
|
identityWindowLimit: refreshReplay.matchedEventLimit + refreshReplay.liveBufferLimit,
|
||||||
subscribeLive: (listener) => bridge.subscribeLiveHwlabEvents(listener),
|
subscribeLive: (listener) => bridge.subscribeLiveHwlabEvents(listener),
|
||||||
queryRetention: ({ signal }) => bridge.queryHwlabEventRetention({
|
queryRetention: async ({ signal }) => {
|
||||||
sessionId: requestedSessionId,
|
const result = await bridge.queryHwlabEventRetention({
|
||||||
traceId: requestedTraceId,
|
sessionId: requestedSessionId,
|
||||||
limit: refreshReplay.matchedEventLimit,
|
traceId: requestedTraceId,
|
||||||
scanLimit: refreshReplay.scanLimit,
|
limit: refreshReplay.matchedEventLimit,
|
||||||
timeoutMs: refreshReplay.timeoutMs,
|
scanLimit: refreshReplay.scanLimit,
|
||||||
groupIdPrefix: refreshReplay.groupIdPrefix,
|
timeoutMs: refreshReplay.timeoutMs,
|
||||||
partitionKey: requestedSessionId,
|
groupIdPrefix: refreshReplay.groupIdPrefix,
|
||||||
fromBeginning: true,
|
partitionKey: requestedSessionId,
|
||||||
signal
|
fromBeginning: true,
|
||||||
}),
|
signal
|
||||||
|
});
|
||||||
|
(options.logger ?? console).log(JSON.stringify({
|
||||||
|
code: "workbench-kafka-refresh-query",
|
||||||
|
component: "hwlab-workbench-realtime",
|
||||||
|
sessionId: requestedSessionId,
|
||||||
|
traceId: requestedTraceId,
|
||||||
|
completionReason: result?.completionReason ?? null,
|
||||||
|
complete: result?.completion?.complete === true,
|
||||||
|
scannedCount: result?.scannedCount ?? null,
|
||||||
|
matchedCount: result?.matchedCount ?? null,
|
||||||
|
partitionKeyScoped: result?.partitionKeyScoped === true,
|
||||||
|
targetPartition: result?.targetPartition ?? null,
|
||||||
|
timing: result?.timing ?? null,
|
||||||
|
valuesPrinted: false
|
||||||
|
}));
|
||||||
|
return result;
|
||||||
|
},
|
||||||
deliverEvent: (envelope, transport) => enqueueEvent("hwlab.event.v1", envelope, transport),
|
deliverEvent: (envelope, transport) => enqueueEvent("hwlab.event.v1", envelope, transport),
|
||||||
deliverConnected: (summary) => enqueueEvent("workbench.connected", {
|
deliverConnected: (summary) => enqueueEvent("workbench.connected", {
|
||||||
type: "connected",
|
type: "connected",
|
||||||
|
|||||||
@@ -132,7 +132,20 @@ describe("Workbench native HTTP adapter", () => {
|
|||||||
], {});
|
], {});
|
||||||
for (let index = 0; index < 20 && subscribers.size === 0; index += 1) await Bun.sleep(5);
|
for (let index = 0; index < 20 && subscribers.size === 0; index += 1) await Bun.sleep(5);
|
||||||
for (const listener of subscribers) listener({ schema: "hwlab.event.v1", sessionId: "ses_l0_sse", traceId: "trc_l0_sse", event: { type: "backend", eventType: "backend", label: "agentrun:backend:command-created" } });
|
for (const listener of subscribers) listener({ schema: "hwlab.event.v1", sessionId: "ses_l0_sse", traceId: "trc_l0_sse", event: { type: "backend", eventType: "backend", label: "agentrun:backend:command-created" } });
|
||||||
expect(await pending).toMatchObject({ ok: true, operation: "events.inspect", connected: true, eventCount: 1, eventTypes: ["backend"] });
|
expect(await pending).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
operation: "events.inspect",
|
||||||
|
connected: true,
|
||||||
|
connectedContract: {
|
||||||
|
deliverySemantics: "live-only",
|
||||||
|
realtimeSource: "hwlab.event.v1",
|
||||||
|
replay: false,
|
||||||
|
liveOnly: true,
|
||||||
|
refreshReplay: null
|
||||||
|
},
|
||||||
|
eventCount: 1,
|
||||||
|
eventTypes: ["backend"]
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
server.stop(true);
|
server.stop(true);
|
||||||
await app.close();
|
await app.close();
|
||||||
|
|||||||
@@ -89,11 +89,13 @@ async function inspectEvents(parsed: Parsed, env: Record<string, string | undefi
|
|||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
const business = businessFrames(frames);
|
const business = businessFrames(frames);
|
||||||
const connected = frames.some((entry) => entry.name === "workbench.connected");
|
const connectedFrame = frames.find((entry) => entry.name === "workbench.connected") ?? null;
|
||||||
|
const connected = connectedFrame !== null;
|
||||||
|
const connectedContract = connectedFrame ? workbenchConnectedContract(connectedFrame.data) : null;
|
||||||
const semantics = semanticSummary(business);
|
const semantics = semanticSummary(business);
|
||||||
const missingSemantics = waitFor.filter((semantic) => !semantics.observedSemantics.includes(semantic));
|
const missingSemantics = waitFor.filter((semantic) => !semantics.observedSemantics.includes(semantic));
|
||||||
const ok = connected && inspectionSatisfied(business, minEvents, waitFor);
|
const ok = connected && inspectionSatisfied(business, minEvents, waitFor);
|
||||||
return { ok, operation: "events.inspect", status: ok ? "passed" : "event-timeout", transport: "api-sse", baseUrl, route: `GET ${path}`, identity: { sessionId: sessionId || null, traceId: traceId || null }, connected, eventCount: business.length, eventTypes: business.map(eventType), timeoutMs, minEvents, waitFor, ...semantics, timing: timingSummary(business, timingDetail), missingSemantics, error: ok ? undefined : { code: "workbench_kafka_sse_event_missing", message: `Workbench SSE did not deliver the required Kafka event conditions; missing=${missingSemantics.join(",") || (business.length < minEvents ? `min-events:${minEvents}` : "connection")}` }, valuesPrinted: false };
|
return { ok, operation: "events.inspect", status: ok ? "passed" : "event-timeout", transport: "api-sse", baseUrl, route: `GET ${path}`, identity: { sessionId: sessionId || null, traceId: traceId || null }, connected, connectedContract, eventCount: business.length, eventTypes: business.map(eventType), timeoutMs, minEvents, waitFor, ...semantics, timing: timingSummary(business, timingDetail), missingSemantics, error: ok ? undefined : { code: "workbench_kafka_sse_event_missing", message: `Workbench SSE did not deliver the required Kafka event conditions; missing=${missingSemantics.join(",") || (business.length < minEvents ? `min-events:${minEvents}` : "connection")}` }, valuesPrinted: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
function commandFrom(parsed: Parsed, env: Record<string, string | undefined>): WorkbenchCommand {
|
function commandFrom(parsed: Parsed, env: Record<string, string | undefined>): WorkbenchCommand {
|
||||||
@@ -148,6 +150,31 @@ function semanticWait(value: unknown): EventSemantic[] {
|
|||||||
return [...new Set(requested)] as EventSemantic[];
|
return [...new Set(requested)] as EventSemantic[];
|
||||||
}
|
}
|
||||||
type SseFrame = { name: string; data: Record<string, any>; observedAt: string };
|
type SseFrame = { name: string; data: Record<string, any>; observedAt: string };
|
||||||
|
function workbenchConnectedContract(data: Record<string, any>) {
|
||||||
|
const refreshReplay = record(data.refreshReplay);
|
||||||
|
const counts = record(refreshReplay?.counts);
|
||||||
|
return {
|
||||||
|
deliverySemantics: String(data.deliverySemantics ?? "").trim() || null,
|
||||||
|
realtimeSource: String(data.realtimeSource ?? "").trim() || null,
|
||||||
|
replay: data.replay === true,
|
||||||
|
liveOnly: data.liveOnly === true,
|
||||||
|
lossPossible: data.lossPossible === true,
|
||||||
|
refreshReplay: refreshReplay ? {
|
||||||
|
phase: String(refreshReplay.phase ?? "").trim() || null,
|
||||||
|
topic: String(refreshReplay.topic ?? "").trim() || null,
|
||||||
|
barrier: Array.isArray(refreshReplay.barrier) ? refreshReplay.barrier : [],
|
||||||
|
counts: counts ? {
|
||||||
|
matched: Number(counts.matched ?? 0),
|
||||||
|
replayed: Number(counts.replayed ?? 0),
|
||||||
|
buffered: Number(counts.buffered ?? 0),
|
||||||
|
bufferedDelivered: Number(counts.bufferedDelivered ?? 0),
|
||||||
|
liveDelivered: Number(counts.liveDelivered ?? 0),
|
||||||
|
deduplicated: Number(counts.deduplicated ?? 0)
|
||||||
|
} : null
|
||||||
|
} : null,
|
||||||
|
valuesPrinted: false
|
||||||
|
};
|
||||||
|
}
|
||||||
function businessFrames(frames: SseFrame[]) { return frames.filter((entry) => entry.name === "hwlab.event.v1"); }
|
function businessFrames(frames: SseFrame[]) { return frames.filter((entry) => entry.name === "hwlab.event.v1"); }
|
||||||
function inspectionSatisfied(business: SseFrame[], minEvents: number, waitFor: EventSemantic[]) {
|
function inspectionSatisfied(business: SseFrame[], minEvents: number, waitFor: EventSemantic[]) {
|
||||||
if (business.length < minEvents) return false;
|
if (business.length < minEvents) return false;
|
||||||
|
|||||||
Reference in New Issue
Block a user