Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
b1a6f1a
feat(webapp,run-engine,core,clickhouse): surface total concurrency in…
matt-aitken Aug 29, 2026
c51afe2
fix(run-engine,clickhouse,webapp): total gauges on enqueue paths; res…
matt-aitken Aug 29, 2026
60c3bcc
fix(clickhouse): keep migration comments semicolon-free
matt-aitken Aug 29, 2026
3e7c719
fix(webapp): skip the total concurrency read when the queue has no cap
matt-aitken Aug 29, 2026
1e10c98
feat(webapp): show the Total column in the non-metrics queues table too
matt-aitken Aug 29, 2026
3d6413f
feat(webapp): fold the total cap into the Limit column
matt-aitken Aug 29, 2026
d5b5866
fix(webapp): saturate the total-cap warning on keyed runs only
matt-aitken Aug 29, 2026
a46c6e0
refactor(webapp,core,clickhouse): combined concurrency in responses, …
matt-aitken Aug 29, 2026
451d4c9
feat(webapp): bracketed combined limit in the Limit column
matt-aitken Aug 29, 2026
bee8ae5
fix(webapp): combined-limit tooltip renders beside the cell link
matt-aitken Aug 29, 2026
9d1a386
Better tooltip message
matt-aitken Aug 31, 2026
8375efb
docs(core): combined.current is the declared cap, clamped at admit time
matt-aitken Aug 31, 2026
a188bcb
fix(run-engine): sample the combined gauge after batch admission
matt-aitken Aug 31, 2026
fdd0af1
refactor(run-engine,webapp): drop per-key override admit reads and li…
matt-aitken Aug 31, 2026
d9f6412
refactor(webapp): concurrency keys resource stops reading per-key ove…
matt-aitken Aug 31, 2026
36e873b
refactor(run-engine): drop the now-unreferenced ck-limits key builders
matt-aitken Aug 31, 2026
aca9667
chore: lift the run-queue knip ignore
matt-aitken Aug 31, 2026
9671fb4
fix(run-engine,webapp,clickhouse): review fixes for the metrics tier
matt-aitken Aug 31, 2026
2bc50f7
chore: drop the pre-rename changeset superseded by the combined one
matt-aitken Aug 31, 2026
2d15c4f
perf(run-engine): share the combined-limit read between the admit gat…
matt-aitken Aug 31, 2026
5d8db93
perf(run-engine): dequeue gauges sample once, at return
matt-aitken Aug 31, 2026
9553fff
test(run-engine): pin dequeue-emitted gauges so a sampling regression…
matt-aitken Aug 31, 2026
600610d
test(run-engine): wait for the metrics emitter connection before exer…
matt-aitken Aug 31, 2026
6000cca
test(run-engine,metrics-pipeline): bound emitter-readiness waits and …
matt-aitken Aug 31, 2026
cc1358e
test(run-engine): abort the readiness race timer so its losing branch…
matt-aitken Aug 31, 2026
d0e5cfa
test(run-engine): close the emitter when the readiness wait times out
matt-aitken Aug 31, 2026
6212f6e
test(metrics-pipeline,run-engine): readiness wait for the per-stream …
matt-aitken Aug 31, 2026
8198c11
test(run-engine): fire-and-forget the emitter close on readiness timeout
matt-aitken Aug 31, 2026
bb24193
rename(webapp,clickhouse): concurrency vocabulary for the unlaunched …
matt-aitken Sep 6, 2026
ca58abd
fix(webapp,clickhouse,run-engine): rename follow-ups from review
matt-aitken Sep 6, 2026
604582e
fix(webapp,clickhouse): pre-cap history keeps its truthful gap in the…
matt-aitken Sep 6, 2026
09f361c
fix(webapp): carry-guard column reaches the chart points, Total namin…
matt-aitken Sep 6, 2026
903af5a
feat(webapp): rename the Concurrency page to Concurrency limits
matt-aitken Sep 6, 2026
2cfd2ff
fix(webapp): deeplinks, favorites and report links follow the concurr…
matt-aitken Sep 6, 2026
8840228
chore(webapp): remove the combined concurrency override and reset end…
matt-aitken Sep 6, 2026
ed25b3d
fix(webapp): agent page labels follow the concurrency-limits rename
matt-aitken Sep 6, 2026
9c7c130
fix(run-engine): base-queue gauges reuse the shared total-limit read
matt-aitken Sep 7, 2026
8182703
fix(run-engine): base-queue admit paths enforce the total limit for k…
matt-aitken Sep 7, 2026
e92efa4
fix(run-engine): the total concurrency limit spans keyed and keyless …
matt-aitken Sep 7, 2026
86e77a0
fix(run-engine): keyless releases drain the total-concurrency group set
matt-aitken Sep 7, 2026
803145a
fix(run-engine): the repair clear drains the group set for keyless runs
matt-aitken Sep 7, 2026
c4b933f
fix(run-engine): the keyless repair clear derives the group key from …
matt-aitken Sep 7, 2026
403cc6d
fix(run-engine): the TTL sweep's defensive removal drains the group s…
matt-aitken Sep 7, 2026
49183d0
feat(run-engine): exact per-gate queued counters
matt-aitken Sep 7, 2026
d97ba34
fix(run-engine): gate queued counters dedupe by base and re-anchor on…
matt-aitken Sep 7, 2026
5b66634
fix(webapp): limit rows mirror the declared shape exactly
matt-aitken Sep 7, 2026
1cadf89
feat(run-engine): expose the per-gate queued counts on the engine
matt-aitken Sep 7, 2026
3347b63
feat(webapp): concurrency limits management API
matt-aitken Sep 7, 2026
ebd6a63
chore(webapp): keep concurrency limit mapping helpers module-local
matt-aitken Sep 7, 2026
c1af5bc
fix(webapp,run-engine): recoverable resets, conflict-guarded limit mu…
matt-aitken Sep 7, 2026
b759342
fix(webapp): limit mutations answer concurrent changes with a 409
matt-aitken Sep 7, 2026
dd2aeff
test(webapp): focused coverage for concurrency limit mutations
matt-aitken Sep 7, 2026
d9e9fbb
fix(webapp,run-engine): limit mutations survive every race direction
matt-aitken Sep 7, 2026
18ffedf
fix(webapp): reset compensation converges instead of restoring a stal…
matt-aitken Sep 7, 2026
804186a
fix(webapp): reset failures keep their original error and every path …
matt-aitken Sep 7, 2026
1220066
fix(webapp): engine writes settle before any failure is reported
matt-aitken Sep 7, 2026
8e2f27d
fix(webapp): no compensation on the not-overridden validation error
matt-aitken Sep 7, 2026
41ff31d
feat(webapp): named concurrency limits appear on the queues page
matt-aitken Sep 7, 2026
98c8122
feat(webapp): the Queues page becomes Concurrency
matt-aitken Sep 7, 2026
cdff91e
fix(webapp): limit rows interleave only on the dashboard list
matt-aitken Sep 7, 2026
c9ba184
fix(webapp): format the renamed task routes, trigger URIs resolve to …
matt-aitken Sep 7, 2026
0b06843
fix(webapp): limit rows on the Concurrency page stay list-level, over…
matt-aitken Sep 7, 2026
4f960a1
fix(webapp): limit rows only warn at their total bound, paused banner…
matt-aitken Sep 7, 2026
eab3dd1
fix(webapp): limit convergence compares enforced values, zero thresho…
matt-aitken Sep 7, 2026
ca7ec2a
docs(webapp): the value fixpoint comment states the real soundness ar…
matt-aitken Sep 7, 2026
177b8ff
docs(webapp): the value fixpoint comment names both residuals precisely
matt-aitken Sep 7, 2026
f493d0a
feat(core,webapp): version-discriminated queue reads, combined never …
matt-aitken Sep 7, 2026
98e1a45
fix(core): queue reads from servers predating the version field parse…
matt-aitken Sep 7, 2026
623b861
chore(webapp): the queue limit bound types stay module-local
matt-aitken Sep 7, 2026
63b0d9f
fix(core,webapp): default-queue inline limits are manageable, V1 endp…
matt-aitken Sep 7, 2026
16862f6
fix(webapp): the limits surface preserves pauses, percent sources, an…
matt-aitken Sep 7, 2026
3745a8a
chore(webapp): the retire queries use bounded in-filters
matt-aitken Sep 7, 2026
910ccb2
fix(webapp): boundless named limits stay cappable, retirement is race…
matt-aitken Sep 7, 2026
cbdbde2
fix(webapp): retirement removes engine keys before nulling bounds, re…
matt-aitken Sep 7, 2026
3af597c
fix(webapp): a retirement losing its guard restores the winner's engi…
matt-aitken Sep 7, 2026
59983e4
fix(webapp): retirement failures heal the engine and are never silent
matt-aitken Sep 8, 2026
4ef1a76
fix(webapp): retirement removals fully settle before healing from a f…
matt-aitken Sep 8, 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
2 changes: 2 additions & 0 deletions .changeset/task-concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ export const generateSummary = task({
`perKey` caps each `concurrencyKey` pool and `total` caps across everything, keys or not. The queue-level `concurrencyLimit` option keeps working unchanged and is deprecated in favor of `concurrency`. Enforcement happens server-side; servers without support accept the option but do not enforce it yet.

Manage limits at runtime with the new `concurrencyLimits` namespace: `list()` and `retrieve(name)` report each limit's bounds plus its live `running` and `queued` counts, `override(name, { perKey, total })` changes only the given bounds (overriding `total` to `0` pauses the limit), and `reset(name)` restores the declared values.

Queue reads (`queues.list()` and `queues.retrieve()`) now report a `version` that discriminates the shape: `V1` queues keep today's fields (their own `concurrencyLimit` and its override state), while `V2` queues (tasks declared with `concurrency`) carry no queue-level concurrency, since their limits are read and overridden through `concurrencyLimits` (a task's inline limit under its derived `task/<task-id>` name). Existing reads keep compiling: a `V2` queue reports `concurrencyLimit` as null and `concurrency` as undefined.
8 changes: 4 additions & 4 deletions apps/webapp/app/components/billing/OrgBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { useOptionalProject, useProject } from "~/hooks/useProject";
import { useShowSelfServe } from "~/hooks/useShowSelfServe";
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
import { v3BillingLimitsPath, v3BillingPath, v3QueuesPath } from "~/utils/pathBuilder";
import { v3BillingLimitsPath, v3BillingPath, concurrencyPath } from "~/utils/pathBuilder";
import { ENVIRONMENT_PAUSE_SOURCE_BILLING_LIMIT } from "~/utils/environmentPauseSource";

function getUpgradeResetDate(): Date {
Expand Down Expand Up @@ -54,7 +54,7 @@ export function OrgBanner() {
showSelfServe,
});

const hideQueuesButton = location.pathname.endsWith("/queues");
const hideConcurrencyButton = location.pathname.endsWith("/concurrency");
const hideBillingLimitBanner = location.pathname.endsWith("/settings/billing-limits");

switch (bannerKind) {
Expand All @@ -70,7 +70,7 @@ export function OrgBanner() {
return isArchived ? (
<ArchivedEnvironmentBanner />
) : (
<PausedEnvironmentBanner hideButton={hideQueuesButton} />
<PausedEnvironmentBanner hideButton={hideConcurrencyButton} />
);
default:
return null;
Expand Down Expand Up @@ -209,7 +209,7 @@ function PausedEnvironmentBanner({ hideButton }: { hideButton: boolean }) {
hideButton ? undefined : (
<LinkButton
variant="tertiary/small"
to={v3QueuesPath(organization, project, environment)}
to={concurrencyPath(organization, project, environment)}
>
Manage
</LinkButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import {
} from "./unread-counts";
import { AgentPanelColumn } from "./panel-layout";
import { markerAfterActiveChat, markerAfterActivity } from "./thinking-marker";
import { concurrencyPath } from "~/utils/pathBuilder";
import { concurrencyLimitsPath } from "~/utils/pathBuilder";

function serializePageContext(pageContext: AgentPageContext): string | undefined {
try {
Expand Down Expand Up @@ -131,7 +131,7 @@ export function DashboardAgentPanel({
const currentPage = agentPageLabel(pageContext, location.pathname);

const pagePaths = useMemo<Record<string, string>>(
() => ({ raise_env_limit: concurrencyPath(organization, project, environment) }),
() => ({ raise_env_limit: concurrencyLimitsPath(organization, project, environment) }),
[organization, project, environment]
);

Expand Down
3 changes: 2 additions & 1 deletion apps/webapp/app/components/dashboard-agent/page-label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const KIND_LABELS: Record<Exclude<AgentPage["kind"], "other">, string> = {
alerts: "Alerts",
apikeys: "API keys",
envvars: "Environment variables",
concurrency: "Concurrency",
concurrency: "Concurrency limits",
regions: "Regions",
settings: "Settings",
waitpoints: "Waitpoints",
Expand All @@ -50,6 +50,7 @@ const SECTION_LABELS: Record<string, string> = {
"bulk-actions": "Bulk actions",
branches: "Branches",
concurrency: "Concurrency",
"concurrency-limits": "Concurrency limits",
dashboards: "Dashboards",
deployments: "Deployments",
"dev-branches": "Branches",
Expand Down
5 changes: 3 additions & 2 deletions apps/webapp/app/components/navigation/favoritePages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,8 @@ const ENV_PAGE_META: Record<string, PageMeta> = {
logs: { icon: "logs", name: "Logs" },
errors: { icon: "errors", name: "Errors", singular: "Error" },
query: { icon: "query", name: "Query" },
queues: { icon: "queues", name: "Queues", singular: "Queue" },
queues: { icon: "queues", name: "Concurrency" },
concurrency: { icon: "queues", name: "Concurrency" },
dashboards: { icon: "dashboards", name: "Dashboards", singular: "Dashboard" },
deployments: { icon: "deployments", name: "Deploys", singular: "Deploy" },
"environment-variables": { icon: "environment-variables", name: "Environment variables" },
Expand All @@ -234,7 +235,7 @@ const ENV_PAGE_META: Record<string, PageMeta> = {
"bulk-actions": { icon: "bulk-actions", name: "Bulk actions", singular: "Bulk action" },
apikeys: { icon: "apikeys", name: "API keys" },
alerts: { icon: "alerts", name: "Alerts", singular: "Alert" },
concurrency: { icon: "concurrency", name: "Concurrency" },
"concurrency-limits": { icon: "concurrency", name: "Concurrency limits" },
limits: { icon: "limits", name: "Limits" },
schedules: { icon: "schedules", name: "Schedules", singular: "Schedule" },
test: { icon: "test", name: "Test", singular: "Test" },
Expand Down
12 changes: 6 additions & 6 deletions apps/webapp/app/components/navigation/sideMenuSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
type OrgForPath,
type ProjectForPath,
branchesPath,
concurrencyPath,
concurrencyLimitsPath,
limitsPath,
queryPath,
regionsPath,
Expand All @@ -39,7 +39,7 @@ import {
v3ProjectAlertsPath,
v3ProjectSettingsIntegrationsPath,
v3PromptsPath,
v3QueuesPath,
concurrencyPath,
v3WaitpointTokensPath,
} from "~/utils/pathBuilder";
import { AlphaBadge, NewBadge } from "../FeatureBadges";
Expand Down Expand Up @@ -160,10 +160,10 @@ export function buildSideMenuSections({
} satisfies SideMenuItemConfig,
{
id: "queues",
name: "Queues",
name: "Concurrency",
icon: QueuesIcon,
activeIconColor: "text-queues",
to: v3QueuesPath(organization, project, environment),
to: concurrencyPath(organization, project, environment),
dataAction: "queues",
} satisfies SideMenuItemConfig,
{
Expand Down Expand Up @@ -269,10 +269,10 @@ export function buildSideMenuSections({
? [
{
id: "concurrency",
name: "Concurrency",
name: "Concurrency limits",
icon: ConcurrencyIcon,
activeIconColor: "text-text-bright",
to: concurrencyPath(organization, project, environment),
to: concurrencyLimitsPath(organization, project, environment),
dataAction: "concurrency",
} satisfies SideMenuItemConfig,
]
Expand Down
23 changes: 14 additions & 9 deletions apps/webapp/app/components/queues/QueueControls.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { AdjustmentsHorizontalIcon, PauseIcon, PlayIcon } from "@heroicons/react/20/solid";
import { DialogClose } from "@radix-ui/react-dialog";
import { Form, useNavigation } from "@remix-run/react";
import type { QueueItem } from "@trigger.dev/core/v3/schemas";
import { useEffect, useState } from "react";
import { cn } from "~/utils/cn";
import type { QueueLimits } from "~/components/queues/queue-limits";
import { Button, type ButtonVariant } from "~/components/primitives/Buttons";
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog";
import { FormButtons } from "~/components/primitives/FormButtons";
Expand Down Expand Up @@ -159,7 +159,12 @@ export function QueueOverrideConcurrencyButton({
environmentConcurrencyLimit,
trigger,
}: {
queue: QueueItem & { concurrencyLimitOverridePercent: number | null };
queue: {
id: string;
name: string;
limits: QueueLimits;
concurrencyLimitOverridePercent: number | null;
};
environmentConcurrencyLimit: number;
/** How to render the dialog trigger. "menu-item" (default) is a PopoverMenuItem for row menus;
* "button" is a standalone labeled button; "icon" is an icon-only button with the label in a
Expand All @@ -172,14 +177,14 @@ export function QueueOverrideConcurrencyButton({
queue.concurrencyLimitOverridePercent !== null ? "percent" : "absolute"
);
const [concurrencyLimit, setConcurrencyLimit] = useState<string>(
queue.concurrencyLimit?.toString() ?? environmentConcurrencyLimit.toString()
queue.limits.perKey.current?.toString() ?? environmentConcurrencyLimit.toString()
);
const [percent, setPercent] = useState<string>(
queue.concurrencyLimitOverridePercent?.toString() ?? "100"
);

const isOverridden = !!queue.concurrency?.overriddenAt;
const currentLimit = queue.concurrencyLimit ?? environmentConcurrencyLimit;
const isOverridden = !!queue.limits.perKey.overriddenAt;
const currentLimit = queue.limits.perKey.current ?? environmentConcurrencyLimit;

useEffect(() => {
if (navigation.state === "loading" || navigation.state === "idle") {
Expand Down Expand Up @@ -277,18 +282,18 @@ export function QueueOverrideConcurrencyButton({
{isOverridden ? (
<Paragraph variant="small">
This queue's concurrency limit is currently overridden to {currentLimit}.
{typeof queue.concurrency?.base === "number" &&
` The original limit set in code was ${queue.concurrency.base}.`}{" "}
{typeof queue.limits.perKey.base === "number" &&
` The original limit set in code was ${queue.limits.perKey.base}.`}{" "}
You can update the override or remove it to restore the{" "}
{typeof queue.concurrency?.base === "number"
{typeof queue.limits.perKey.base === "number"
? "limit set in code"
: "environment concurrency limit"}
.
</Paragraph>
) : (
<Paragraph variant="small">
Override this queue's concurrency limit. The current limit is {currentLimit}, which is
set {queue.concurrencyLimit !== null ? "in code" : "by the environment"}.
set {queue.limits.perKey.current !== null ? "in code" : "by the environment"}.
</Paragraph>
)}
<Form method="post" onSubmit={() => setIsOpen(false)} className="space-y-3">
Expand Down
17 changes: 14 additions & 3 deletions apps/webapp/app/components/queues/QueueMetricCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ type QueueMetricChartProps = {
* are config values that existed all along, so carry the first value backward instead.
*/
carryBackfill?: string[];
/**
* Column that marks a bucket as genuinely sampled. When set, carryBackfill only
* overwrites buckets where this column is absent or zero, so history from before
* a config value existed keeps its truthful gap instead of inheriting the value.
*/
carryBackfillGuard?: string;
/** Show the series legend below the chart (use for multi-series charts). */
showLegend?: boolean;
/**
Expand Down Expand Up @@ -141,6 +147,7 @@ export function QueueMetricChart({
defaultPeriod,
warningOverlay,
carryBackfill,
carryBackfillGuard,
thresholdStroke,
onHasDataChange,
minBucketSeconds,
Expand All @@ -163,6 +170,7 @@ export function QueueMetricChart({
};
const hasSamples = sampleCountColumn ? toNumber(r[sampleCountColumn]) > 0 : true;
for (const s of series) point[s.key] = hasSamples ? toNumber(r[s.key]) : null;
if (carryBackfillGuard) point[carryBackfillGuard] = toNumber(r[carryBackfillGuard]);
return point;
})
.filter((p) => Number.isFinite(p.bucket));
Expand All @@ -174,12 +182,15 @@ export function QueueMetricChart({
const first = points.findIndex((p) => toNumber(p[key]) > 0);
if (first > 0) {
const value = points[first]![key]!;
for (let i = 0; i < first; i++) points[i]![key] = value;
for (let i = 0; i < first; i++) {
if (carryBackfillGuard && toNumber(points[i]![carryBackfillGuard]) > 0) continue;
Comment thread
matt-aitken marked this conversation as resolved.
points[i]![key] = value;
Comment thread
matt-aitken marked this conversation as resolved.
}
}
}
}
return points;
}, [rows, series, carryBackfill, sampleCountColumn]);
}, [rows, series, carryBackfill, carryBackfillGuard, sampleCountColumn]);

const chartConfig = useMemo(() => {
const cfg: ChartConfig = {};
Expand Down Expand Up @@ -340,7 +351,7 @@ export function QueueSidebarStats({
};

const { rows, showLoading } = useQueueMetric(
`SELECT max(max_queued) AS peak_queued,\n round(quantilesMerge(0.5, 0.9, 0.95, 0.99)(wait_quantiles)[3]) AS worst_p95\nFROM queue_metrics`,
`SELECT max(max_queued) AS peak_queued,\n round(quantilesMerge(0.5, 0.9, 0.95, 0.99)(wait_quantiles)[3]) AS worst_p95\nFROM concurrency_metrics`,
{ ids, timeRange, queueName, defaultPeriod }
);
const row = rows[0];
Expand Down
32 changes: 32 additions & 0 deletions apps/webapp/app/components/queues/queue-limits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Dashboard-only view of a queue row's configured bounds. The public QueueItem
* shape is version-discriminated (V2 queues carry no queue-level concurrency),
* but the dashboard shows configured limits for every row, so presenters attach
* this alongside the public fields.
*/
type QueueLimitBound = {
/** The enforced value right now (declared, or the override when one is active) */
current: number | null;
/** The declared value an override reverts to */
base: number | null;
/** The overridden value, when an override is active */
override: number | null;
overriddenAt: Date | null;
/** Display name of who applied the override (null when via the API) */
overriddenBy: string | null;
};

type QueueTotalBound = {
current: number;
base: number | null;
override: number | null;
overriddenAt: Date | null;
/** Runs in flight across every pool of the row (keyed and keyless) */
running: number | null;
};

export type QueueLimits = {
perKey: QueueLimitBound;
/** Null when the row declares no total bound */
total: QueueTotalBound | null;
};
30 changes: 30 additions & 0 deletions apps/webapp/app/components/runs/v3/QueueName.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,49 @@
import { TasksIcon } from "~/assets/icons/TasksIcon";
import { ConcurrencyIcon } from "~/assets/icons/ConcurrencyIcon";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { cn } from "~/utils/cn";
import { RectangleStackIcon } from "@heroicons/react/20/solid";

const LIMIT_PREFIX = "limit/";
const TASK_PREFIX = "task/";

export function QueueName({
name,
type,
kind,
paused,
className,
}: {
name: string;
type: "task" | "custom";
/** "limit" rows are named concurrency limits rather than queues. */
kind?: "queue" | "limit";
paused?: boolean;
className?: string;
}) {
if (kind === "limit") {
const displayName = name.startsWith(LIMIT_PREFIX) ? name.slice(LIMIT_PREFIX.length) : name;
return (
<span className={cn("flex items-center gap-1", className)}>
<SimpleTooltip
button={
<ConcurrencyIcon
className={cn("size-[1.125rem] text-amber-500", paused && "opacity-50")}
/>
}
content={
displayName.startsWith(TASK_PREFIX)
? `This is the inline concurrency limit of your "${displayName.slice(
TASK_PREFIX.length
)}" task`
: "This is a named concurrency limit declared in your code."
}
/>
<span className={paused ? "opacity-50" : undefined}>{displayName}</span>
</span>
);
}

return (
<span className={cn("flex items-center gap-1", className)}>
{type === "task" ? (
Expand Down
10 changes: 5 additions & 5 deletions apps/webapp/app/presenters/v3/BuiltInDashboards.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,7 @@ const queuesDashboard: BuiltInDashboard = {
"t-pressure": { title: "Queue pressure", query: "", display: { type: "title" } },
pressure: {
title: "Queue pressure",
query: `SELECT queue,\n argMax(max_running, bucket_start) AS running,\n argMax(max_queued, bucket_start) AS queued,\n argMax(max_limit, bucket_start) AS limit,\n running + queued AS demand,\n max(max_queued) AS peak_queued,\n sum(throttled_count) AS throttled,\n multiIf(running >= limit AND queued > 0, 'queue-limited', queued > 0, 'backlogged', 'healthy') AS status\nFROM queue_metrics\nGROUP BY queue\nORDER BY peak_queued DESC`,
query: `SELECT queue,\n argMax(max_running, bucket_start) AS running,\n argMax(max_queued, bucket_start) AS queued,\n argMax(max_limit, bucket_start) AS limit,\n running + queued AS demand,\n max(max_queued) AS peak_queued,\n sum(throttled_count) AS throttled,\n multiIf(running >= limit AND queued > 0, 'queue-limited', queued > 0, 'backlogged', 'healthy') AS status\nFROM concurrency_metrics\nGROUP BY queue\nORDER BY peak_queued DESC`,
display: {
type: "table",
prettyFormatting: true,
Expand All @@ -644,7 +644,7 @@ const queuesDashboard: BuiltInDashboard = {
"t-trends": { title: "Per-queue trends", query: "", display: { type: "title" } },
"running-q": {
title: "Running by queue",
query: `SELECT timeBucket() AS t, queue, max(max_running) AS running\nFROM queue_metrics\nGROUP BY t, queue\nORDER BY t`,
query: `SELECT timeBucket() AS t, queue, max(max_running) AS running\nFROM concurrency_metrics\nGROUP BY t, queue\nORDER BY t`,
// Grouped gauge: carry each queue's running across idle buckets (per-group LOCF).
fillGaps: true,
display: {
Expand All @@ -661,7 +661,7 @@ const queuesDashboard: BuiltInDashboard = {
},
"queued-q": {
title: "Queue depth (backlog) by queue",
query: `SELECT timeBucket() AS t, queue, max(max_queued) AS queued\nFROM queue_metrics\nGROUP BY t, queue\nORDER BY t`,
query: `SELECT timeBucket() AS t, queue, max(max_queued) AS queued\nFROM concurrency_metrics\nGROUP BY t, queue\nORDER BY t`,
// Grouped gauge: carry each queue's backlog across idle buckets (per-group LOCF).
fillGaps: true,
display: {
Expand All @@ -678,7 +678,7 @@ const queuesDashboard: BuiltInDashboard = {
},
"throttled-q": {
title: "Throttled buckets by queue",
query: `SELECT timeBucket() AS t, queue, sum(throttled_count) AS throttled\nFROM queue_metrics\nGROUP BY t, queue\nORDER BY t`,
query: `SELECT timeBucket() AS t, queue, sum(throttled_count) AS throttled\nFROM concurrency_metrics\nGROUP BY t, queue\nORDER BY t`,
// Grouped counter: per-group zero-fill so idle buckets read 0, not a gap.
fillGaps: true,
display: {
Expand All @@ -697,7 +697,7 @@ const queuesDashboard: BuiltInDashboard = {
title: "Enqueued vs started",
// Counter states merge per queue, then sum outside: a single merge across queues
// mixes unrelated odometers and returns wrong totals.
query: `SELECT t, sum(enq) AS enqueued, sum(st) AS started\nFROM (\n SELECT timeBucket() AS t, queue,\n deltaSumTimestampMerge(enqueue_delta) AS enq,\n deltaSumTimestampMerge(started_delta) AS st\n FROM queue_metrics\n GROUP BY t, queue\n)\nGROUP BY t\nORDER BY t`,
query: `SELECT t, sum(enq) AS enqueued, sum(st) AS started\nFROM (\n SELECT timeBucket() AS t, queue,\n deltaSumTimestampMerge(enqueue_delta) AS enq,\n deltaSumTimestampMerge(started_delta) AS st\n FROM concurrency_metrics\n GROUP BY t, queue\n)\nGROUP BY t\nORDER BY t`,
display: {
type: "chart",
chartType: "line",
Expand Down
Loading