fix(workbench): show and accelerate Kafka replay
This commit is contained in:
@@ -6,7 +6,7 @@ import { test } from "bun:test";
|
||||
|
||||
import { createCloudApiBunServer } from "./bun-server.ts";
|
||||
import { buildCloudApiReadiness } from "./health-contract.ts";
|
||||
import { decodeCanonicalAgentRunKafkaMessage, kafkaDnsLookup, kafkaEventBridgeConfig, kafkaPartitionForKey, projectAgentRunKafkaEventToHwlabEvent, publishAgentRunKafkaMessageLive, relayHwlabKafkaOutboxOnce, startHwlabKafkaEventBridge } from "./kafka-event-bridge.ts";
|
||||
import { decodeCanonicalAgentRunKafkaMessage, kafkaDnsLookup, kafkaEventBridgeConfig, kafkaMessageKeyCanMatchPartitionKey, kafkaPartitionForKey, projectAgentRunKafkaEventToHwlabEvent, publishAgentRunKafkaMessageLive, relayHwlabKafkaOutboxOnce, startHwlabKafkaEventBridge } from "./kafka-event-bridge.ts";
|
||||
|
||||
test("Workbench refresh replay resolves the same Kafka partition as the session-keyed producer", () => {
|
||||
const offsets = [
|
||||
@@ -23,6 +23,13 @@ test("Workbench refresh replay resolves the same Kafka partition as the session-
|
||||
assert.equal(kafkaPartitionForKey(null, "hwlab.event.v1", offsets), null);
|
||||
});
|
||||
|
||||
test("Workbench refresh replay rejects foreign keyed values before JSON parsing", () => {
|
||||
assert.equal(kafkaMessageKeyCanMatchPartitionKey("ses_target", "ses_target"), true);
|
||||
assert.equal(kafkaMessageKeyCanMatchPartitionKey("ses_foreign", "ses_target"), false);
|
||||
assert.equal(kafkaMessageKeyCanMatchPartitionKey(null, "ses_target"), true);
|
||||
assert.equal(kafkaMessageKeyCanMatchPartitionKey("ses_target", null), true);
|
||||
});
|
||||
|
||||
const PROJECTOR_ENV = Object.freeze({
|
||||
HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED: "false",
|
||||
HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED: "false",
|
||||
|
||||
@@ -973,6 +973,7 @@ export async function queryKafkaEventStream({ env = process.env, stream = "hwlab
|
||||
const clientId = stringValue(env.HWLAB_KAFKA_CLIENT_ID) || DEFAULT_CLIENT_ID;
|
||||
const resolvedTopic = stringValue(topic) || kafkaTopicForStream(stream, env);
|
||||
const maxEvents = Math.max(1, integerValue(limit) || DEFAULT_QUERY_LIMIT);
|
||||
const resolvedPartitionKey = stringValue(partitionKey);
|
||||
const maxScannedRecords = scanLimit === null || scanLimit === undefined
|
||||
? null
|
||||
: strictPositiveInteger(scanLimit, "scanLimit");
|
||||
@@ -989,7 +990,7 @@ export async function queryKafkaEventStream({ env = process.env, stream = "hwlab
|
||||
timing.endOffsetSnapshotMs = Date.now() - stageStartedAtMs;
|
||||
const endOffsetByPartition = new Map(endOffsetSnapshot.offsets.map((entry) => [entry.partition, entry.endOffset]));
|
||||
const startOffsetByPartition = new Map(endOffsetSnapshot.offsets.map((entry) => [entry.partition, entry.startOffset]));
|
||||
const targetPartition = kafkaPartitionForKey(partitionKey, resolvedTopic, endOffsetSnapshot.offsets);
|
||||
const targetPartition = kafkaPartitionForKey(resolvedPartitionKey, resolvedTopic, endOffsetSnapshot.offsets);
|
||||
const skippedPartitions = targetPartition === null ? [] : endOffsetSnapshot.offsets.filter((entry) => entry.partition !== targetPartition).map((entry) => entry.partition);
|
||||
const requiresScopedGroupOffsets = skippedPartitions.length > 0;
|
||||
const reachedEndPartitions = new Set([...skippedPartitions, ...endOffsetSnapshot.offsets.filter(kafkaOffsetPartitionIsEmpty).map((entry) => entry.partition)]);
|
||||
@@ -1009,6 +1010,7 @@ export async function queryKafkaEventStream({ env = process.env, stream = "hwlab
|
||||
let parsedCount = 0;
|
||||
let invalidJsonCount = 0;
|
||||
let filterRejectedCount = 0;
|
||||
let keyRejectedCount = 0;
|
||||
let excludedPostBarrierCount = 0;
|
||||
let completionReason = "timeout";
|
||||
let timer = null;
|
||||
@@ -1090,22 +1092,28 @@ export async function queryKafkaEventStream({ env = process.env, stream = "hwlab
|
||||
}
|
||||
scannedCount += 1;
|
||||
lastScannedOffsetByPartition.set(partition, stringValue(message.offset));
|
||||
const valueText = message.value ? Buffer.from(message.value).toString("utf8") : "";
|
||||
const value = parseJson(valueText);
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) invalidJsonCount += 1;
|
||||
else {
|
||||
parsedCount += 1;
|
||||
if (!eventMatchesFilters(value, { traceId, sessionId, runId, commandId })) filterRejectedCount += 1;
|
||||
const messageKey = message.key ? Buffer.from(message.key).toString("utf8") : null;
|
||||
if (!kafkaMessageKeyCanMatchPartitionKey(messageKey, resolvedPartitionKey)) {
|
||||
filterRejectedCount += 1;
|
||||
keyRejectedCount += 1;
|
||||
} else {
|
||||
const valueText = message.value ? Buffer.from(message.value).toString("utf8") : "";
|
||||
const value = parseJson(valueText);
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) invalidJsonCount += 1;
|
||||
else {
|
||||
events.push({
|
||||
topic: messageTopic,
|
||||
partition,
|
||||
offset: message.offset,
|
||||
key: message.key ? Buffer.from(message.key).toString("utf8") : null,
|
||||
timestamp: message.timestamp ?? null,
|
||||
valueSha256: sha256(valueText),
|
||||
value
|
||||
});
|
||||
parsedCount += 1;
|
||||
if (!eventMatchesFilters(value, { traceId, sessionId, runId, commandId })) filterRejectedCount += 1;
|
||||
else {
|
||||
events.push({
|
||||
topic: messageTopic,
|
||||
partition,
|
||||
offset: message.offset,
|
||||
key: messageKey,
|
||||
timestamp: message.timestamp ?? null,
|
||||
valueSha256: sha256(valueText),
|
||||
value
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fromBeginning !== false && endOffset && kafkaOffsetReached(message.offset, endOffset)) reachedEndPartitions.add(partition);
|
||||
@@ -1153,6 +1161,7 @@ export async function queryKafkaEventStream({ env = process.env, stream = "hwlab
|
||||
matchedCount: events.length,
|
||||
invalidJsonCount,
|
||||
filterRejectedCount,
|
||||
keyRejectedCount,
|
||||
excludedPostBarrierCount,
|
||||
completionReason,
|
||||
completion: {
|
||||
@@ -1190,6 +1199,12 @@ export async function queryKafkaEventStream({ env = process.env, stream = "hwlab
|
||||
}
|
||||
}
|
||||
|
||||
export function kafkaMessageKeyCanMatchPartitionKey(messageKey, partitionKey) {
|
||||
const expected = stringValue(partitionKey);
|
||||
const actual = stringValue(messageKey);
|
||||
return !expected || !actual || actual === expected;
|
||||
}
|
||||
|
||||
export function kafkaPartitionForKey(partitionKey, topic, offsets = []) {
|
||||
const key = stringValue(partitionKey);
|
||||
const partitionMetadata = offsets
|
||||
|
||||
@@ -778,7 +778,10 @@ async function handleKafkaRefreshReplayWorkbenchRealtimeHttp(request, response,
|
||||
completionReason: result?.completionReason ?? null,
|
||||
complete: result?.completion?.complete === true,
|
||||
scannedCount: result?.scannedCount ?? null,
|
||||
parsedCount: result?.parsedCount ?? null,
|
||||
matchedCount: result?.matchedCount ?? null,
|
||||
filterRejectedCount: result?.filterRejectedCount ?? null,
|
||||
keyRejectedCount: result?.keyRejectedCount ?? null,
|
||||
partitionKeyScoped: result?.partitionKeyScoped === true,
|
||||
targetPartition: result?.targetPartition ?? null,
|
||||
timing: result?.timing ?? null,
|
||||
|
||||
@@ -403,7 +403,10 @@ function boundedQueryDiagnostics(result) {
|
||||
completionReason: textValue(result?.completion?.reason ?? result?.completionReason),
|
||||
complete: result?.completion?.complete === true,
|
||||
scannedCount: numericValue(result?.scannedCount),
|
||||
parsedCount: numericValue(result?.parsedCount),
|
||||
matchedCount: numericValue(result?.matchedCount),
|
||||
filterRejectedCount: numericValue(result?.filterRejectedCount),
|
||||
keyRejectedCount: numericValue(result?.keyRejectedCount),
|
||||
partitionKeyScoped: result?.partitionKeyScoped === true,
|
||||
targetPartition: partitionValue(result?.targetPartition),
|
||||
timing: {
|
||||
|
||||
@@ -170,7 +170,10 @@ function nativeKafkaRefreshEventStream(bridge: any, sessionId: string, traceId:
|
||||
completionReason: result?.completionReason ?? null,
|
||||
complete: result?.completion?.complete === true,
|
||||
scannedCount: result?.scannedCount ?? null,
|
||||
parsedCount: result?.parsedCount ?? null,
|
||||
matchedCount: result?.matchedCount ?? null,
|
||||
filterRejectedCount: result?.filterRejectedCount ?? null,
|
||||
keyRejectedCount: result?.keyRejectedCount ?? null,
|
||||
partitionKeyScoped: result?.partitionKeyScoped === true,
|
||||
targetPartition: result?.targetPartition ?? null,
|
||||
timing: result?.timing ?? null,
|
||||
|
||||
@@ -185,7 +185,10 @@ describe("Workbench native HTTP adapter", () => {
|
||||
endOffsetsAvailable: true,
|
||||
endOffsets: [{ partition: 0, startOffset: "0", endOffset: "100" }],
|
||||
scannedCount: 100,
|
||||
parsedCount: 100,
|
||||
matchedCount: 100,
|
||||
filterRejectedCount: 0,
|
||||
keyRejectedCount: 0,
|
||||
partitionKeyScoped: true,
|
||||
targetPartition: 0,
|
||||
timing: { endOffsetSnapshotMs: 2, groupOffsetsMs: 0, consumerConnectMs: 3, consumerSubscribeMs: 1, scanMs: 7, cleanupMs: 1, totalMs: 14 }
|
||||
@@ -222,7 +225,10 @@ describe("Workbench native HTTP adapter", () => {
|
||||
query: {
|
||||
complete: true,
|
||||
scannedCount: 100,
|
||||
parsedCount: 100,
|
||||
matchedCount: 100,
|
||||
filterRejectedCount: 0,
|
||||
keyRejectedCount: 0,
|
||||
partitionKeyScoped: true,
|
||||
targetPartition: 0,
|
||||
timing: { scanMs: 7, totalMs: 14 }
|
||||
|
||||
Reference in New Issue
Block a user