Skip to content

Commit 80c5baf

Browse files
refactor(webapp): complete modularization of reconciliation logic
1 parent 4dfadb9 commit 80c5baf

File tree

2 files changed

+28
-72
lines changed

2 files changed

+28
-72
lines changed

apps/webapp/app/presenters/v3/RunPresenter.server.ts

Lines changed: 27 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { SpanSummary } from "~/v3/eventRepository/eventRepository.types";
88
import { getTaskEventStoreTableForRun } from "~/v3/taskEventStore.server";
99
import { isFailedRunStatus, isFinalRunStatus } from "~/v3/taskStatus";
1010
import { env } from "~/env.server";
11+
import { reconcileTraceWithRunLifecycle } from "./reconcileTrace.server";
1112

1213
type Result = Awaited<ReturnType<RunPresenter["call"]>>;
1314
export type Run = Result["run"];
@@ -124,6 +125,7 @@ export class RunPresenter {
124125
isFinished: isFinalRunStatus(run.status),
125126
startedAt: run.startedAt,
126127
completedAt: run.completedAt,
128+
createdAt: run.createdAt,
127129
logsDeletedAt: showDeletedLogs ? null : run.logsDeletedAt,
128130
rootTaskRun: run.rootTaskRun,
129131
parentTaskRun: run.parentTaskRun,
@@ -210,14 +212,30 @@ export class RunPresenter {
210212
const treeRootStartTimeMs = tree ? tree?.data.startTime.getTime() : 0;
211213
let totalDuration = tree?.data.duration ?? 0;
212214
const events = tree
213-
? flattenTree(tree).map((n) => {
214-
const offset = millisecondsToNanoseconds(
215-
n.data.startTime.getTime() - treeRootStartTimeMs
216-
);
215+
? flattenTree(tree).map((n, index) => {
216+
const isRoot = index === 0;
217+
const offset = millisecondsToNanoseconds(n.data.startTime.getTime() - treeRootStartTimeMs);
218+
219+
let nIsPartial = n.data.isPartial;
220+
let nDuration = n.data.duration;
221+
let nIsError = n.data.isError;
222+
223+
// NOTE: Clickhouse trace ingestion is eventually consistent.
224+
// When a run is marked finished in Postgres, we reconcile the
225+
// root span to reflect completion even if telemetry is still partial.
226+
// This is a deliberate UI-layer tradeoff to prevent stale or "stuck"
227+
// run states in the dashboard.
228+
if (isRoot && runData.isFinished && nIsPartial) {
229+
nIsPartial = false;
230+
nDuration = Math.max(nDuration ?? 0, postgresRunDuration);
231+
nIsError = isFailedRunStatus(runData.status);
232+
}
233+
217234
//only let non-debug events extend the total duration
218235
if (!n.data.isDebug) {
219-
totalDuration = Math.max(totalDuration, offset + n.data.duration);
236+
totalDuration = Math.max(totalDuration, offset + (nIsPartial ? 0 : nDuration));
220237
}
238+
221239
return {
222240
...n,
223241
data: {
@@ -228,9 +246,11 @@ export class RunPresenter {
228246
treeRootStartTimeMs
229247
),
230248
//set partial nodes to null duration
231-
duration: n.data.isPartial ? null : n.data.duration,
249+
duration: nIsPartial ? null : nDuration,
250+
isPartial: nIsPartial,
251+
isError: nIsError,
232252
offset,
233-
isRoot: n.id === traceSummary.rootSpan.id,
253+
isRoot,
234254
},
235255
};
236256
})
@@ -264,67 +284,3 @@ export class RunPresenter {
264284
}
265285
}
266286

267-
// NOTE: Clickhouse trace ingestion is eventually consistent.
268-
// When a run is marked finished in Postgres, we reconcile the
269-
// root span to reflect completion even if telemetry is still partial.
270-
// This is a deliberate UI-layer tradeoff to prevent stale or "stuck"
271-
// run states in the dashboard.
272-
export function reconcileTraceWithRunLifecycle(
273-
runData: {
274-
isFinished: boolean;
275-
status: Run["status"];
276-
createdAt: Date;
277-
completedAt: Date | null;
278-
rootTaskRun: { createdAt: Date } | null;
279-
},
280-
rootSpanId: string,
281-
events: RunEvent[],
282-
totalDuration: number
283-
): {
284-
events: RunEvent[];
285-
totalDuration: number;
286-
rootSpanStatus: "executing" | "completed" | "failed";
287-
} {
288-
const rootEvent = events.find((e) => e.id === rootSpanId);
289-
const currentStatus: "executing" | "completed" | "failed" = rootEvent
290-
? rootEvent.data.isError
291-
? "failed"
292-
: !rootEvent.data.isPartial
293-
? "completed"
294-
: "executing"
295-
: "executing";
296-
297-
if (!runData.isFinished) {
298-
return { events, totalDuration, rootSpanStatus: currentStatus };
299-
}
300-
301-
const postgresRunDuration = runData.completedAt
302-
? millisecondsToNanoseconds(
303-
runData.completedAt.getTime() -
304-
(runData.rootTaskRun?.createdAt ?? runData.createdAt).getTime()
305-
)
306-
: 0;
307-
308-
const updatedTotalDuration = Math.max(totalDuration, postgresRunDuration);
309-
310-
const updatedEvents = events.map((e) => {
311-
if (e.id === rootSpanId && e.data.isPartial) {
312-
return {
313-
...e,
314-
data: {
315-
...e.data,
316-
isPartial: false,
317-
duration: Math.max(e.data.duration ?? 0, postgresRunDuration),
318-
isError: isFailedRunStatus(runData.status),
319-
},
320-
};
321-
}
322-
return e;
323-
});
324-
325-
return {
326-
events: updatedEvents,
327-
totalDuration: updatedTotalDuration,
328-
rootSpanStatus: isFailedRunStatus(runData.status) ? "failed" : "completed",
329-
};
330-
}

apps/webapp/test/RunPresenter.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ vi.mock("../app/utils/username", () => ({
2424
getUsername: vi.fn(),
2525
}));
2626

27-
import { reconcileTraceWithRunLifecycle } from "../app/presenters/v3/RunPresenter.server";
27+
import { reconcileTraceWithRunLifecycle } from "../app/presenters/v3/reconcileTrace.server";
2828
import { millisecondsToNanoseconds } from "@trigger.dev/core/v3";
2929

3030
describe("reconcileTraceWithRunLifecycle", () => {

0 commit comments

Comments
 (0)