Skip to content

Commit d1fc356

Browse files
committed
fix(webapp): timestamp live metric responses
1 parent 465f52b commit d1fc356

4 files changed

Lines changed: 47 additions & 17 deletions

File tree

  • apps/webapp/app
    • hooks
    • routes
      • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam
      • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues
      • resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam

apps/webapp/app/hooks/useMetricResourceQuery.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export function useMetricResourceQuery(query: string, opts: MetricResourceQueryO
102102
);
103103
const [isLoading, setIsLoading] = useState(true);
104104
const [failed, setFailed] = useState(false);
105+
const [responseReceivedAt, setResponseReceivedAt] = useState<number | null>(null);
105106
const abortRef = useRef<AbortController | null>(null);
106107
const loadedKeyRef = useRef<string | null>(null);
107108

@@ -111,6 +112,7 @@ export function useMetricResourceQuery(query: string, opts: MetricResourceQueryO
111112
loadedKeyRef.current = cacheKey;
112113
setRows(null);
113114
setFailed(false);
115+
setResponseReceivedAt(null);
114116
setIsLoading(false);
115117
return;
116118
}
@@ -125,6 +127,7 @@ export function useMetricResourceQuery(query: string, opts: MetricResourceQueryO
125127
loadedKeyRef.current = cacheKey;
126128
setRows(responseCache.get(cacheKey) ?? null);
127129
setFailed(false);
130+
setResponseReceivedAt(null);
128131
}
129132
setIsLoading(true);
130133
fetch("/resources/metric", {
@@ -152,15 +155,18 @@ export function useMetricResourceQuery(query: string, opts: MetricResourceQueryO
152155
cacheSet(cacheKey, data.data.rows);
153156
setRows(data.data.rows);
154157
setFailed(false);
158+
setResponseReceivedAt(Date.now());
155159
} else {
156160
setFailed(true);
161+
setResponseReceivedAt(null);
157162
}
158163
setIsLoading(false);
159164
})
160165
.catch((error) => {
161166
if (error instanceof DOMException && error.name === "AbortError") return;
162167
if (!controller.signal.aborted) {
163168
setFailed(true);
169+
setResponseReceivedAt(null);
164170
setIsLoading(false);
165171
}
166172
});
@@ -191,5 +197,11 @@ export function useMetricResourceQuery(query: string, opts: MetricResourceQueryO
191197
callback: load,
192198
});
193199

194-
return { rows: rows ?? [], isLoading, showLoading: isLoading && !rows, failed };
200+
return {
201+
rows: rows ?? [],
202+
isLoading,
203+
showLoading: isLoading && !rows,
204+
failed,
205+
responseReceivedAt,
206+
};
195207
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -420,25 +420,29 @@ function QueuesWithMetricsView() {
420420
// Empty rows (quiet env, or the very first fetch still in flight) fall back to the loader values,
421421
// so we never flash a stale 0. Fixed 15m window, env-wide (no queue filter), CH-only recurring
422422
// load; pauses while the tab is hidden (handled inside the hook).
423-
const { rows: liveBlockRows } = useMetricResourceQuery(QUEUE_LIVE_BLOCKS_QUERY, {
424-
organizationId: organization.id,
425-
projectId: project.id,
426-
environmentId: env.id,
427-
timeRange: { period: QUEUE_LIVE_BLOCKS_PERIOD, from: null, to: null },
428-
defaultPeriod: QUEUE_LIVE_BLOCKS_PERIOD,
429-
fillGaps: false,
430-
refreshIntervalMs: 15_000,
431-
});
423+
const { rows: liveBlockRows, responseReceivedAt } = useMetricResourceQuery(
424+
QUEUE_LIVE_BLOCKS_QUERY,
425+
{
426+
organizationId: organization.id,
427+
projectId: project.id,
428+
environmentId: env.id,
429+
timeRange: { period: QUEUE_LIVE_BLOCKS_PERIOD, from: null, to: null },
430+
defaultPeriod: QUEUE_LIVE_BLOCKS_PERIOD,
431+
fillGaps: false,
432+
refreshIntervalMs: 15_000,
433+
}
434+
);
432435
const lastLiveBlockRow =
433436
liveBlockRows.length > 0 ? liveBlockRows[liveBlockRows.length - 1] : null;
434437
// Only trust the gauge while its newest bucket is fresh. A row painted from the hook's cache on
435438
// client-side nav-back (responseCache), or a quiet env whose latest bucket is minutes old, must
436439
// not override the loader's Redis-exact live values with a stale count.
437440
const lastLiveBucketMs = lastLiveBlockRow ? tileTimeToMs(lastLiveBlockRow.t) : NaN;
438441
const freshLiveBlockRow =
442+
responseReceivedAt !== null &&
439443
lastLiveBlockRow &&
440444
Number.isFinite(lastLiveBucketMs) &&
441-
Date.now() - lastLiveBucketMs < LIVE_GAUGE_FRESH_MS
445+
responseReceivedAt - lastLiveBucketMs < LIVE_GAUGE_FRESH_MS
442446
? lastLiveBlockRow
443447
: null;
444448
const envQueuedLive = freshLiveBlockRow

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,7 +1121,7 @@ function QueueStats({
11211121
// Latest gauges from ClickHouse, polled every 15s so the live blocks keep ticking after first
11221122
// paint. Read the newest bucket (largest t); until the first poll lands liveRows is empty and the
11231123
// *Live values stay null, so the blocks show the loader values instead of flashing 0.
1124-
const { rows: liveRows } = useQueueMetric(
1124+
const { rows: liveRows, responseReceivedAt } = useQueueMetric(
11251125
`SELECT timeBucket() AS t, max(max_running) AS running, max(max_queued) AS queued, max(max_limit) AS q_limit, max(max_ck_wait_ms) AS ck_wait FROM queue_metrics GROUP BY t ORDER BY t`,
11261126
{
11271127
ids,
@@ -1138,7 +1138,9 @@ function QueueStats({
11381138
const latest = liveRows.length > 0 ? liveRows[liveRows.length - 1] : undefined;
11391139
const latestBucketMs = latest ? clickhouseTimeToMs(latest.t) : NaN;
11401140
const liveFresh =
1141-
Number.isFinite(latestBucketMs) && Date.now() - latestBucketMs < LIVE_GAUGE_FRESH_MS;
1141+
responseReceivedAt !== null &&
1142+
Number.isFinite(latestBucketMs) &&
1143+
responseReceivedAt - latestBucketMs < LIVE_GAUGE_FRESH_MS;
11421144
const fresh = latest && liveFresh ? latest : undefined;
11431145
const runningLive = fresh ? toNumber(fresh.running) : null;
11441146
const queuedLive = fresh ? toNumber(fresh.queued) : null;

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,12 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
176176
envParam,
177177
run: result.run,
178178
});
179-
return typedjson({ type: "run" as const, run: result.run, queueMetrics });
179+
return typedjson({
180+
type: "run" as const,
181+
run: result.run,
182+
queueMetrics,
183+
loadedAt: Date.now(),
184+
});
180185
}
181186
return typedjson({ type: "span" as const, span: result.span });
182187
} catch (error) {
@@ -270,6 +275,7 @@ export function SpanView({
270275
<RunBody
271276
run={fetcher.data.run}
272277
queueMetrics={fetcher.data.queueMetrics}
278+
loadedAt={fetcher.data.loadedAt}
273279
runParam={runParam}
274280
spanId={spanId}
275281
closePanel={closePanel}
@@ -395,12 +401,14 @@ function applySpanOverrides(span: Span, spanOverrides?: SpanOverride): Span {
395401
function RunBody({
396402
run,
397403
queueMetrics,
404+
loadedAt,
398405
runParam,
399406
spanId,
400407
closePanel,
401408
}: {
402409
run: SpanRun;
403410
queueMetrics: RunQueueMetrics | null;
411+
loadedAt: number;
404412
runParam: string;
405413
spanId: string;
406414
closePanel?: () => void;
@@ -1148,6 +1156,7 @@ function RunBody({
11481156
waiting={queueMetrics.waiting}
11491157
status={run.status}
11501158
createdAt={run.createdAt}
1159+
loadedAt={loadedAt}
11511160
runFriendlyId={run.friendlyId}
11521161
/>
11531162
) : null}
@@ -1283,6 +1292,7 @@ function WaitingInQueueBlock({
12831292
waiting,
12841293
status,
12851294
createdAt,
1295+
loadedAt,
12861296
runFriendlyId,
12871297
}: {
12881298
queueName: string;
@@ -1291,11 +1301,12 @@ function WaitingInQueueBlock({
12911301
waiting: RunQueueWaiting;
12921302
status: SpanRun["status"];
12931303
createdAt: Date;
1304+
loadedAt: number;
12941305
runFriendlyId: string;
12951306
}) {
12961307
// Latest gauges from ClickHouse (as on the queue page), polled so the blocks keep ticking. Trust
12971308
// the newest bucket only while fresh; otherwise fall back to the loader's live values.
1298-
const { rows: liveRows } = useQueueMetric(
1309+
const { rows: liveRows, responseReceivedAt } = useQueueMetric(
12991310
`SELECT timeBucket() AS t, max(max_running) AS running, max(max_queued) AS queued, max(max_limit) AS q_limit\nFROM queue_metrics\nGROUP BY t\nORDER BY t`,
13001311
{
13011312
ids: waiting.ids,
@@ -1307,8 +1318,9 @@ function WaitingInQueueBlock({
13071318
);
13081319
const latest = liveRows.length > 0 ? liveRows[liveRows.length - 1] : undefined;
13091320
const latestBucketMs = latest ? clickhouseTimeToMs(latest.t) : NaN;
1321+
const now = responseReceivedAt ?? loadedAt;
13101322
const fresh =
1311-
latest && Number.isFinite(latestBucketMs) && Date.now() - latestBucketMs < LIVE_GAUGE_FRESH_MS
1323+
latest && Number.isFinite(latestBucketMs) && now - latestBucketMs < LIVE_GAUGE_FRESH_MS
13121324
? latest
13131325
: undefined;
13141326

@@ -1325,7 +1337,7 @@ function WaitingInQueueBlock({
13251337
const showAtLimit = status === "PENDING" && atLimit && !paused;
13261338
const pct =
13271339
limit && limit > 0 ? Math.min(100, Math.round((runningAgainstLimit / limit) * 100)) : null;
1328-
const waitedMs = Math.max(0, Date.now() - new Date(createdAt).getTime());
1340+
const waitedMs = Math.max(0, now - new Date(createdAt).getTime());
13291341

13301342
// Why the run is held, surfaced as a warning icon on the Status tile (queue-page style) rather
13311343
// than a separate sentence.

0 commit comments

Comments
 (0)