Skip to content

Commit 6df8069

Browse files
ericallammatt-aitkencoderabbitai[bot]
authored
Batch Trigger upgrades (#1502)
* WIP batch trigger v2 * Fix for the DateField being one month out… getUTCMonth() is zero indexed 🤦‍♂️ * Added a custom date range filter * Deal with closing the custom date range * Child runs filter * Fix for the clear button untoggling the child runs * WIP batchTriggerV2 * Finished removing rate limit from the webapp * Added an index TaskRun to make useRealtimeBatch performant * Renamed the period filter labels to be “Last X mins” * Denormalize background worker columns into TaskRun * Use the runTags column on TaskRun * Add TaskRun ("projectId", "id" DESC) index * Improved the v2 batch trigger endpoint to process items in parallel and also added a threshold, below which the processing of items is async * Added a runId filter, and WIP for batchId filter * WIP triggerAll * Add new batch methods for triggering multiple different tasks in a single batch * Disabled switch styling * Batch filtering, force child runs to show if filtering by batch/run * Added schedule ID filtering * Force child runs to show when filtering by scheduleId, for consistency * realtime: allow setting enabled: false on useApiClient * Batches page * Always complete batches, not only batchTriggerAndWait in deployed tasks * Add batch.retrieve and allow filtering by batch in runs.list * Renamed pending to “In progress” * Tidied up the table a bit * Deal with old batches: “Legacy batch” * Added the Batch to the run inspector * Fixed the migration that created the new idempotency key index on BatchTaskRun * Fixed the name of the idempotencyKeyExpiresAt option and now default idempotency key TTL is 30 days, not 24 hours * Timezone fix: wrong month in Usage page dropdown * The DateField now defaults to local time, but can be overriden to use utc with an option * Don’t allow the task icon to get squished * BatchFilters removed unused imports * In the batch filtering, use `id` instead of `batchId` in the URL * BatchFilters: we don’t need a child tasks hidden input field * Creates some common filter components/functions * Fix for batchVersion check when filtering by batch status * Add additional logging around telemetry and more attributes for trigger spans * Show clear button for specific id filters * Batch list: only allow environments that are part of this project * Unnecessary optional chain Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Add JSDocs --------- Co-authored-by: Matt Aitken <matt@mattaitken.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent b714d6f commit 6df8069

File tree

82 files changed

+5617
-646
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+5617
-646
lines changed

.changeset/perfect-onions-call.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
"trigger.dev": patch
4+
"@trigger.dev/core": patch
5+
---
6+
7+
Added new batch.trigger and batch.triggerByTask methods that allows triggering multiple different tasks in a single batch:
8+
9+
```ts
10+
import { batch } from '@trigger.dev/sdk/v3';
11+
import type { myTask1, myTask2 } from './trigger/tasks';
12+
13+
// Somewhere in your backend code
14+
const response = await batch.trigger<typeof myTask1 | typeof myTask2>([
15+
{ id: 'task1', payload: { foo: 'bar' } },
16+
{ id: 'task2', payload: { baz: 'qux' } },
17+
]);
18+
19+
for (const run of response.runs) {
20+
if (run.ok) {
21+
console.log(run.output);
22+
} else {
23+
console.error(run.error);
24+
}
25+
}
26+
```
27+
28+
Or if you are inside of a task, you can use `triggerByTask`:
29+
30+
```ts
31+
import { batch, task, runs } from '@trigger.dev/sdk/v3';
32+
33+
export const myParentTask = task({
34+
id: 'myParentTask',
35+
run: async () => {
36+
const response = await batch.triggerByTask([
37+
{ task: myTask1, payload: { foo: 'bar' } },
38+
{ task: myTask2, payload: { baz: 'qux' } },
39+
]);
40+
41+
const run1 = await runs.retrieve(response.runs[0]);
42+
console.log(run1.output) // typed as { foo: string }
43+
44+
const run2 = await runs.retrieve(response.runs[1]);
45+
console.log(run2.output) // typed as { baz: string }
46+
47+
const response2 = await batch.triggerByTaskAndWait([
48+
{ task: myTask1, payload: { foo: 'bar' } },
49+
{ task: myTask2, payload: { baz: 'qux' } },
50+
]);
51+
52+
if (response2.runs[0].ok) {
53+
console.log(response2.runs[0].output) // typed as { foo: string }
54+
}
55+
56+
if (response2.runs[1].ok) {
57+
console.log(response2.runs[1].output) // typed as { baz: string }
58+
}
59+
}
60+
});
61+
62+
export const myTask1 = task({
63+
id: 'myTask1',
64+
run: async () => {
65+
return {
66+
foo: 'bar'
67+
}
68+
}
69+
});
70+
71+
export const myTask2 = task({
72+
id: 'myTask2',
73+
run: async () => {
74+
return {
75+
baz: 'qux'
76+
}
77+
}
78+
});
79+
80+
```

.changeset/ten-pans-itch.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@trigger.dev/react-hooks": minor
3+
"@trigger.dev/sdk": minor
4+
"@trigger.dev/core": minor
5+
---
6+
7+
Improved Batch Triggering:
8+
9+
- The new Batch Trigger endpoint is now asynchronous and supports up to 500 runs per request.
10+
- The new endpoint also supports triggering multiple different tasks in a single batch request (support in the SDK coming soon).
11+
- The existing `batchTrigger` method now supports the new endpoint, and shouldn't require any changes to your code.
12+
13+
- Idempotency keys now expire after 24 hours, and you can customize the expiration time when creating a new key by using the `idempotencyKeyTTL` parameter:
14+
15+
```ts
16+
await myTask.batchTrigger([{ payload: { foo: "bar" }}], { idempotencyKey: "my-key", idempotencyKeyTTL: "60s" })
17+
// Works for individual items as well:
18+
await myTask.batchTrigger([{ payload: { foo: "bar" }, options: { idempotencyKey: "my-key", idempotencyKeyTTL: "60s" }}])
19+
// And `trigger`:
20+
await myTask.trigger({ foo: "bar" }, { idempotencyKey: "my-key", idempotencyKeyTTL: "60s" });
21+
```
22+
23+
### Breaking Changes
24+
25+
- We've removed the `idempotencyKey` option from `triggerAndWait` and `batchTriggerAndWait`, because it can lead to permanently frozen runs in deployed tasks. We're working on upgrading our entire system to support idempotency keys on these methods, and we'll re-add the option once that's complete.

.changeset/wild-needles-hunt.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@trigger.dev/react-hooks": patch
3+
"@trigger.dev/sdk": patch
4+
"@trigger.dev/core": patch
5+
---
6+
7+
Added ability to subscribe to a batch of runs using runs.subscribeToBatch

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This is the repo for Trigger.dev, a background jobs platform written in TypeScript. Our webapp at apps/webapp is a Remix 2.1 app that uses Node.js v20. Our SDK is an isomorphic TypeScript SDK at packages/trigger-sdk. Always prefer using isomorphic code like fetch, ReadableStream, etc. instead of Node.js specific code. Our tests are all vitest. We use prisma in internal-packages/database for our database interactions using PostgreSQL. For TypeScript, we usually use types over interfaces. We use zod a lot in packages/core and in the webapp. Avoid enums. Use strict mode. No default exports, use function declarations.

apps/webapp/app/components/navigation/SideMenu.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
RectangleStackIcon,
1313
ServerStackIcon,
1414
ShieldCheckIcon,
15+
Squares2X2Icon,
1516
} from "@heroicons/react/20/solid";
1617
import { UserGroupIcon, UserPlusIcon } from "@heroicons/react/24/solid";
1718
import { useNavigation } from "@remix-run/react";
@@ -45,6 +46,7 @@ import {
4546
projectSetupPath,
4647
projectTriggersPath,
4748
v3ApiKeysPath,
49+
v3BatchesPath,
4850
v3BillingPath,
4951
v3ConcurrencyPath,
5052
v3DeploymentsPath,
@@ -475,6 +477,13 @@ function V3ProjectSideMenu({
475477
activeIconColor="text-teal-500"
476478
to={v3RunsPath(organization, project)}
477479
/>
480+
<SideMenuItem
481+
name="Batches"
482+
icon={Squares2X2Icon}
483+
activeIconColor="text-blue-500"
484+
to={v3BatchesPath(organization, project)}
485+
data-action="batches"
486+
/>
478487
<SideMenuItem
479488
name="Test"
480489
icon={BeakerIcon}

apps/webapp/app/components/primitives/DateField.tsx

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BellAlertIcon } from "@heroicons/react/20/solid";
1+
import { BellAlertIcon, XMarkIcon } from "@heroicons/react/20/solid";
22
import { CalendarDateTime, createCalendar } from "@internationalized/date";
33
import { useDateField, useDateSegment } from "@react-aria/datepicker";
44
import type { DateFieldState, DateSegment } from "@react-stately/datepicker";
@@ -12,7 +12,7 @@ const variants = {
1212
small: {
1313
fieldStyles: "h-5 text-sm rounded-sm px-0.5",
1414
nowButtonVariant: "tertiary/small" as const,
15-
clearButtonVariant: "minimal/small" as const,
15+
clearButtonVariant: "tertiary/small" as const,
1616
},
1717
medium: {
1818
fieldStyles: "h-7 text-base rounded px-1",
@@ -35,9 +35,12 @@ type DateFieldProps = {
3535
showNowButton?: boolean;
3636
showClearButton?: boolean;
3737
onValueChange?: (value: Date | undefined) => void;
38+
utc?: boolean;
3839
variant?: Variant;
3940
};
4041

42+
const deviceTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
43+
4144
export function DateField({
4245
label,
4346
defaultValue,
@@ -50,22 +53,23 @@ export function DateField({
5053
showGuide = false,
5154
showNowButton = false,
5255
showClearButton = false,
56+
utc = false,
5357
variant = "small",
5458
}: DateFieldProps) {
5559
const [value, setValue] = useState<undefined | CalendarDateTime>(
56-
utcDateToCalendarDate(defaultValue)
60+
utc ? utcDateToCalendarDate(defaultValue) : dateToCalendarDate(defaultValue)
5761
);
5862

5963
const state = useDateFieldState({
6064
value: value,
6165
onChange: (value) => {
6266
if (value) {
6367
setValue(value);
64-
onValueChange?.(value.toDate("utc"));
68+
onValueChange?.(value.toDate(utc ? "utc" : deviceTimezone));
6569
}
6670
},
67-
minValue: utcDateToCalendarDate(minValue),
68-
maxValue: utcDateToCalendarDate(maxValue),
71+
minValue: utc ? utcDateToCalendarDate(minValue) : dateToCalendarDate(minValue),
72+
maxValue: utc ? utcDateToCalendarDate(maxValue) : dateToCalendarDate(maxValue),
6973
shouldForceLeadingZeros: true,
7074
granularity,
7175
locale: "en-US",
@@ -78,7 +82,9 @@ export function DateField({
7882
useEffect(() => {
7983
if (state.value === undefined && defaultValue === undefined) return;
8084

81-
const calendarDate = utcDateToCalendarDate(defaultValue);
85+
const calendarDate = utc
86+
? utcDateToCalendarDate(defaultValue)
87+
: dateToCalendarDate(defaultValue);
8288
//unchanged
8389
if (state.value?.toDate("utc").getTime() === defaultValue?.getTime()) {
8490
return;
@@ -134,23 +140,19 @@ export function DateField({
134140
<Button
135141
type="button"
136142
variant={variants[variant].nowButtonVariant}
137-
LeadingIcon={BellAlertIcon}
138-
leadingIconClassName="text-text-dimmed group-hover:text-text-bright"
139143
onClick={() => {
140144
const now = new Date();
141-
setValue(utcDateToCalendarDate(new Date()));
145+
setValue(utc ? utcDateToCalendarDate(now) : dateToCalendarDate(now));
142146
onValueChange?.(now);
143147
}}
144148
>
145-
<span className="text-text-dimmed transition group-hover:text-text-bright">Now</span>
149+
Now
146150
</Button>
147151
)}
148152
{showClearButton && (
149153
<Button
150154
type="button"
151155
variant={variants[variant].clearButtonVariant}
152-
LeadingIcon={"close"}
153-
leadingIconClassName="-mr-2"
154156
onClick={() => {
155157
setValue(undefined);
156158
onValueChange?.(undefined);
@@ -181,7 +183,7 @@ function utcDateToCalendarDate(date?: Date) {
181183
return date
182184
? new CalendarDateTime(
183185
date.getUTCFullYear(),
184-
date.getUTCMonth(),
186+
date.getUTCMonth() + 1,
185187
date.getUTCDate(),
186188
date.getUTCHours(),
187189
date.getUTCMinutes(),
@@ -190,6 +192,19 @@ function utcDateToCalendarDate(date?: Date) {
190192
: undefined;
191193
}
192194

195+
function dateToCalendarDate(date?: Date) {
196+
return date
197+
? new CalendarDateTime(
198+
date.getFullYear(),
199+
date.getMonth() + 1,
200+
date.getDate(),
201+
date.getHours(),
202+
date.getMinutes(),
203+
date.getSeconds()
204+
)
205+
: undefined;
206+
}
207+
193208
type DateSegmentProps = {
194209
segment: DateSegment;
195210
state: DateFieldState;

apps/webapp/app/components/primitives/Select.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ export interface SelectItemProps extends Ariakit.SelectItemProps {
440440
}
441441

442442
const selectItemClasses =
443-
"group cursor-pointer px-1 pt-1 text-sm text-text-dimmed focus-custom last:pb-1";
443+
"group cursor-pointer px-1 pt-1 text-2sm text-text-dimmed focus-custom last:pb-1";
444444

445445
export function SelectItem({
446446
icon,
@@ -613,7 +613,7 @@ export function SelectPopover({
613613
"z-50 flex flex-col overflow-clip rounded border border-charcoal-700 bg-background-bright shadow-md outline-none animate-in fade-in-40",
614614
"min-w-[max(180px,calc(var(--popover-anchor-width)+0.5rem))]",
615615
"max-w-[min(480px,var(--popover-available-width))]",
616-
"max-h-[min(520px,var(--popover-available-height))]",
616+
"max-h-[min(600px,var(--popover-available-height))]",
617617
"origin-[var(--popover-transform-origin)]",
618618
className
619619
)}

apps/webapp/app/components/primitives/Switch.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ const variations = {
1313
},
1414
small: {
1515
container:
16-
"flex items-center h-[1.5rem] gap-x-1.5 rounded hover:bg-tertiary pr-1 py-[0.1rem] pl-1.5 transition focus-custom",
16+
"flex items-center h-[1.5rem] gap-x-1.5 rounded hover:bg-tertiary disabled:hover:bg-transparent pr-1 py-[0.1rem] pl-1.5 transition focus-custom disabled:hover:text-charcoal-400 disabled:opacity-50 text-charcoal-400 hover:text-charcoal-200 disabled:hover:cursor-not-allowed hover:cursor-pointer",
1717
root: "h-3 w-6",
1818
thumb: "h-2.5 w-2.5 data-[state=checked]:translate-x-2.5 data-[state=unchecked]:translate-x-0",
19-
text: "text-xs text-charcoal-400 group-hover:text-charcoal-200 hover:cursor-pointer transition",
19+
text: "text-xs transition",
2020
},
2121
};
2222

apps/webapp/app/components/runs/TimeFrameFilter.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ export function AbsoluteTimeFrame({
214214
granularity="second"
215215
showNowButton
216216
showClearButton
217+
utc
217218
/>
218219
</div>
219220
<div className="space-y-1">
@@ -227,6 +228,7 @@ export function AbsoluteTimeFrame({
227228
granularity="second"
228229
showNowButton
229230
showClearButton
231+
utc
230232
/>
231233
</div>
232234
</div>

0 commit comments

Comments
 (0)