Skip to content

Commit f12fa44

Browse files
committed
fix(webapp): only count a dropped batch when nothing landed, and assert the isolation cap bounds inserts
The whole-batch-dropped counter fired whenever a recovered batch stripped no rows but dropped at least one, which mislabels a batch where clean rows landed alongside one unrecoverable row. Gate it on rowsDropped === batchSize (nothing landed) instead. The cap unit test now also asserts the bounded insert count so a broken budget cannot pass unnoticed.
1 parent efbcda6 commit f12fa44

3 files changed

Lines changed: 21 additions & 10 deletions

File tree

apps/webapp/app/services/runsReplicationService.server.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,7 +1142,7 @@ export class RunsReplicationService {
11421142
insertSync: (rows) => rawInsert(rows, { async_insert: 0 }),
11431143
stripJsonColumns: stripTaskRunJsonColumns,
11441144
});
1145-
this.#recordRecoveryOutcome(outcome, "task_runs_v2");
1145+
this.#recordRecoveryOutcome(outcome, "task_runs_v2", taskRunInserts.length);
11461146
return outcome;
11471147
});
11481148
}
@@ -1185,12 +1185,16 @@ export class RunsReplicationService {
11851185
insertSync: (rows) => rawInsert(rows, { async_insert: 0 }),
11861186
stripJsonColumns: stripPayloadJsonColumns,
11871187
});
1188-
this.#recordRecoveryOutcome(outcome, "raw_task_runs_payload_v1");
1188+
this.#recordRecoveryOutcome(outcome, "raw_task_runs_payload_v1", payloadInserts.length);
11891189
return outcome;
11901190
});
11911191
}
11921192

1193-
#recordRecoveryOutcome(outcome: JsonParseRecoveryOutcome, contextLabel: string) {
1193+
#recordRecoveryOutcome(
1194+
outcome: JsonParseRecoveryOutcome,
1195+
contextLabel: string,
1196+
batchSize: number
1197+
) {
11941198
if (outcome.kind !== "recovered") {
11951199
return;
11961200
}
@@ -1211,7 +1215,7 @@ export class RunsReplicationService {
12111215
if (outcome.rowsDropped > 0) {
12121216
this._permanentlyDroppedRows += outcome.rowsDropped;
12131217
this._rowsDroppedCounter.add(outcome.rowsDropped, { table: contextLabel });
1214-
if (outcome.rowsStripped === 0) {
1218+
if (outcome.rowsDropped === batchSize) {
12151219
this._permanentlyDroppedBatches += 1;
12161220
this._droppedBatchesCounter.add(1, { table: contextLabel });
12171221
}

apps/webapp/app/v3/eventRepository/clickhouseEventRepository.server.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ export class ClickhouseEventRepository implements IEventRepository {
354354
insertSync: (rows) => rawInsert(rows, { async_insert: 0 }),
355355
stripJsonColumns: stripTaskEventJsonColumns,
356356
});
357-
this.#recordRecoveryOutcome(outcome, contextLabel);
357+
this.#recordRecoveryOutcome(outcome, contextLabel, events.length);
358358

359359
logger.debug("ClickhouseEventRepository.flushBatch Inserted batch into clickhouse", {
360360
events: events.length,
@@ -386,15 +386,19 @@ export class ClickhouseEventRepository implements IEventRepository {
386386
insertSync: (batch) => rawInsert(batch, { async_insert: 0 }),
387387
stripJsonColumns: (row) => row,
388388
});
389-
this.#recordRecoveryOutcome(outcome, "llm_metrics_v1");
389+
this.#recordRecoveryOutcome(outcome, "llm_metrics_v1", rows.length);
390390

391391
logger.debug("ClickhouseEventRepository.flushLlmMetricsBatch Inserted LLM metrics batch", {
392392
rows: rows.length,
393393
outcome: outcome.kind,
394394
});
395395
}
396396

397-
#recordRecoveryOutcome(outcome: JsonParseRecoveryOutcome, contextLabel: string) {
397+
#recordRecoveryOutcome(
398+
outcome: JsonParseRecoveryOutcome,
399+
contextLabel: string,
400+
batchSize: number
401+
) {
398402
if (outcome.kind !== "recovered") {
399403
return;
400404
}
@@ -415,7 +419,7 @@ export class ClickhouseEventRepository implements IEventRepository {
415419
if (outcome.rowsDropped > 0) {
416420
this._permanentlyDroppedRows += outcome.rowsDropped;
417421
this._rowsDroppedCounter.add(outcome.rowsDropped, { table: contextLabel });
418-
if (outcome.rowsStripped === 0) {
422+
if (outcome.rowsDropped === batchSize) {
419423
this._permanentlyDroppedBatches += 1;
420424
this._droppedBatchesCounter.add(1, { table: contextLabel });
421425
}

apps/webapp/test/sanitizeRowsOnParseError.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,16 @@ function parseError() {
2727
*/
2828
function makeHarness() {
2929
const landed: FakeRow[] = [];
30+
let insertCalls = 0;
3031
const insertSync = async (rows: FakeRow[]) => {
32+
insertCalls += 1;
3133
if (rows.some((r) => r.poison || r.unstrippable)) throw parseError();
3234
landed.push(...rows);
3335
return undefined;
3436
};
3537
const stripJsonColumns = (row: FakeRow): FakeRow =>
3638
row.unstrippable ? row : { ...row, poison: false, stripped: true };
37-
return { landed, insertSync, stripJsonColumns };
39+
return { landed, insertSync, stripJsonColumns, insertCalls: () => insertCalls };
3840
}
3941

4042
describe("isClickHouseJsonParseError", () => {
@@ -379,7 +381,7 @@ describe("insertWithJsonParseRecovery", () => {
379381
});
380382

381383
it("caps recovery cost on a poison flood by stripping the remainder in one insert", async () => {
382-
const { landed, insertSync, stripJsonColumns } = makeHarness();
384+
const { landed, insertSync, stripJsonColumns, insertCalls } = makeHarness();
383385
const rows = Array.from({ length: 8 }, (_, id) => ({ id, poison: true as const }));
384386

385387
const outcome = await insertWithJsonParseRecovery({
@@ -400,6 +402,7 @@ describe("insertWithJsonParseRecovery", () => {
400402
}
401403
expect(landed).toHaveLength(8);
402404
expect(landed.every((r) => r.stripped)).toBe(true);
405+
expect(insertCalls()).toBeLessThanOrEqual(6);
403406
});
404407

405408
it("rethrows non-parse errors so the caller's transient-retry path handles them", async () => {

0 commit comments

Comments
 (0)