Skip to content

Commit fa94feb

Browse files
matt-aitkenTrigger.dev RepoOps
authored andcommitted
feat(webapp,sdk,core): first-class pause and resume for concurrency limits
Concurrency limits can now be paused and resumed from the dashboard, the API, and the SDK (`concurrencyLimits.pause()` / `concurrencyLimits.resume()`), just like queues. A paused limit stops every run holding it from being dequeued while keeping its configured bounds, and resuming restores them. Mono-RevId: 33cbe4dc731671f083779d05cedac03c3709a654
1 parent de21ec8 commit fa94feb

18 files changed

Lines changed: 1081 additions & 75 deletions

File tree

.changeset/chilly-plums-pause.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@trigger.dev/sdk": minor
3+
"@trigger.dev/core": minor
4+
---
5+
6+
Concurrency limits can now be paused and resumed, just like queues: `concurrencyLimits.pause(name)` stops every run holding the limit from being dequeued while keeping its configured bounds, and `concurrencyLimits.resume(name)` starts them again.
7+
8+
```ts
9+
import { concurrencyLimits } from "@trigger.dev/sdk";
10+
11+
await concurrencyLimits.pause("openai");
12+
await concurrencyLimits.resume("openai");
13+
```

apps/webapp/app/components/queues/QueueControls.tsx

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export function QueuePauseResumeButton({
3636
iconOnly = false,
3737
withQueueName = false,
3838
disabled = false,
39+
noun = "queue",
3940
}: {
4041
/** The "id" here is a friendlyId */
4142
queue: { id: string; name: string; paused: boolean };
@@ -48,14 +49,21 @@ export function QueuePauseResumeButton({
4849
/** Render the full "Pause/Resume {name} queue" label instead of the short "Pause"/"Resume". */
4950
withQueueName?: boolean;
5051
disabled?: boolean;
52+
/** What the row is called in every user-facing string: named concurrency limits pause through
53+
* the same actions but their dialogs must say "limit". */
54+
noun?: "queue" | "limit";
5155
}) {
5256
const [isOpen, setIsOpen] = useState(false);
5357

5458
const label = queue.paused
55-
? `Resumes the "${queue.name}" queue so its runs can be dequeued again.`
56-
: `Pauses all runs from being dequeued in the "${queue.name}" queue. Any executing runs will continue to run.`;
59+
? noun === "limit"
60+
? `Resumes the "${queue.name}" limit so runs holding it can be dequeued again.`
61+
: `Resumes the "${queue.name}" queue so its runs can be dequeued again.`
62+
: noun === "limit"
63+
? `Pauses all runs holding the "${queue.name}" limit from being dequeued. Any executing runs will continue to run.`
64+
: `Pauses all runs from being dequeued in the "${queue.name}" queue. Any executing runs will continue to run.`;
5765

58-
const tooltip = disabled ? "You don't have permission to manage queues" : label;
66+
const tooltip = disabled ? `You don't have permission to manage ${noun}s` : label;
5967

6068
const trigger = showTooltip ? (
6169
<div>
@@ -91,8 +99,8 @@ export function QueuePauseResumeButton({
9199
? undefined
92100
: withQueueName
93101
? queue.paused
94-
? "Resume this queue…"
95-
: "Pause this queue…"
102+
? `Resume this ${noun}…`
103+
: `Pause this ${noun}…`
96104
: queue.paused
97105
? "Resume"
98106
: "Pause"}
@@ -113,7 +121,7 @@ export function QueuePauseResumeButton({
113121
leadingIconClassName={queue.paused ? "text-success" : "text-warning"}
114122
title={
115123
disabled
116-
? "You don't have permission to manage queues"
124+
? `You don't have permission to manage ${noun}s`
117125
: queue.paused
118126
? "Resume..."
119127
: "Pause..."
@@ -127,12 +135,16 @@ export function QueuePauseResumeButton({
127135
<Dialog open={isOpen} onOpenChange={setIsOpen}>
128136
{trigger}
129137
<DialogContent>
130-
<DialogHeader>{queue.paused ? "Resume queue?" : "Pause queue?"}</DialogHeader>
138+
<DialogHeader>{queue.paused ? `Resume ${noun}?` : `Pause ${noun}?`}</DialogHeader>
131139
<div className="flex flex-col gap-3 pt-3">
132140
<Paragraph>
133141
{queue.paused
134-
? `This will allow runs to be dequeued in the "${queue.name}" queue again.`
135-
: `This will pause all runs from being dequeued in the "${queue.name}" queue. Any executing runs will continue to run.`}
142+
? noun === "limit"
143+
? `This will allow runs holding the "${queue.name}" limit to be dequeued again.`
144+
: `This will allow runs to be dequeued in the "${queue.name}" queue again.`
145+
: noun === "limit"
146+
? `This will pause all runs holding the "${queue.name}" limit from being dequeued. Any executing runs will continue to run.`
147+
: `This will pause all runs from being dequeued in the "${queue.name}" queue. Any executing runs will continue to run.`}
136148
</Paragraph>
137149
<Form method="post" onSubmit={() => setIsOpen(false)}>
138150
<input
@@ -141,6 +153,7 @@ export function QueuePauseResumeButton({
141153
value={queue.paused ? "queue-resume" : "queue-pause"}
142154
/>
143155
<input type="hidden" name="friendlyId" value={queue.id} />
156+
<input type="hidden" name="noun" value={noun} />
144157
<FormButtons
145158
confirmButton={
146159
<Button
@@ -149,7 +162,7 @@ export function QueuePauseResumeButton({
149162
variant={queue.paused ? "primary/medium" : "danger/medium"}
150163
LeadingIcon={queue.paused ? PlayIcon : PauseIcon}
151164
>
152-
{queue.paused ? "Resume queue" : "Pause queue"}
165+
{queue.paused ? `Resume ${noun}` : `Pause ${noun}`}
153166
</Button>
154167
}
155168
cancelButton={
@@ -172,6 +185,7 @@ export function QueueOverrideConcurrencyButton({
172185
environmentConcurrencyLimit,
173186
trigger,
174187
disabled = false,
188+
noun = "queue",
175189
}: {
176190
queue: {
177191
id: string;
@@ -185,6 +199,9 @@ export function QueueOverrideConcurrencyButton({
185199
* hover tooltip, for compact placements like the detail-page live blocks. */
186200
trigger?: "menu-item" | "button" | "icon";
187201
disabled?: boolean;
202+
/** What the row is called in every user-facing string: named concurrency limits are overridden
203+
* through the same actions but their dialogs must say "limit". */
204+
noun?: "queue" | "limit";
188205
}) {
189206
const navigation = useNavigation();
190207
const [isOpen, setIsOpen] = useState(false);
@@ -316,7 +333,7 @@ export function QueueOverrideConcurrencyButton({
316333
</div>
317334
</TooltipTrigger>
318335
<TooltipContent side="right" className="text-xs">
319-
{disabled ? "You don't have permission to manage queues" : iconLabel}
336+
{disabled ? `You don't have permission to manage ${noun}s` : iconLabel}
320337
</TooltipContent>
321338
</Tooltip>
322339
</TooltipProvider>
@@ -343,10 +360,10 @@ export function QueueOverrideConcurrencyButton({
343360
</TooltipTrigger>
344361
<TooltipContent side="bottom" className="max-w-[230px] text-xs">
345362
{disabled
346-
? "You don't have permission to manage queues"
363+
? `You don't have permission to manage ${noun}s`
347364
: hasTotal
348-
? "Override this queue's per-key and total concurrency limits."
349-
: "Give this queue its own concurrency limit instead of the environment default. Set it as a number or a percentage of the environment limit."}
365+
? `Override this ${noun}'s per-key and total concurrency limits.`
366+
: `Give this ${noun} its own concurrency limit instead of the environment default. Set it as a number or a percentage of the environment limit.`}
350367
</TooltipContent>
351368
</Tooltip>
352369
</TooltipProvider>
@@ -356,7 +373,7 @@ export function QueueOverrideConcurrencyButton({
356373
icon={AdjustmentsHorizontalIcon}
357374
title={
358375
disabled
359-
? "You don't have permission to manage queues"
376+
? `You don't have permission to manage ${noun}s`
360377
: isOverridden
361378
? "Edit override…"
362379
: "Override limit…"
@@ -373,20 +390,20 @@ export function QueueOverrideConcurrencyButton({
373390
{hasTotal ? (
374391
isOverridden ? (
375392
<Paragraph variant="small">
376-
This queue's limits are currently overridden. Fill a field to change that limit
393+
This {noun}'s limits are currently overridden. Fill a field to change that limit
377394
(blank fields stay unchanged), or remove the override to restore the limits set in
378395
code.
379396
</Paragraph>
380397
) : (
381398
<Paragraph variant="small">
382-
Override this queue's limits. Per key caps each concurrency key's pool, and total
399+
Override this {noun}'s limits. Per key caps each concurrency key's pool, and total
383400
caps runs across all keys together. Leave a field blank to keep that limit
384401
unchanged.
385402
</Paragraph>
386403
)
387404
) : isOverridden ? (
388405
<Paragraph variant="small">
389-
This queue's concurrency limit is currently overridden to {currentLimit}.
406+
This {noun}'s concurrency limit is currently overridden to {currentLimit}.
390407
{typeof queue.limits.perKey.base === "number" &&
391408
` The original limit set in code was ${queue.limits.perKey.base}.`}{" "}
392409
You can update the override or remove it to restore the{" "}
@@ -397,12 +414,13 @@ export function QueueOverrideConcurrencyButton({
397414
</Paragraph>
398415
) : (
399416
<Paragraph variant="small">
400-
Override this queue's concurrency limit. The current limit is {currentLimit}, which is
401-
set {queue.limits.perKey.current !== null ? "in code" : "by the environment"}.
417+
Override this {noun}'s concurrency limit. The current limit is {currentLimit}, which
418+
is set {queue.limits.perKey.current !== null ? "in code" : "by the environment"}.
402419
</Paragraph>
403420
)}
404421
<Form method="post" onSubmit={() => setIsOpen(false)} className="space-y-3">
405422
<input type="hidden" name="friendlyId" value={queue.id} />
423+
<input type="hidden" name="noun" value={noun} />
406424
<input type="hidden" name="mode" value={hasTotal ? "bounds" : mode} />
407425
{hasTotal ? (
408426
<>
@@ -524,7 +542,7 @@ export function QueueOverrideConcurrencyButton({
524542
<Hint className={limitOverCap ? "text-warning tabular-nums" : "tabular-nums"}>
525543
{limitOverCap
526544
? `Can't exceed the environment limit of ${environmentConcurrencyLimit}.`
527-
: `The most concurrent runs this queue can use at once. It can't exceed the environment limit of ${environmentConcurrencyLimit}.`}
545+
: `The most concurrent runs this ${noun} can use at once. It can't exceed the environment limit of ${environmentConcurrencyLimit}.`}
528546
</Hint>
529547
)}
530548
</InputGroup>

apps/webapp/app/models/queueMutation.server.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ export async function handleQueueMutationAction({
3535
case "queue-pause":
3636
case "queue-resume": {
3737
const friendlyId = formData.get("friendlyId");
38+
/** Named concurrency limits pause through these same actions; the noun only
39+
* changes the user-facing messages. */
40+
const noun = formData.get("noun") === "limit" ? "limit" : "queue";
3841
if (!friendlyId) {
3942
return redirectWithErrorMessage(redirectPath, request, "Queue ID is required");
4043
}
@@ -43,25 +46,27 @@ export async function handleQueueMutationAction({
4346
const result = await queueService.call(
4447
environment,
4548
friendlyId.toString(),
46-
action === "queue-pause" ? "paused" : "resumed"
49+
action === "queue-pause" ? "paused" : "resumed",
50+
{ roles: ["QUEUE", "LIMIT"] }
4751
);
4852

4953
if (!result.success) {
5054
return redirectWithErrorMessage(
5155
redirectPath,
5256
request,
53-
result.error ?? `Failed to ${action === "queue-pause" ? "pause" : "resume"} queue`
57+
result.error ?? `Failed to ${action === "queue-pause" ? "pause" : "resume"} ${noun}`
5458
);
5559
}
5660

5761
return redirectWithSuccessMessage(
5862
redirectPath,
5963
request,
60-
`Queue ${action === "queue-pause" ? "paused" : "resumed"}`
64+
`${noun === "limit" ? "Limit" : "Queue"} ${action === "queue-pause" ? "paused" : "resumed"}`
6165
);
6266
}
6367
case "queue-override": {
6468
const friendlyId = formData.get("friendlyId");
69+
const noun = formData.get("noun") === "limit" ? "limit" : "queue";
6570
const mode =
6671
formData.get("mode") === "percent"
6772
? "percent"
@@ -217,18 +222,21 @@ export async function handleQueueMutationAction({
217222
const message =
218223
"message" in error && typeof error.message === "string"
219224
? error.message
220-
: "Failed to override queue concurrency limit";
225+
: noun === "limit"
226+
? "Failed to override the limit"
227+
: "Failed to override queue concurrency limit";
221228
return redirectWithErrorMessage(redirectPath, request, message);
222229
}
223230

224231
return redirectWithSuccessMessage(
225232
redirectPath,
226233
request,
227-
"Queue concurrency limit overridden"
234+
noun === "limit" ? "Limit overridden" : "Queue concurrency limit overridden"
228235
);
229236
}
230237
case "queue-remove-override": {
231238
const friendlyId = formData.get("friendlyId");
239+
const noun = formData.get("noun") === "limit" ? "limit" : "queue";
232240

233241
if (!friendlyId) {
234242
return redirectWithErrorMessage(redirectPath, request, "Queue ID is required");
@@ -245,7 +253,9 @@ export async function handleQueueMutationAction({
245253
return redirectWithErrorMessage(
246254
redirectPath,
247255
request,
248-
"Failed to reset queue concurrency limit"
256+
noun === "limit"
257+
? "Failed to remove the limit override"
258+
: "Failed to reset queue concurrency limit"
249259
);
250260
}
251261

@@ -265,7 +275,11 @@ export async function handleQueueMutationAction({
265275
}
266276
}
267277

268-
return redirectWithSuccessMessage(redirectPath, request, "Queue concurrency limit reset");
278+
return redirectWithSuccessMessage(
279+
redirectPath,
280+
request,
281+
noun === "limit" ? "Limit override removed" : "Queue concurrency limit reset"
282+
);
269283
}
270284
default:
271285
return null;

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import {
77
} from "@trigger.dev/core/v3";
88
import type { QueueLimits } from "~/components/queues/queue-limits";
99
import {
10+
boundedIn,
1011
type PrismaClientOrTransaction,
1112
type TaskQueue,
13+
type TaskQueueRole,
1214
type User,
1315
type TaskQueueType,
1416
} from "@trigger.dev/database";
@@ -23,14 +25,17 @@ export type FoundQueue = Prettify<
2325
>;
2426

2527
/**
26-
* Shared queue lookup logic used by both QueueRetrievePresenter and PauseQueueService
28+
* Shared queue lookup logic used by both QueueRetrievePresenter and PauseQueueService.
29+
* Resolves QUEUE rows by default; callers whose operation also applies to named
30+
* concurrency limits (pause/resume) widen `roles` to include LIMIT rows.
2731
*/
2832
export async function getQueue(
2933
prismaClient: PrismaClientOrTransaction,
3034
environment: AuthenticatedEnvironment,
31-
queue: RetrieveQueueParam
35+
queue: RetrieveQueueParam,
36+
opts?: { roles?: TaskQueueRole[] }
3237
) {
33-
const role = "QUEUE" as const;
38+
const roles = opts?.roles ?? ["QUEUE" as const];
3439

3540
if (typeof queue === "string") {
3641
return joinQueueWithUser(
@@ -39,7 +44,7 @@ export async function getQueue(
3944
where: {
4045
friendlyId: queue,
4146
runtimeEnvironmentId: environment.id,
42-
role,
47+
role: { in: boundedIn(roles) },
4348
},
4449
})
4550
);
@@ -53,7 +58,7 @@ export async function getQueue(
5358
where: {
5459
name: queueName,
5560
runtimeEnvironmentId: environment.id,
56-
role,
61+
role: { in: boundedIn(roles) },
5762
},
5863
})
5964
);

0 commit comments

Comments
 (0)