mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-08 03:10:26 +00:00
Simplify parallel duration and event failure logging
The parallel stage summary rendered a Duration tile directly below StageMetaBar, which already shows the same stage's duration with a live ticking clock and a started-at tooltip. The two disagreed while running: the meta bar counted up, the tile showed the static word "running". The cancelled-stage bug lived only in the duplicate. Drop the tile. The meta bar owns duration for every stage renderer, and it was already correct for cancelled, pending and skipped stages. That removes the three-way duration branch, the "--" sentinel decode, and the ACTIVE_STAGE_STATES and formatDurationMs imports. With the tile gone, ParallelOverview.durationMs is dead, as were successCount, failureCount and isComplete — the renderer counts the branch rows it draws. ParallelOverview reduces to branch identity. For run event write failures, log the first at error with run_id and event name, the rest at debug, and summarize new losses at flush. A broken sink fails for every event, so a bare error would emit one "investigate me" line per event for the life of the run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8881bcab3e
commit
8db771bbd0
5 changed files with 53 additions and 51 deletions
|
|
@ -195,15 +195,11 @@ describe("parseParallelOverview", () => {
|
|||
const overview = parseParallelOverview(events);
|
||||
expect(overview).toEqual({
|
||||
branchCount: 3,
|
||||
successCount: 2,
|
||||
failureCount: 1,
|
||||
durationMs: 12000,
|
||||
results: [
|
||||
{ id: "branch-a", index: null, itemLabel: null, status: "succeeded" },
|
||||
{ id: "branch-b", index: null, itemLabel: null, status: "succeeded" },
|
||||
{ id: "branch-c", index: null, itemLabel: null, status: "failed" },
|
||||
],
|
||||
isComplete: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -249,7 +245,6 @@ describe("parseParallelOverview", () => {
|
|||
}),
|
||||
];
|
||||
const overview = parseParallelOverview(events);
|
||||
expect(overview.isComplete).toBe(false);
|
||||
expect(overview.branchCount).toBe(4);
|
||||
expect(overview.results).toEqual([]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -172,35 +172,28 @@ export interface ParallelBranchSummary {
|
|||
|
||||
export interface ParallelOverview {
|
||||
branchCount: number | null;
|
||||
successCount: number | null;
|
||||
failureCount: number | null;
|
||||
durationMs: number | null;
|
||||
results: ParallelBranchSummary[];
|
||||
isComplete: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Roll up the `parallel.started` (announces branch count) and
|
||||
* `parallel.completed` (carries the rolled-up results) events for a parallel
|
||||
* `parallel.completed` (carries the per-branch results) events for a parallel
|
||||
* stage. Pre-completion, only the announce data is available.
|
||||
*
|
||||
* Only branch identity is parsed. The event's own `success_count`,
|
||||
* `failure_count` and `duration_ms` rollups are deliberately ignored: the
|
||||
* renderer counts the branch rows it actually draws, and duration comes from
|
||||
* the stage record via `StageMetaBar`.
|
||||
*/
|
||||
export function parseParallelOverview(events: EventEnvelope[]): ParallelOverview {
|
||||
let branchCount: number | null = null;
|
||||
let successCount: number | null = null;
|
||||
let failureCount: number | null = null;
|
||||
let durationMs: number | null = null;
|
||||
let results: ParallelBranchSummary[] = [];
|
||||
let isComplete = false;
|
||||
|
||||
for (const event of events) {
|
||||
const props: UnknownRecord = event.properties ?? {};
|
||||
if (event.event === "parallel.started") {
|
||||
branchCount = getNumber(props, "branch_count") ?? branchCount;
|
||||
} else if (event.event === "parallel.completed") {
|
||||
isComplete = true;
|
||||
successCount = getNumber(props, "success_count") ?? successCount;
|
||||
failureCount = getNumber(props, "failure_count") ?? failureCount;
|
||||
durationMs = getNumber(props, "duration_ms") ?? durationMs;
|
||||
const rawResults = getArray(props, "results") ?? [];
|
||||
results = rawResults
|
||||
.map((entry) => {
|
||||
|
|
@ -218,14 +211,7 @@ export function parseParallelOverview(events: EventEnvelope[]): ParallelOverview
|
|||
}
|
||||
}
|
||||
|
||||
return {
|
||||
branchCount,
|
||||
successCount,
|
||||
failureCount,
|
||||
durationMs,
|
||||
results,
|
||||
isComplete,
|
||||
};
|
||||
return { branchCount, results };
|
||||
}
|
||||
|
||||
export interface ReducerTranscript {
|
||||
|
|
|
|||
|
|
@ -144,18 +144,23 @@ describe("ParallelChildren", () => {
|
|||
expect(statValue(renderer, "Failed")).toBe("0");
|
||||
});
|
||||
|
||||
test("uses the stage duration when cancellation interrupts the parallel summary", () => {
|
||||
test("shows the recorded stage duration when cancellation interrupts the fan-out", () => {
|
||||
const renderer = renderParallel(
|
||||
[startedEvent(2)],
|
||||
[],
|
||||
{
|
||||
...parallelStage,
|
||||
makeStage({
|
||||
id: "fork@1",
|
||||
name: "fork",
|
||||
nodeId: "fork",
|
||||
handler: "parallel",
|
||||
status: StageState.CANCELLED,
|
||||
duration: "53m 29s",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(statValue(renderer, "Duration")).toBe("53m 29s");
|
||||
// No `parallel.completed` event is emitted for an interrupted fan-out, so
|
||||
// the stage record is the only duration there is.
|
||||
expect(textContent(renderer.root)).toContain("53m 29s");
|
||||
});
|
||||
|
||||
test("keeps looped fork links scoped to the selected fork visit", () => {
|
||||
|
|
|
|||
|
|
@ -5,13 +5,7 @@ import { StageState } from "@qltysh/fabro-api-client";
|
|||
import type { EventEnvelope } from "@qltysh/fabro-api-client";
|
||||
|
||||
import type { Stage } from "../stage-sidebar";
|
||||
import {
|
||||
ACTIVE_STAGE_STATES,
|
||||
formatStageLabel,
|
||||
stageStatusLabel,
|
||||
stageStatusTone,
|
||||
} from "../../lib/stage-sidebar";
|
||||
import { formatDurationMs } from "../../lib/format";
|
||||
import { formatStageLabel, stageStatusLabel, stageStatusTone } from "../../lib/stage-sidebar";
|
||||
import { StageMetaBar } from "./meta-bar";
|
||||
import { parseParallelOverview } from "./helpers";
|
||||
import type { ParallelBranchSummary } from "./helpers";
|
||||
|
|
@ -187,18 +181,13 @@ export function ParallelChildren({
|
|||
else if (row.status === StageState.FAILED) failureCount += 1;
|
||||
}
|
||||
|
||||
let duration = stage.duration === "--" ? "—" : stage.duration;
|
||||
if (overview.durationMs != null) {
|
||||
duration = formatDurationMs(overview.durationMs);
|
||||
} else if (ACTIVE_STAGE_STATES.has(stage.status)) {
|
||||
duration = "running";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 pl-3 pr-4 sm:pr-6 lg:pr-8">
|
||||
{/* The meta bar owns duration for every stage renderer, including the
|
||||
live clock while running, so the tiles below stay outcome-only. */}
|
||||
<StageMetaBar stage={stage} />
|
||||
|
||||
<section className="grid grid-cols-2 gap-x-6 gap-y-4 rounded-lg bg-panel p-5 outline-1 -outline-offset-1 outline-line sm:grid-cols-4">
|
||||
<section className="grid grid-cols-2 gap-x-6 gap-y-4 rounded-lg bg-panel p-5 outline-1 -outline-offset-1 outline-line sm:grid-cols-3">
|
||||
<StatItem label="Branches" value={branchCount || "—"} />
|
||||
<StatItem
|
||||
label="Succeeded"
|
||||
|
|
@ -210,10 +199,6 @@ export function ParallelChildren({
|
|||
value={failureCount}
|
||||
tone={failureCount > 0 ? "danger" : "default"}
|
||||
/>
|
||||
<StatItem
|
||||
label="Duration"
|
||||
value={duration}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
|
|
|||
|
|
@ -163,14 +163,45 @@ impl RunEventLogger {
|
|||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
|
||||
tokio::spawn(async move {
|
||||
// A dropped run event is unrecoverable history loss, so the first
|
||||
// one is an ERROR worth investigating. A broken sink fails for
|
||||
// every event that follows, so report the rest as a count at flush
|
||||
// instead of one ERROR per event. Flush runs per stage and per
|
||||
// agent turn, so only losses since the last summary are reported.
|
||||
let mut write_failures: u64 = 0;
|
||||
let mut summarized_failures: u64 = 0;
|
||||
while let Some(command) = rx.recv().await {
|
||||
match command {
|
||||
RunEventCommand::Event(event) => {
|
||||
if let Err(err) = sink.write_run_event(&event).await {
|
||||
tracing::error!(error = %err, "Failed to write run event");
|
||||
write_failures += 1;
|
||||
if write_failures == 1 {
|
||||
tracing::error!(
|
||||
run_id = %event.run_id,
|
||||
event = %event.body.event_name(),
|
||||
error = %err,
|
||||
"Failed to write run event",
|
||||
);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
run_id = %event.run_id,
|
||||
event = %event.body.event_name(),
|
||||
failures = write_failures,
|
||||
error = %err,
|
||||
"Failed to write run event",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
RunEventCommand::Flush(tx) => {
|
||||
if write_failures > summarized_failures {
|
||||
tracing::error!(
|
||||
lost = write_failures - summarized_failures,
|
||||
total = write_failures,
|
||||
"Run events were lost to write failures",
|
||||
);
|
||||
summarized_failures = write_failures;
|
||||
}
|
||||
let _ = tx.send(());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue