Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
365bf20
feat(webapp,run-engine,core,clickhouse): surface total concurrency in…
matt-aitken Aug 29, 2026
cc6ed7f
fix(run-engine,clickhouse,webapp): total gauges on enqueue paths; res…
matt-aitken Aug 29, 2026
47314a9
fix(clickhouse): keep migration comments semicolon-free
matt-aitken Aug 29, 2026
4ad7e5c
fix(webapp): skip the total concurrency read when the queue has no cap
matt-aitken Aug 29, 2026
d90a9cf
feat(webapp): show the Total column in the non-metrics queues table too
matt-aitken Aug 29, 2026
c4866bb
feat(webapp): fold the total cap into the Limit column
matt-aitken Aug 29, 2026
518f3d6
fix(webapp): saturate the total-cap warning on keyed runs only
matt-aitken Aug 29, 2026
badaadb
refactor(webapp,core,clickhouse): combined concurrency in responses, …
matt-aitken Aug 29, 2026
8cbe657
feat(webapp): bracketed combined limit in the Limit column
matt-aitken Aug 29, 2026
b4adcd5
fix(webapp): combined-limit tooltip renders beside the cell link
matt-aitken Aug 29, 2026
3e41a5f
Better tooltip message
matt-aitken Aug 31, 2026
1edebce
docs(core): combined.current is the declared cap, clamped at admit time
matt-aitken Aug 31, 2026
f38b296
fix(run-engine): sample the combined gauge after batch admission
matt-aitken Aug 31, 2026
b2bc593
refactor(run-engine,webapp): drop per-key override admit reads and li…
matt-aitken Aug 31, 2026
925d139
refactor(webapp): concurrency keys resource stops reading per-key ove…
matt-aitken Aug 31, 2026
eeeca71
refactor(run-engine): drop the now-unreferenced ck-limits key builders
matt-aitken Aug 31, 2026
f391197
chore: lift the run-queue knip ignore
matt-aitken Aug 31, 2026
d9ad085
fix(run-engine,webapp,clickhouse): review fixes for the metrics tier
matt-aitken Aug 31, 2026
35972f5
chore: drop the pre-rename changeset superseded by the combined one
matt-aitken Aug 31, 2026
9933847
perf(run-engine): share the combined-limit read between the admit gat…
matt-aitken Aug 31, 2026
e2153e2
perf(run-engine): dequeue gauges sample once, at return
matt-aitken Aug 31, 2026
1beeb54
test(run-engine): pin dequeue-emitted gauges so a sampling regression…
matt-aitken Aug 31, 2026
10941ac
test(run-engine): wait for the metrics emitter connection before exer…
matt-aitken Aug 31, 2026
1519340
test(run-engine,metrics-pipeline): bound emitter-readiness waits and …
matt-aitken Aug 31, 2026
306ea8a
test(run-engine): abort the readiness race timer so its losing branch…
matt-aitken Aug 31, 2026
5d26c21
test(run-engine): close the emitter when the readiness wait times out
matt-aitken Aug 31, 2026
5d5ac60
test(metrics-pipeline,run-engine): readiness wait for the per-stream …
matt-aitken Aug 31, 2026
d9458db
test(run-engine): fire-and-forget the emitter close on readiness timeout
matt-aitken Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions apps/webapp/app/presenters/v3/QueueListPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import { engine } from "~/v3/runEngine.server";
import { BasePresenter } from "./basePresenter.server";
import { toQueueItem } from "./QueueRetrievePresenter.server";

type QueueListEngine = Pick<RunEngine, "lengthOfQueues" | "currentConcurrencyOfQueues">;
type QueueListEngine = Pick<
RunEngine,
"lengthOfQueues" | "currentConcurrencyOfQueues" | "totalConcurrencyOfQueues"
>;

export const QUEUE_LIST_DEFAULT_ITEMS_PER_PAGE = 25;
const MAX_ITEMS_PER_PAGE = 100;
Expand All @@ -34,6 +37,9 @@ const queueListSelect = {
concurrencyLimitOverriddenAt: true,
concurrencyLimitOverriddenBy: true,
concurrencyLimitOverridePercent: true,
totalConcurrencyLimit: true,
totalConcurrencyLimitBase: true,
totalConcurrencyLimitOverriddenAt: true,
type: true,
paused: true,
} satisfies Prisma.TaskQueueSelect;
Expand Down Expand Up @@ -333,11 +339,15 @@ export class QueueListPresenter extends BasePresenter {
concurrencyLimitOverriddenAt: Date | null;
concurrencyLimitOverriddenBy: string | null;
concurrencyLimitOverridePercent: Prisma.Decimal | null;
totalConcurrencyLimit: number | null;
totalConcurrencyLimitBase: number | null;
totalConcurrencyLimitOverriddenAt: Date | null;
type: TaskQueueType;
paused: boolean;
}[]
): Promise<QueueListItem[]> {
const [queuedByQueue, runningByQueue] = await Promise.all([
const queuesWithTotalCap = queues.filter((q) => q.totalConcurrencyLimit !== null);
const [queuedByQueue, runningByQueue, totalRunningByQueue] = await Promise.all([
this.engineClient.lengthOfQueues(
environment,
queues.map((q) => q.name)
Expand All @@ -346,6 +356,12 @@ export class QueueListPresenter extends BasePresenter {
environment,
queues.map((q) => q.name)
),
queuesWithTotalCap.length > 0
? this.engineClient.totalConcurrencyOfQueues(
environment,
queuesWithTotalCap.map((q) => q.name)
)
: Promise.resolve({} as Record<string, number>),
]);

// Manually "join" the overridden users because there is no way to implement the relationship
Expand Down Expand Up @@ -373,6 +389,11 @@ export class QueueListPresenter extends BasePresenter {
? (overriddenByMap.get(queue.concurrencyLimitOverriddenBy) ?? null)
: null,
paused: queue.paused,
totalConcurrencyLimit: queue.totalConcurrencyLimit,
totalConcurrencyLimitBase: queue.totalConcurrencyLimitBase,
totalConcurrencyLimitOverriddenAt: queue.totalConcurrencyLimitOverriddenAt,
totalRunning:
queue.totalConcurrencyLimit !== null ? (totalRunningByQueue[queue.name] ?? 0) : null,
}),
// Prisma returns Decimal; the client only needs a plain number (null for absolute overrides).
concurrencyLimitOverridePercent:
Expand Down
22 changes: 22 additions & 0 deletions apps/webapp/app/presenters/v3/QueueRetrievePresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ export class QueueRetrievePresenter extends BasePresenter {
const results = await Promise.all([
engine.lengthOfQueues(environment, [queue.name]),
engine.currentConcurrencyOfQueues(environment, [queue.name]),
queue.totalConcurrencyLimit != null
? engine.totalConcurrencyOfQueues(environment, [queue.name])
: undefined,
]);

// Transform queues to include running and queued counts
Expand All @@ -107,6 +110,11 @@ export class QueueRetrievePresenter extends BasePresenter {
concurrencyLimitOverriddenAt: queue.concurrencyLimitOverriddenAt ?? null,
concurrencyLimitOverriddenBy: queue.concurrencyLimitOverriddenBy ?? null,
paused: queue.paused,
totalConcurrencyLimit: queue.totalConcurrencyLimit ?? null,
totalConcurrencyLimitBase: queue.totalConcurrencyLimitBase ?? null,
totalConcurrencyLimitOverriddenAt: queue.totalConcurrencyLimitOverriddenAt ?? null,
totalRunning:
queue.totalConcurrencyLimit != null ? (results[2]?.[queue.name] ?? 0) : null,
}),
// The percent source-of-truth for percent-based overrides isn't part of the shared
// `QueueItem` schema (that's a public contract), so we surface it as an extra field on
Expand Down Expand Up @@ -148,6 +156,10 @@ export function toQueueItem(data: {
concurrencyLimitOverriddenAt: Date | null;
concurrencyLimitOverriddenBy: User | null;
paused: boolean;
totalConcurrencyLimit?: number | null;
totalConcurrencyLimitBase?: number | null;
totalConcurrencyLimitOverriddenAt?: Date | null;
totalRunning?: number | null;
}): QueueItem & { releaseConcurrencyOnWaitpoint: boolean } {
return {
id: data.friendlyId,
Expand All @@ -164,6 +176,16 @@ export function toQueueItem(data: {
override: data.concurrencyLimitOverriddenAt ? data.concurrencyLimit : null,
overriddenBy: toQueueConcurrencyOverriddenBy(data.concurrencyLimitOverriddenBy),
overriddenAt: data.concurrencyLimitOverriddenAt,
combined:
data.totalConcurrencyLimit !== undefined
? {
current: data.totalConcurrencyLimit,
base: data.totalConcurrencyLimitBase ?? null,
override: data.totalConcurrencyLimitOverriddenAt ? data.totalConcurrencyLimit : null,
overriddenAt: data.totalConcurrencyLimitOverriddenAt ?? null,
running: data.totalRunning ?? null,
}
: undefined,
},
// TODO: This needs to be removed but keeping this here for now to avoid breaking existing clients
releaseConcurrencyOnWaitpoint: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ import {
import { queueMetricsMaxPeriodDays } from "~/components/queues/queueMetricsPeriod.server";
import { isQueueAtCapacity } from "~/components/queues/queue-thresholds";
import { pageMeta } from "~/utils/pageTitle";
import { InlineCode } from "~/components/code/InlineCode";

const SearchParamsSchema = z.object({
query: z.string().optional(),
Expand Down Expand Up @@ -218,8 +219,13 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const timeRange = clipQueueMetricsWindow(
timeFilterFromTo({
period:
resolveQueueMetricsPeriod({ period, from, to, defaultPeriod, maxPeriodDays }) ??
undefined,
resolveQueueMetricsPeriod({
period,
from,
to,
defaultPeriod,
maxPeriodDays,
}) ?? undefined,
from: parseFiniteInt(from),
to: parseFiniteInt(to),
defaultPeriod,
Expand All @@ -243,7 +249,9 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
};
}
} catch (error) {
logger.warn("Queue list metrics unavailable, rendering without them", { error });
logger.warn("Queue list metrics unavailable, rendering without them", {
error,
});
}
}

Expand Down Expand Up @@ -706,7 +714,14 @@ function QueuesWithMetricsView() {
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell alignment="right">Queued</TableHeaderCell>
<TableHeaderCell alignment="right">Running</TableHeaderCell>
<TableHeaderCell alignment="right">Limit</TableHeaderCell>
<TableHeaderCell
alignment="right"
disableTooltipHoverableContent
tooltip={limitTooltip}
tooltipContentClassName="max-w-xs"
>
Limit
</TableHeaderCell>
<TableHeaderCell
alignment="right"
tooltipContentClassName="max-w-max"
Expand Down Expand Up @@ -849,7 +864,14 @@ function QueuesWithMetricsView() {
className={cn(
"w-[1%]",
queue.paused ? "opacity-50" : undefined,
queue.running > 0 && "text-text-bright"
queue.concurrency?.combined?.current != null &&
(queue.concurrency.combined.running ?? 0) >=
Math.min(
queue.concurrency.combined.current,
environment.concurrencyLimit
)
? "text-warning"
: queue.running > 0 && "text-text-bright"
)}
>
{queue.running}
Expand All @@ -863,12 +885,46 @@ function QueuesWithMetricsView() {
queue.paused ? "opacity-50" : undefined,
queue.concurrency?.overriddenAt && "font-medium text-text-bright"
)}
// The combined-limit hint is a tooltip button, so it renders beside the
// link (trailing) rather than nested inside the <a>; the number stays the
// link.
trailingContent={
queue.concurrency?.combined?.current != null ? (
<SimpleTooltip
disableHoverableContent
buttonClassName="-ml-1 cursor-default"
button={
<span className="text-text-dimmed bg-repeat-x pb-[3px] [background-image:linear-gradient(to_right,currentColor_2px,transparent_2px)] [background-position:bottom] [background-size:4px_1px] group-hover/table-row:text-text-bright">
(
{Math.min(
queue.concurrency.combined.current,
environment.concurrencyLimit
)}
)
</span>
}
content={
<>
Combined limit: at most{" "}
{Math.min(
queue.concurrency.combined.current,
environment.concurrencyLimit
)}{" "}
runs across all concurrency keys of this queue. The main limit
applies to each key separately.
</>
}
className="max-w-[260px]"
/>
) : undefined
}
>
{queue.concurrencyLimitOverridePercent !== null ? (
<>
{limit}
<span className="ml-1 text-text-dimmed group-hover/table-row:text-text-bright">
({formatOverridePercent(queue.concurrencyLimitOverridePercent)}%)
({formatOverridePercent(queue.concurrencyLimitOverridePercent)}
%)
</span>
</>
) : (
Expand Down Expand Up @@ -1265,7 +1321,11 @@ const QUEUE_HEADER_TILES: QueueHeaderTile[] = [
value: limit > 0 ? Math.round((tileNumber(r.running) / limit) * 100) : 0,
};
});
return { points, total: peakOf(points), formatTotal: (v) => `${v}% peak` };
return {
points,
total: peakOf(points),
formatTotal: (v) => `${v}% peak`,
};
},
},
{
Expand All @@ -1279,7 +1339,11 @@ const QUEUE_HEADER_TILES: QueueHeaderTile[] = [
bucket: tileTimeToMs(r.t),
value: tileNumber(r.queued),
}));
return { points, total: peakOf(points), formatTotal: (v) => `${v.toLocaleString()} peak` };
return {
points,
total: peakOf(points),
formatTotal: (v) => `${v.toLocaleString()} peak`,
};
},
},
{
Expand Down Expand Up @@ -1784,7 +1848,13 @@ function ClassicQueuesView() {
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell alignment="right">Queued</TableHeaderCell>
<TableHeaderCell alignment="right">Running</TableHeaderCell>
<TableHeaderCell alignment="right">Limit</TableHeaderCell>
<TableHeaderCell
alignment="right"
tooltip={limitTooltip}
tooltipContentClassName="max-w-xs"
>
Limit
</TableHeaderCell>
<TableHeaderCell
alignment="right"
tooltip={
Expand Down Expand Up @@ -1891,7 +1961,14 @@ function ClassicQueuesView() {
className={cn(
"w-[1%] pl-16 tabular-nums",
queue.paused ? "opacity-50" : undefined,
queue.running > 0 && "text-text-bright",
queue.concurrency?.combined?.current != null &&
(queue.concurrency.combined.running ?? 0) >=
Math.min(
queue.concurrency.combined.current,
environment.concurrencyLimit
)
? "text-warning"
: queue.running > 0 && "text-text-bright",
isAtConcurrencyLimit && "text-warning"
)}
>
Expand All @@ -1906,6 +1983,34 @@ function ClassicQueuesView() {
)}
>
{limit}
{queue.concurrency?.combined?.current != null ? (
<SimpleTooltip
disableHoverableContent
buttonClassName="ml-1 cursor-default"
button={
<span className="text-text-dimmed bg-repeat-x pb-[3px] [background-image:linear-gradient(to_right,currentColor_2px,transparent_2px)] [background-position:bottom] [background-size:4px_1px]">
(
{Math.min(
queue.concurrency.combined.current,
environment.concurrencyLimit
)}
)
</span>
}
content={
<>
Combined limit: at most{" "}
{Math.min(
queue.concurrency.combined.current,
environment.concurrencyLimit
)}{" "}
runs across all concurrency keys of this queue. The main limit
applies to each key separately.
</>
}
className="max-w-[260px]"
/>
) : null}
</TableCell>
<TableCell
alignment="right"
Expand Down Expand Up @@ -2024,3 +2129,16 @@ function BurstFactorTooltip({
/>
);
}

const limitTooltip = (
<>
<Paragraph variant="extra-small" spacing>
How many runs can execute at once.{" "}
</Paragraph>
<Paragraph variant="extra-small" spacing>
<InlineCode variant="extra-extra-small">1 (20)</InlineCode> means 1 run per concurrency key,
but at most 20 runs across all keys. Set using{" "}
<InlineCode variant="extra-extra-small">combinedConcurrencyLimit</InlineCode> in your code.
</Paragraph>
</>
);
Loading