Merge remote-tracking branch 'origin/main' into feat/compute-workload-manager
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/build": patch
|
||||
---
|
||||
|
||||
Add syncSupabaseEnvVars to pull database connection strings and save them as trigger.dev environment variables
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
area: webapp
|
||||
type: feature
|
||||
---
|
||||
|
||||
A new Errors page for viewing and tracking errors that cause runs to fail
|
||||
|
||||
- Errors are grouped using error fingerprinting
|
||||
- View top errors for a time period, filter by task, or search the text
|
||||
- View occurrences over time
|
||||
- View all the runs for an error and bulk replay them
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
area: webapp
|
||||
type: feature
|
||||
---
|
||||
|
||||
Added `/engine/v1/dev/disconnect` endpoint to auto-cancel runs when the CLI disconnects. Maximum of 500 runs can be cancelled. Uses the bulk action system when there are more than 25 runs to cancel.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
area: webapp
|
||||
type: feature
|
||||
---
|
||||
|
||||
Add sidebar tabs (Options, AI, Schema) to the Test page for schemaTask payload generation and schema viewing.
|
||||
@@ -210,11 +210,22 @@ export function TechnologyPicker({
|
||||
|
||||
const addCustomValue = useCallback(() => {
|
||||
const trimmed = otherInputValue.trim();
|
||||
if (trimmed && !customValues.includes(trimmed) && !value.includes(trimmed)) {
|
||||
if (!trimmed) return;
|
||||
|
||||
const matchedOption = TECHNOLOGY_OPTIONS.find(
|
||||
(opt) => opt.toLowerCase() === trimmed.toLowerCase()
|
||||
);
|
||||
|
||||
if (matchedOption) {
|
||||
if (!value.includes(matchedOption)) {
|
||||
onChange([...value, matchedOption]);
|
||||
}
|
||||
} else if (!customValues.includes(trimmed) && !value.includes(trimmed)) {
|
||||
onCustomValuesChange([...customValues, trimmed]);
|
||||
setOtherInputValue("");
|
||||
}
|
||||
}, [otherInputValue, customValues, onCustomValuesChange, value]);
|
||||
|
||||
setOtherInputValue("");
|
||||
}, [otherInputValue, customValues, onCustomValuesChange, value, onChange]);
|
||||
|
||||
const handleOtherKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { Link, type LinkProps, NavLink, type NavLinkProps } from "@remix-run/react";
|
||||
import React, { forwardRef, type ReactNode, useImperativeHandle, useRef } from "react";
|
||||
import React, {
|
||||
forwardRef,
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { type ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { ShortcutKey } from "./ShortcutKey";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./Tooltip";
|
||||
import { Icon, type RenderIcon } from "./Icon";
|
||||
import { Spinner } from "./Spinner";
|
||||
|
||||
const sizes = {
|
||||
small: {
|
||||
@@ -180,6 +188,7 @@ export type ButtonContentPropsType = {
|
||||
tooltip?: ReactNode;
|
||||
iconSpacing?: string;
|
||||
hideShortcutKey?: boolean;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
export function ButtonContent(props: ButtonContentPropsType) {
|
||||
@@ -196,7 +205,19 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
tooltip,
|
||||
iconSpacing,
|
||||
hideShortcutKey,
|
||||
isLoading,
|
||||
} = props;
|
||||
|
||||
const [showSpinner, setShowSpinner] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
setShowSpinner(false);
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => setShowSpinner(true), 200);
|
||||
return () => clearTimeout(timer);
|
||||
}, [isLoading]);
|
||||
|
||||
const variation = allVariants.variant[props.variant];
|
||||
|
||||
const btnClassName = cn(allVariants.$all, variation.button);
|
||||
@@ -217,56 +238,64 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
|
||||
const buttonContent = (
|
||||
<div className={cn("flex", fullWidth ? "" : "w-fit text-xxs", btnClassName, className)}>
|
||||
<div
|
||||
className={cn(
|
||||
textAlignLeft ? "text-left" : "justify-center",
|
||||
"flex w-full items-center",
|
||||
iconSpacingClassName,
|
||||
iconSpacing
|
||||
<div className={cn("relative", "flex w-full items-center")}>
|
||||
<div
|
||||
className={cn(
|
||||
textAlignLeft ? "text-left" : "justify-center",
|
||||
"flex w-full items-center",
|
||||
iconSpacingClassName,
|
||||
iconSpacing,
|
||||
showSpinner && "invisible"
|
||||
)}
|
||||
>
|
||||
{LeadingIcon && (
|
||||
<Icon
|
||||
icon={LeadingIcon}
|
||||
className={cn(
|
||||
iconClassName,
|
||||
variation.icon,
|
||||
leadingIconClassName,
|
||||
"shrink-0 justify-start"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{text &&
|
||||
(typeof text === "string" ? (
|
||||
<span className={cn("mx-auto grow self-center truncate", textColorClassName)}>
|
||||
{text}
|
||||
</span>
|
||||
) : (
|
||||
<>{text}</>
|
||||
))}
|
||||
|
||||
{shortcut &&
|
||||
!tooltip &&
|
||||
props.shortcutPosition === "before-trailing-icon" &&
|
||||
renderShortcutKey()}
|
||||
|
||||
{TrailingIcon && (
|
||||
<Icon
|
||||
icon={TrailingIcon}
|
||||
className={cn(
|
||||
iconClassName,
|
||||
variation.icon,
|
||||
trailingIconClassName,
|
||||
"shrink-0 justify-end"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shortcut &&
|
||||
!tooltip &&
|
||||
(!props.shortcutPosition || props.shortcutPosition === "after-trailing-icon") &&
|
||||
renderShortcutKey()}
|
||||
</div>
|
||||
{showSpinner && (
|
||||
<span className="absolute inset-0 flex items-center justify-center">
|
||||
<Spinner className="size-3.5" color="white" />
|
||||
</span>
|
||||
)}
|
||||
>
|
||||
{LeadingIcon && (
|
||||
<Icon
|
||||
icon={LeadingIcon}
|
||||
className={cn(
|
||||
iconClassName,
|
||||
variation.icon,
|
||||
leadingIconClassName,
|
||||
"shrink-0 justify-start"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{text &&
|
||||
(typeof text === "string" ? (
|
||||
<span className={cn("mx-auto grow self-center truncate", textColorClassName)}>
|
||||
{text}
|
||||
</span>
|
||||
) : (
|
||||
<>{text}</>
|
||||
))}
|
||||
|
||||
{shortcut &&
|
||||
!tooltip &&
|
||||
props.shortcutPosition === "before-trailing-icon" &&
|
||||
renderShortcutKey()}
|
||||
|
||||
{TrailingIcon && (
|
||||
<Icon
|
||||
icon={TrailingIcon}
|
||||
className={cn(
|
||||
iconClassName,
|
||||
variation.icon,
|
||||
trailingIconClassName,
|
||||
"shrink-0 justify-end"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shortcut &&
|
||||
!tooltip &&
|
||||
(!props.shortcutPosition || props.shortcutPosition === "after-trailing-icon") &&
|
||||
renderShortcutKey()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -298,6 +327,8 @@ export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||
const innerRef = useRef<HTMLButtonElement>(null);
|
||||
useImperativeHandle(ref, () => innerRef.current as HTMLButtonElement);
|
||||
|
||||
const isDisabled = disabled || props.isLoading;
|
||||
|
||||
useShortcutKeys({
|
||||
shortcut: props.shortcut,
|
||||
action: (e) => {
|
||||
@@ -307,14 +338,14 @@ export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
disabled: disabled || !props.shortcut,
|
||||
disabled: isDisabled || !props.shortcut,
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn("group/button outline-none focus-custom", props.fullWidth ? "w-full" : "")}
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
disabled={isDisabled}
|
||||
onClick={onClick}
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { Input } from "~/components/primitives/Input";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
import { Select, SelectItem } from "~/components/primitives/Select";
|
||||
import { ButtonSpinner } from "~/components/primitives/Spinner";
|
||||
|
||||
import { prisma } from "~/db.server";
|
||||
import { featuresForRequest } from "~/features.server";
|
||||
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
||||
@@ -169,6 +169,8 @@ const schema = z.object({
|
||||
technologiesOther: z.string().optional(),
|
||||
goals: z.string().optional(),
|
||||
goalsOther: z.string().optional(),
|
||||
workingOnPositions: z.string().optional(),
|
||||
goalsPositions: z.string().optional(),
|
||||
});
|
||||
|
||||
export const action: ActionFunction = async ({ request, params }) => {
|
||||
@@ -200,10 +202,25 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const numberArraySchema = z.array(z.number());
|
||||
function safeParseNumberArray(value: string | undefined): number[] | undefined {
|
||||
if (!value) return undefined;
|
||||
try {
|
||||
const result = numberArraySchema.safeParse(JSON.parse(value));
|
||||
return result.success && result.data.length > 0 ? result.data : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const onboardingData: Record<string, Prisma.InputJsonValue> = {};
|
||||
|
||||
const workingOn = safeParseStringArray(submission.value.workingOn);
|
||||
if (workingOn) onboardingData.workingOn = workingOn;
|
||||
if (workingOn) {
|
||||
onboardingData.workingOn = workingOn;
|
||||
const workingOnPositions = safeParseNumberArray(submission.value.workingOnPositions);
|
||||
if (workingOnPositions) onboardingData.workingOnPositions = workingOnPositions;
|
||||
}
|
||||
|
||||
if (submission.value.workingOnOther) {
|
||||
onboardingData.workingOnOther = submission.value.workingOnOther;
|
||||
@@ -216,7 +233,11 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
if (technologiesOther) onboardingData.technologiesOther = technologiesOther;
|
||||
|
||||
const goals = safeParseStringArray(submission.value.goals);
|
||||
if (goals) onboardingData.goals = goals;
|
||||
if (goals) {
|
||||
onboardingData.goals = goals;
|
||||
const goalsPositions = safeParseNumberArray(submission.value.goalsPositions);
|
||||
if (goalsPositions) onboardingData.goalsPositions = goalsPositions;
|
||||
}
|
||||
|
||||
if (submission.value.goalsOther) {
|
||||
onboardingData.goalsOther = submission.value.goalsOther;
|
||||
@@ -376,6 +397,13 @@ export default function Page() {
|
||||
<InputGroup>
|
||||
<Label>What are you working on?</Label>
|
||||
<input type="hidden" name="workingOn" value={JSON.stringify(selectedWorkingOn)} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="workingOnPositions"
|
||||
value={JSON.stringify(
|
||||
selectedWorkingOn.map((v) => shuffledWorkingOn.indexOf(v) + 1)
|
||||
)}
|
||||
/>
|
||||
<MultiSelectField
|
||||
value={selectedWorkingOn}
|
||||
setValue={setSelectedWorkingOn}
|
||||
@@ -421,6 +449,13 @@ export default function Page() {
|
||||
<InputGroup>
|
||||
<Label>What are you trying to do with Trigger.dev?</Label>
|
||||
<input type="hidden" name="goals" value={JSON.stringify(selectedGoals)} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="goalsPositions"
|
||||
value={JSON.stringify(
|
||||
selectedGoals.map((v) => shuffledGoals.indexOf(v) + 1)
|
||||
)}
|
||||
/>
|
||||
<MultiSelectField
|
||||
value={selectedGoals}
|
||||
setValue={setSelectedGoals}
|
||||
@@ -445,13 +480,8 @@ export default function Page() {
|
||||
|
||||
<FormButtons
|
||||
confirmButton={
|
||||
<Button
|
||||
type="submit"
|
||||
variant={"primary/small"}
|
||||
disabled={isLoading}
|
||||
TrailingIcon={isLoading ? ButtonSpinner : undefined}
|
||||
>
|
||||
{isLoading ? "Creating…" : "Create"}
|
||||
<Button type="submit" variant={"primary/small"} isLoading={isLoading}>
|
||||
Create
|
||||
</Button>
|
||||
}
|
||||
cancelButton={
|
||||
|
||||
@@ -220,7 +220,7 @@ export default function NewOrganizationPage() {
|
||||
|
||||
<FormButtons
|
||||
confirmButton={
|
||||
<Button type="submit" variant={"primary/small"} disabled={isLoading}>
|
||||
<Button type="submit" variant={"primary/small"} isLoading={isLoading}>
|
||||
Create
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ArrowRightIcon, EnvelopeIcon, UserGroupIcon, UserIcon } from "@heroicon
|
||||
import { HandRaisedIcon } from "@heroicons/react/24/solid";
|
||||
import { RadioGroup } from "@radix-ui/react-radio-group";
|
||||
import { json, type ActionFunction } from "@remix-run/node";
|
||||
import { Form, useActionData } from "@remix-run/react";
|
||||
import { Form, useActionData, useNavigation } from "@remix-run/react";
|
||||
import { motion } from "framer-motion";
|
||||
import { forwardRef, useEffect, useState } from "react";
|
||||
import { z } from "zod";
|
||||
@@ -99,6 +99,8 @@ function createSchema(
|
||||
referralSourceOther: z.string().optional(),
|
||||
role: z.string().optional(),
|
||||
roleOther: z.string().optional(),
|
||||
referralSourcePosition: z.coerce.number().optional(),
|
||||
rolePosition: z.coerce.number().optional(),
|
||||
})
|
||||
.refine((value) => value.email === value.confirmEmail, {
|
||||
message: "Emails must match",
|
||||
@@ -141,6 +143,9 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
|
||||
if (submission.value.referralSource) {
|
||||
onboardingData.referralSource = submission.value.referralSource;
|
||||
if (submission.value.referralSourcePosition) {
|
||||
onboardingData.referralSourcePosition = String(submission.value.referralSourcePosition);
|
||||
}
|
||||
if (submission.value.referralSource === "Other" && submission.value.referralSourceOther) {
|
||||
onboardingData.referralSourceOther = submission.value.referralSourceOther;
|
||||
}
|
||||
@@ -148,6 +153,9 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
|
||||
if (submission.value.role) {
|
||||
onboardingData.role = submission.value.role;
|
||||
if (submission.value.rolePosition) {
|
||||
onboardingData.rolePosition = String(submission.value.rolePosition);
|
||||
}
|
||||
if (submission.value.role === "Other" && submission.value.roleOther) {
|
||||
onboardingData.roleOther = submission.value.roleOther;
|
||||
}
|
||||
@@ -201,6 +209,8 @@ export default function Page() {
|
||||
const lastSubmission = useActionData();
|
||||
const [enteredEmail, setEnteredEmail] = useState<string>(user.email ?? "");
|
||||
const { isManagedCloud } = useFeatures();
|
||||
const navigation = useNavigation();
|
||||
const isSubmitting = navigation.state === "submitting" || navigation.state === "loading";
|
||||
const [selectedReferralSource, setSelectedReferralSource] = useState<string | undefined>();
|
||||
const [selectedRole, setSelectedRole] = useState<string>("");
|
||||
|
||||
@@ -317,6 +327,15 @@ export default function Page() {
|
||||
name="referralSource"
|
||||
value={selectedReferralSource ?? ""}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="referralSourcePosition"
|
||||
value={
|
||||
selectedReferralSource
|
||||
? shuffledReferralSources.indexOf(selectedReferralSource) + 1
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
<RadioGroup
|
||||
value={selectedReferralSource}
|
||||
onValueChange={setSelectedReferralSource}
|
||||
@@ -348,6 +367,11 @@ export default function Page() {
|
||||
<InputGroup className="mt-1">
|
||||
<Label id="role-label">What role fits you best?</Label>
|
||||
<input type="hidden" name="role" value={selectedRole} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="rolePosition"
|
||||
value={selectedRole ? shuffledRoles.indexOf(selectedRole) + 1 : ""}
|
||||
/>
|
||||
<Select<string, string>
|
||||
value={selectedRole}
|
||||
setValue={setSelectedRole}
|
||||
@@ -384,7 +408,12 @@ export default function Page() {
|
||||
|
||||
<FormButtons
|
||||
confirmButton={
|
||||
<Button type="submit" variant={"primary/small"} TrailingIcon={ArrowRightIcon}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant={"primary/small"}
|
||||
TrailingIcon={ArrowRightIcon}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { Ratelimit } from "@upstash/ratelimit";
|
||||
import { tryCatch } from "@trigger.dev/core";
|
||||
import { DevDisconnectRequestBody } from "@trigger.dev/core/v3";
|
||||
import { BulkActionId, RunId } from "@trigger.dev/core/v3/isomorphic";
|
||||
import { BulkActionNotificationType, BulkActionType } from "@trigger.dev/database";
|
||||
import { prisma } from "~/db.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { RateLimiter } from "~/services/rateLimiter.server";
|
||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
import { CancelTaskRunService } from "~/v3/services/cancelTaskRun.server";
|
||||
import { commonWorker } from "~/v3/commonWorker.server";
|
||||
import pMap from "p-map";
|
||||
|
||||
const CANCEL_REASON = "Dev session ended (CLI exited)";
|
||||
|
||||
// Below this threshold, cancel runs inline with pMap.
|
||||
// Above it, create a bulk action and process asynchronously.
|
||||
const BULK_ACTION_THRESHOLD = 25;
|
||||
|
||||
// Maximum number of runs that can be cancelled in a single disconnect call.
|
||||
const MAX_RUNS = 500;
|
||||
|
||||
// Rate limit: 5 calls per minute per environment
|
||||
const disconnectRateLimiter = new RateLimiter({
|
||||
keyPrefix: "dev-disconnect",
|
||||
limiter: Ratelimit.fixedWindow(5, "1 m"),
|
||||
logFailure: true,
|
||||
});
|
||||
|
||||
const { action } = createActionApiRoute(
|
||||
{
|
||||
body: DevDisconnectRequestBody,
|
||||
maxContentLength: 1024 * 256, // 256KB
|
||||
method: "POST",
|
||||
},
|
||||
async ({ authentication, body }) => {
|
||||
// Only allow dev environments — this endpoint uses finalizeRun which
|
||||
// skips PENDING_CANCEL and immediately finalizes executing runs.
|
||||
if (authentication.environment.type !== "DEVELOPMENT") {
|
||||
return json({ error: "This endpoint is only available for dev environments" }, { status: 403 });
|
||||
}
|
||||
|
||||
const environmentId = authentication.environment.id;
|
||||
|
||||
// Rate limit per environment
|
||||
const rateLimitResult = await disconnectRateLimiter.limit(environmentId);
|
||||
if (!rateLimitResult.success) {
|
||||
return json(
|
||||
{ error: "Rate limit exceeded", retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000) },
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
|
||||
if (body.runFriendlyIds.length > MAX_RUNS) {
|
||||
return json(
|
||||
{ error: `A maximum of ${MAX_RUNS} runs can be cancelled per request` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { runFriendlyIds } = body;
|
||||
|
||||
if (runFriendlyIds.length === 0) {
|
||||
return json({ cancelled: 0 }, { status: 200 });
|
||||
}
|
||||
|
||||
logger.info("Dev disconnect: cancelling runs", {
|
||||
environmentId,
|
||||
runCount: runFriendlyIds.length,
|
||||
});
|
||||
|
||||
// For small numbers of runs, cancel inline
|
||||
if (runFriendlyIds.length <= BULK_ACTION_THRESHOLD) {
|
||||
const cancelled = await cancelRunsInline(runFriendlyIds, environmentId);
|
||||
return json({ cancelled }, { status: 200 });
|
||||
}
|
||||
|
||||
// For large numbers, create a bulk action to process asynchronously
|
||||
const bulkActionId = await createBulkCancelAction(
|
||||
runFriendlyIds,
|
||||
authentication.environment.project.id,
|
||||
environmentId
|
||||
);
|
||||
|
||||
logger.info("Dev disconnect: created bulk action for large run set", {
|
||||
environmentId,
|
||||
bulkActionId,
|
||||
runCount: runFriendlyIds.length,
|
||||
});
|
||||
|
||||
return json({ cancelled: 0, bulkActionId }, { status: 200 });
|
||||
}
|
||||
);
|
||||
|
||||
async function cancelRunsInline(
|
||||
runFriendlyIds: string[],
|
||||
environmentId: string
|
||||
): Promise<number> {
|
||||
const runIds = runFriendlyIds.map((fid) => RunId.toId(fid));
|
||||
|
||||
const runs = await prisma.taskRun.findMany({
|
||||
where: {
|
||||
id: { in: runIds },
|
||||
runtimeEnvironmentId: environmentId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
engine: true,
|
||||
friendlyId: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
completedAt: true,
|
||||
taskEventStore: true,
|
||||
},
|
||||
});
|
||||
|
||||
let cancelled = 0;
|
||||
const cancelService = new CancelTaskRunService(prisma);
|
||||
|
||||
await pMap(
|
||||
runs,
|
||||
async (run) => {
|
||||
const [error, result] = await tryCatch(
|
||||
cancelService.call(run, { reason: CANCEL_REASON, finalizeRun: true })
|
||||
);
|
||||
|
||||
if (error) {
|
||||
logger.error("Dev disconnect: failed to cancel run", {
|
||||
runId: run.id,
|
||||
error,
|
||||
});
|
||||
} else if (result && !result.alreadyFinished) {
|
||||
cancelled++;
|
||||
}
|
||||
},
|
||||
{ concurrency: 10 }
|
||||
);
|
||||
|
||||
logger.info("Dev disconnect: completed inline cancellation", {
|
||||
environmentId,
|
||||
cancelled,
|
||||
total: runFriendlyIds.length,
|
||||
});
|
||||
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
async function createBulkCancelAction(
|
||||
runFriendlyIds: string[],
|
||||
projectId: string,
|
||||
environmentId: string
|
||||
): Promise<string> {
|
||||
const { id, friendlyId } = BulkActionId.generate();
|
||||
|
||||
await prisma.bulkActionGroup.create({
|
||||
data: {
|
||||
id,
|
||||
friendlyId,
|
||||
projectId,
|
||||
environmentId,
|
||||
name: "Dev session disconnect",
|
||||
type: BulkActionType.CANCEL,
|
||||
params: { runId: runFriendlyIds, finalizeRun: true },
|
||||
queryName: "bulk_action_v1",
|
||||
totalCount: runFriendlyIds.length,
|
||||
completionNotification: BulkActionNotificationType.NONE,
|
||||
},
|
||||
});
|
||||
|
||||
await commonWorker.enqueue({
|
||||
id: `processBulkAction-${id}`,
|
||||
job: "processBulkAction",
|
||||
payload: { bulkActionId: id },
|
||||
});
|
||||
|
||||
return friendlyId;
|
||||
}
|
||||
|
||||
export { action };
|
||||
@@ -138,11 +138,13 @@ export class BulkActionService extends BaseService {
|
||||
}
|
||||
|
||||
// 2. Parse the params
|
||||
const rawParams = group.params && typeof group.params === "object" ? group.params : {};
|
||||
const finalizeRun = "finalizeRun" in rawParams && (rawParams as any).finalizeRun === true;
|
||||
const filters = parseRunListInputOptions({
|
||||
organizationId: group.project.organizationId,
|
||||
projectId: group.projectId,
|
||||
environmentId: group.environmentId,
|
||||
...(group.params && typeof group.params === "object" ? group.params : {}),
|
||||
...rawParams,
|
||||
});
|
||||
|
||||
const runsRepository = new RunsRepository({
|
||||
@@ -199,6 +201,7 @@ export class BulkActionService extends BaseService {
|
||||
cancelService.call(run, {
|
||||
reason: `Bulk action ${group.friendlyId} cancelled run`,
|
||||
bulkActionId: bulkActionId,
|
||||
finalizeRun,
|
||||
})
|
||||
);
|
||||
if (error) {
|
||||
|
||||
@@ -8,6 +8,8 @@ export type CancelTaskRunServiceOptions = {
|
||||
cancelAttempts?: boolean;
|
||||
cancelledAt?: Date;
|
||||
bulkActionId?: string;
|
||||
/** Skip PENDING_CANCEL and finalize immediately (use when the worker is known to be dead). */
|
||||
finalizeRun?: boolean;
|
||||
};
|
||||
|
||||
type CancelTaskRunServiceResult = {
|
||||
@@ -57,6 +59,7 @@ export class CancelTaskRunService extends BaseService {
|
||||
runId: taskRun.id,
|
||||
completedAt: options?.cancelledAt,
|
||||
reason: options?.reason,
|
||||
finalizeRun: options?.finalizeRun,
|
||||
bulkActionId: options?.bulkActionId,
|
||||
tx: this._prisma,
|
||||
});
|
||||
|
||||
@@ -1436,35 +1436,39 @@ export class RunAttemptSystem {
|
||||
});
|
||||
|
||||
//if executing, we need to message the worker to cancel the run and put it into `PENDING_CANCEL` status
|
||||
//unless finalizeRun is true (worker is known to be dead), in which case skip straight to FINISHED
|
||||
if (
|
||||
isExecuting(latestSnapshot.executionStatus) ||
|
||||
isPendingExecuting(latestSnapshot.executionStatus)
|
||||
) {
|
||||
const newSnapshot = await this.executionSnapshotSystem.createExecutionSnapshot(prisma, {
|
||||
run,
|
||||
snapshot: {
|
||||
executionStatus: "PENDING_CANCEL",
|
||||
description: "Run was cancelled",
|
||||
},
|
||||
previousSnapshotId: latestSnapshot.id,
|
||||
environmentId: latestSnapshot.environmentId,
|
||||
environmentType: latestSnapshot.environmentType,
|
||||
projectId: latestSnapshot.projectId,
|
||||
organizationId: latestSnapshot.organizationId,
|
||||
workerId,
|
||||
runnerId,
|
||||
});
|
||||
if (!finalizeRun) {
|
||||
const newSnapshot = await this.executionSnapshotSystem.createExecutionSnapshot(prisma, {
|
||||
run,
|
||||
snapshot: {
|
||||
executionStatus: "PENDING_CANCEL",
|
||||
description: "Run was cancelled",
|
||||
},
|
||||
previousSnapshotId: latestSnapshot.id,
|
||||
environmentId: latestSnapshot.environmentId,
|
||||
environmentType: latestSnapshot.environmentType,
|
||||
projectId: latestSnapshot.projectId,
|
||||
organizationId: latestSnapshot.organizationId,
|
||||
workerId,
|
||||
runnerId,
|
||||
});
|
||||
|
||||
//the worker needs to be notified so it can kill the run and complete the attempt
|
||||
await sendNotificationToWorker({
|
||||
runId,
|
||||
snapshot: newSnapshot,
|
||||
eventBus: this.$.eventBus,
|
||||
});
|
||||
return {
|
||||
alreadyFinished: false,
|
||||
...executionResultFromSnapshot(newSnapshot),
|
||||
};
|
||||
//the worker needs to be notified so it can kill the run and complete the attempt
|
||||
await sendNotificationToWorker({
|
||||
runId,
|
||||
snapshot: newSnapshot,
|
||||
eventBus: this.$.eventBus,
|
||||
});
|
||||
return {
|
||||
alreadyFinished: false,
|
||||
...executionResultFromSnapshot(newSnapshot),
|
||||
};
|
||||
}
|
||||
// finalizeRun is true — fall through to finish the run immediately
|
||||
}
|
||||
|
||||
//not executing, so we will actually finish the run
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @trigger.dev/build
|
||||
|
||||
## 4.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Add syncSupabaseEnvVars to pull database connection strings and save them as trigger.dev environment variables ([#3152](https://github.com/triggerdotdev/trigger.dev/pull/3152))
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@4.4.3`
|
||||
|
||||
## 4.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/build",
|
||||
"version": "4.4.2",
|
||||
"version": "4.4.3",
|
||||
"description": "trigger.dev build extensions",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
@@ -78,7 +78,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/config": "^6.10.0",
|
||||
"@trigger.dev/core": "workspace:4.4.2",
|
||||
"@trigger.dev/core": "workspace:4.4.3",
|
||||
"mlly": "^1.7.1",
|
||||
"pkg-types": "^1.1.3",
|
||||
"resolve": "^1.22.8",
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# trigger.dev
|
||||
|
||||
## 4.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Auto-cancel in-flight dev runs when the CLI exits, using a detached watchdog process that survives pnpm SIGKILL ([#3191](https://github.com/triggerdotdev/trigger.dev/pull/3191))
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@4.4.3`
|
||||
- `@trigger.dev/build@4.4.3`
|
||||
- `@trigger.dev/schema-to-json@4.4.3`
|
||||
|
||||
## 4.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trigger.dev",
|
||||
"version": "4.4.2",
|
||||
"version": "4.4.3",
|
||||
"description": "A Command-Line Interface for Trigger.dev projects",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -93,9 +93,9 @@
|
||||
"@opentelemetry/sdk-trace-node": "2.0.1",
|
||||
"@opentelemetry/semantic-conventions": "1.36.0",
|
||||
"@s2-dev/streamstore": "^0.22.5",
|
||||
"@trigger.dev/build": "workspace:4.4.2",
|
||||
"@trigger.dev/core": "workspace:4.4.2",
|
||||
"@trigger.dev/schema-to-json": "workspace:4.4.2",
|
||||
"@trigger.dev/build": "workspace:4.4.3",
|
||||
"@trigger.dev/core": "workspace:4.4.3",
|
||||
"@trigger.dev/schema-to-json": "workspace:4.4.3",
|
||||
"ansi-escapes": "^7.0.0",
|
||||
"braces": "^3.0.3",
|
||||
"c12": "^1.11.1",
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
DevConfigResponseBody,
|
||||
DevDequeueRequestBody,
|
||||
DevDequeueResponseBody,
|
||||
DevDisconnectRequestBody,
|
||||
DevDisconnectResponseBody,
|
||||
EnvironmentVariableResponseBody,
|
||||
FailDeploymentRequestBody,
|
||||
FailDeploymentResponseBody,
|
||||
@@ -557,6 +559,7 @@ export class CliApiClient {
|
||||
heartbeatRun: this.devHeartbeatRun.bind(this),
|
||||
startRunAttempt: this.devStartRunAttempt.bind(this),
|
||||
completeRunAttempt: this.devCompleteRunAttempt.bind(this),
|
||||
disconnect: this.devDisconnect.bind(this),
|
||||
setEngineURL: this.setEngineURL.bind(this),
|
||||
} as const;
|
||||
}
|
||||
@@ -681,6 +684,23 @@ export class CliApiClient {
|
||||
return eventSource;
|
||||
}
|
||||
|
||||
private async devDisconnect(
|
||||
body: DevDisconnectRequestBody
|
||||
): Promise<ApiResult<DevDisconnectResponseBody>> {
|
||||
if (!this.accessToken) {
|
||||
throw new Error("devDisconnect: No access token");
|
||||
}
|
||||
|
||||
return wrapZodFetch(DevDisconnectResponseBody, `${this.engineURL}/engine/v1/dev/disconnect`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.accessToken}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
private async devDequeue(
|
||||
body: DevDequeueRequestBody
|
||||
): Promise<ApiResult<DevDequeueResponseBody>> {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { setTimeout as awaitTimeout } from "node:timers/promises";
|
||||
import {
|
||||
BuildManifest,
|
||||
@@ -71,6 +75,11 @@ class DevSupervisor implements WorkerRuntime {
|
||||
private runLimiter?: ReturnType<typeof pLimit>;
|
||||
private taskRunProcessPool?: TaskRunProcessPool;
|
||||
|
||||
/** Detached watchdog process that cancels runs if the CLI is killed */
|
||||
private watchdogProcess?: ChildProcess;
|
||||
private activeRunsPath?: string;
|
||||
private watchdogPidPath?: string;
|
||||
|
||||
constructor(public readonly options: WorkerRuntimeOptions) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
@@ -138,25 +147,39 @@ class DevSupervisor implements WorkerRuntime {
|
||||
//start an SSE connection for presence
|
||||
this.disconnectPresence = await this.#startPresenceConnection();
|
||||
|
||||
// Handle SIGTERM to gracefully stop all run controllers
|
||||
// Handle SIGTERM/SIGINT to gracefully stop all run controllers
|
||||
process.on("SIGTERM", this.#handleSigterm);
|
||||
process.on("SIGINT", this.#handleSigterm);
|
||||
|
||||
// Spawn detached watchdog to cancel runs if CLI is killed (e.g. pnpm SIGKILL)
|
||||
this.#spawnWatchdog();
|
||||
|
||||
//start dequeuing
|
||||
await this.#dequeueRuns();
|
||||
}
|
||||
|
||||
#handleSigterm = async () => {
|
||||
logger.debug("[DevSupervisor] Received SIGTERM, stopping all run controllers");
|
||||
logger.debug("[DevSupervisor] Received SIGTERM/SIGINT, stopping all run controllers");
|
||||
|
||||
const stopPromises = Array.from(this.runControllers.values()).map((controller) =>
|
||||
controller.stop()
|
||||
);
|
||||
await this.shutdown();
|
||||
|
||||
await Promise.allSettled(stopPromises);
|
||||
// Must exit explicitly since registering a custom SIGINT handler
|
||||
// overrides Node's default process termination behavior.
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
process.off("SIGTERM", this.#handleSigterm);
|
||||
process.off("SIGINT", this.#handleSigterm);
|
||||
|
||||
// Stop all local run controllers first so active-runs.json is up-to-date
|
||||
const stopPromises = Array.from(this.runControllers.values()).map((controller) =>
|
||||
controller.stop()
|
||||
);
|
||||
await Promise.allSettled(stopPromises);
|
||||
|
||||
// Kill watchdog on clean shutdown — no disconnect needed since runs are stopped locally
|
||||
this.#killWatchdog();
|
||||
|
||||
this.disconnectPresence?.();
|
||||
try {
|
||||
@@ -177,6 +200,107 @@ class DevSupervisor implements WorkerRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
#spawnWatchdog() {
|
||||
const triggerDir = join(this.options.config.workingDir, ".trigger");
|
||||
if (!existsSync(triggerDir)) {
|
||||
mkdirSync(triggerDir, { recursive: true });
|
||||
}
|
||||
|
||||
this.activeRunsPath = join(triggerDir, "active-runs.json");
|
||||
this.watchdogPidPath = join(triggerDir, "watchdog.pid");
|
||||
|
||||
// Write empty active-runs file
|
||||
this.#updateActiveRunsFile();
|
||||
|
||||
// Resolve the compiled watchdog script path relative to this file
|
||||
const thisDir = fileURLToPath(new URL(".", import.meta.url));
|
||||
const watchdogScript = join(thisDir, "devWatchdog.js");
|
||||
|
||||
if (!existsSync(watchdogScript)) {
|
||||
logger.debug("[DevSupervisor] Watchdog script not found, skipping", { watchdogScript });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.watchdogProcess = spawn(process.execPath, [watchdogScript], {
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
env: {
|
||||
...process.env,
|
||||
WATCHDOG_PARENT_PID: process.pid.toString(),
|
||||
WATCHDOG_API_URL: this.config?.engineUrl ?? this.options.client.apiURL,
|
||||
WATCHDOG_API_KEY: this.options.client.accessToken ?? "",
|
||||
WATCHDOG_ACTIVE_RUNS: this.activeRunsPath,
|
||||
WATCHDOG_PID_FILE: this.watchdogPidPath,
|
||||
},
|
||||
});
|
||||
|
||||
this.watchdogProcess.unref();
|
||||
|
||||
logger.debug("[DevSupervisor] Spawned watchdog", {
|
||||
watchdogPid: this.watchdogProcess.pid,
|
||||
parentPid: process.pid,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.debug("[DevSupervisor] Failed to spawn watchdog", { error });
|
||||
}
|
||||
}
|
||||
|
||||
#killWatchdog() {
|
||||
const knownPid = this.watchdogProcess?.pid;
|
||||
|
||||
if (knownPid) {
|
||||
try {
|
||||
process.kill(knownPid, "SIGTERM");
|
||||
} catch {
|
||||
// Already dead
|
||||
}
|
||||
this.watchdogProcess = undefined;
|
||||
}
|
||||
|
||||
// Fallback: try via PID file, but only if the PID matches our spawned watchdog
|
||||
// to avoid killing an unrelated process that reused a stale PID
|
||||
if (this.watchdogPidPath) {
|
||||
try {
|
||||
const content = readFileSync(this.watchdogPidPath, "utf8");
|
||||
const prefix = "trigger-watchdog:";
|
||||
if (content.startsWith(prefix)) {
|
||||
const pid = parseInt(content.slice(prefix.length), 10);
|
||||
if (pid && (!knownPid || pid === knownPid)) {
|
||||
process.kill(pid, "SIGTERM");
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Already dead or no file
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up files
|
||||
try {
|
||||
if (this.activeRunsPath) unlinkSync(this.activeRunsPath);
|
||||
} catch {}
|
||||
try {
|
||||
if (this.watchdogPidPath) unlinkSync(this.watchdogPidPath);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
#updateActiveRunsFile() {
|
||||
if (!this.activeRunsPath) return;
|
||||
|
||||
try {
|
||||
const data = {
|
||||
parentPid: process.pid,
|
||||
runFriendlyIds: Array.from(this.runControllers.keys()),
|
||||
};
|
||||
// Atomic write: write to temp file then rename to avoid corrupt reads
|
||||
const tmpPath = this.activeRunsPath + ".tmp";
|
||||
writeFileSync(tmpPath, JSON.stringify(data));
|
||||
renameSync(tmpPath, this.activeRunsPath);
|
||||
} catch (error) {
|
||||
logger.debug("[DevSupervisor] Failed to update active-runs file", { error });
|
||||
}
|
||||
}
|
||||
|
||||
async initializeWorker(
|
||||
manifest: BuildManifest,
|
||||
metafile: Metafile,
|
||||
@@ -386,6 +510,7 @@ class DevSupervisor implements WorkerRuntime {
|
||||
//stop the run controller, and remove it
|
||||
runController?.stop();
|
||||
this.runControllers.delete(message.run.friendlyId);
|
||||
this.#updateActiveRunsFile();
|
||||
this.#unsubscribeFromRunNotifications(message.run.friendlyId);
|
||||
|
||||
//stop the worker if it is deprecated and there are no more runs
|
||||
@@ -402,6 +527,7 @@ class DevSupervisor implements WorkerRuntime {
|
||||
});
|
||||
|
||||
this.runControllers.set(message.run.friendlyId, runController);
|
||||
this.#updateActiveRunsFile();
|
||||
|
||||
if (this.runLimiter) {
|
||||
this.runLimiter(() => runController.start(message)).then(() => {
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Dev Watchdog — a detached process that cancels in-flight runs when the dev CLI exits.
|
||||
*
|
||||
* Spawned by the dev CLI with `detached: true, stdio: "ignore", unref()`.
|
||||
* Survives when pnpm sends SIGKILL to the CLI process tree.
|
||||
*
|
||||
* Lifecycle:
|
||||
* 1. CLI spawns this script, passing config via env vars
|
||||
* 2. Writes PID file for single-instance guarantee
|
||||
* 3. Polls parent PID to detect when the CLI exits
|
||||
* 4. On parent death: reads active-runs file → calls disconnect endpoint → exits
|
||||
*
|
||||
* Environment variables:
|
||||
* WATCHDOG_PARENT_PID - The PID of the parent dev CLI process
|
||||
* WATCHDOG_API_URL - The Trigger.dev API/engine URL
|
||||
* WATCHDOG_API_KEY - The API key for authentication
|
||||
* WATCHDOG_ACTIVE_RUNS - Path to the active-runs JSON file
|
||||
* WATCHDOG_PID_FILE - Path to write the watchdog PID file
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
const POLL_INTERVAL_MS = 1000;
|
||||
|
||||
// Safety timeout: if the watchdog has been running for 24 hours, exit regardless.
|
||||
// Prevents zombie watchdogs from PID reuse scenarios.
|
||||
const MAX_LIFETIME_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
const parentPid = parseInt(process.env.WATCHDOG_PARENT_PID!, 10);
|
||||
const apiUrl = process.env.WATCHDOG_API_URL!;
|
||||
const apiKey = process.env.WATCHDOG_API_KEY!;
|
||||
const activeRunsPath = process.env.WATCHDOG_ACTIVE_RUNS!;
|
||||
const pidFilePath = process.env.WATCHDOG_PID_FILE!;
|
||||
|
||||
if (!parentPid || !apiUrl || !apiKey || !activeRunsPath || !pidFilePath) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
const dir = dirname(pidFilePath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const PID_FILE_PREFIX = "trigger-watchdog:";
|
||||
|
||||
// Single instance: kill any existing watchdog
|
||||
try {
|
||||
const pidFileContent = readFileSync(pidFilePath, "utf8");
|
||||
if (pidFileContent.startsWith(PID_FILE_PREFIX)) {
|
||||
const existingPid = parseInt(pidFileContent.slice(PID_FILE_PREFIX.length), 10);
|
||||
if (existingPid && existingPid !== process.pid) {
|
||||
try {
|
||||
process.kill(existingPid, 0); // Check if alive
|
||||
process.kill(existingPid, "SIGTERM"); // Kill it
|
||||
} catch {
|
||||
// Already dead
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// No PID file or invalid format
|
||||
}
|
||||
|
||||
// Write our PID with prefix so we can verify ownership later
|
||||
writeFileSync(pidFilePath, `${PID_FILE_PREFIX}${process.pid}`);
|
||||
|
||||
function cleanup() {
|
||||
try {
|
||||
unlinkSync(pidFilePath);
|
||||
} catch {}
|
||||
try {
|
||||
unlinkSync(activeRunsPath);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function isParentAlive(): boolean {
|
||||
try {
|
||||
process.kill(parentPid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function readActiveRuns(): string[] {
|
||||
try {
|
||||
const data = JSON.parse(readFileSync(activeRunsPath, "utf8"));
|
||||
return data.runFriendlyIds ?? [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function callDisconnect(runFriendlyIds: string[]): Promise<void> {
|
||||
const response = await fetch(`${apiUrl}/engine/v1/dev/disconnect`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({ runFriendlyIds }),
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Disconnect failed with status ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_DISCONNECT_ATTEMPTS = 5;
|
||||
const INITIAL_BACKOFF_MS = 500;
|
||||
|
||||
async function onParentDied(): Promise<void> {
|
||||
const runFriendlyIds = readActiveRuns();
|
||||
|
||||
if (runFriendlyIds.length > 0) {
|
||||
for (let attempt = 0; attempt < MAX_DISCONNECT_ATTEMPTS; attempt++) {
|
||||
try {
|
||||
await callDisconnect(runFriendlyIds);
|
||||
break;
|
||||
} catch {
|
||||
if (attempt < MAX_DISCONNECT_ATTEMPTS - 1) {
|
||||
const backoff = INITIAL_BACKOFF_MS * 2 ** attempt;
|
||||
await new Promise((resolve) => setTimeout(resolve, backoff));
|
||||
}
|
||||
// Final attempt failed — runs will eventually time out via heartbeat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanup();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Guard against overlapping async callbacks
|
||||
let checking = false;
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
if (checking) return;
|
||||
checking = true;
|
||||
|
||||
try {
|
||||
if (!isParentAlive()) {
|
||||
clearInterval(interval);
|
||||
clearTimeout(lifetimeTimeout);
|
||||
await onParentDied();
|
||||
}
|
||||
} finally {
|
||||
checking = false;
|
||||
}
|
||||
}, POLL_INTERVAL_MS);
|
||||
|
||||
// Safety timeout: exit after MAX_LIFETIME_MS to prevent zombie watchdogs
|
||||
const lifetimeTimeout = setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
cleanup();
|
||||
process.exit(0);
|
||||
}, MAX_LIFETIME_MS);
|
||||
|
||||
// Unref the timeout so it doesn't keep the process alive if the interval is cleared
|
||||
lifetimeTimeout.unref();
|
||||
|
||||
// Clean exit on any termination signal
|
||||
function handleSignal() {
|
||||
clearInterval(interval);
|
||||
clearTimeout(lifetimeTimeout);
|
||||
cleanup();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on("SIGTERM", handleSignal);
|
||||
process.on("SIGINT", handleSignal);
|
||||
@@ -1,5 +1,11 @@
|
||||
# internal-platform
|
||||
|
||||
## 4.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Auto-cancel in-flight dev runs when the CLI exits, using a detached watchdog process that survives pnpm SIGKILL ([#3191](https://github.com/triggerdotdev/trigger.dev/pull/3191))
|
||||
|
||||
## 4.4.2
|
||||
|
||||
## 4.4.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/core",
|
||||
"version": "4.4.2",
|
||||
"version": "4.4.3",
|
||||
"description": "Core code used across the Trigger.dev SDK and platform",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -838,6 +838,17 @@ export const DevDequeueResponseBody = z.object({
|
||||
});
|
||||
export type DevDequeueResponseBody = z.infer<typeof DevDequeueResponseBody>;
|
||||
|
||||
export const DevDisconnectRequestBody = z.object({
|
||||
runFriendlyIds: z.string().array(),
|
||||
});
|
||||
export type DevDisconnectRequestBody = z.infer<typeof DevDisconnectRequestBody>;
|
||||
|
||||
export const DevDisconnectResponseBody = z.object({
|
||||
cancelled: z.number(),
|
||||
bulkActionId: z.string().optional(),
|
||||
});
|
||||
export type DevDisconnectResponseBody = z.infer<typeof DevDisconnectResponseBody>;
|
||||
|
||||
export type CreateUploadPayloadUrlResponseBody = z.infer<typeof CreateUploadPayloadUrlResponseBody>;
|
||||
|
||||
export const ReplayRunResponse = z.object({
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# @trigger.dev/python
|
||||
|
||||
## 4.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@4.4.3`
|
||||
- `@trigger.dev/build@4.4.3`
|
||||
- `@trigger.dev/sdk@4.4.3`
|
||||
|
||||
## 4.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/python",
|
||||
"version": "4.4.2",
|
||||
"version": "4.4.3",
|
||||
"description": "Python runtime and build extension for Trigger.dev",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
@@ -45,7 +45,7 @@
|
||||
"check-exports": "attw --pack ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/core": "workspace:4.4.2",
|
||||
"@trigger.dev/core": "workspace:4.4.3",
|
||||
"tinyexec": "^0.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -56,12 +56,12 @@
|
||||
"tsx": "4.17.0",
|
||||
"esbuild": "^0.23.0",
|
||||
"@arethetypeswrong/cli": "^0.15.4",
|
||||
"@trigger.dev/build": "workspace:4.4.2",
|
||||
"@trigger.dev/sdk": "workspace:4.4.2"
|
||||
"@trigger.dev/build": "workspace:4.4.3",
|
||||
"@trigger.dev/sdk": "workspace:4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@trigger.dev/sdk": "workspace:^4.4.2",
|
||||
"@trigger.dev/build": "workspace:^4.4.2"
|
||||
"@trigger.dev/sdk": "workspace:^4.4.3",
|
||||
"@trigger.dev/build": "workspace:^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.0"
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @trigger.dev/react-hooks
|
||||
|
||||
## 4.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@4.4.3`
|
||||
|
||||
## 4.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/react-hooks",
|
||||
"version": "4.4.2",
|
||||
"version": "4.4.3",
|
||||
"description": "trigger.dev react hooks",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
@@ -37,7 +37,7 @@
|
||||
"check-exports": "attw --pack ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/core": "workspace:^4.4.2",
|
||||
"@trigger.dev/core": "workspace:^4.4.3",
|
||||
"swr": "^2.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @trigger.dev/redis-worker
|
||||
|
||||
## 4.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@4.4.3`
|
||||
|
||||
## 4.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/redis-worker",
|
||||
"version": "4.4.2",
|
||||
"version": "4.4.3",
|
||||
"description": "Redis worker for trigger.dev",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
@@ -23,7 +23,7 @@
|
||||
"test": "vitest --sequence.concurrent=false --no-file-parallelism"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/core": "workspace:4.4.2",
|
||||
"@trigger.dev/core": "workspace:4.4.3",
|
||||
"lodash.omit": "^4.5.0",
|
||||
"nanoid": "^5.0.7",
|
||||
"p-limit": "^6.2.0",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @trigger.dev/rsc
|
||||
|
||||
## 4.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@4.4.3`
|
||||
|
||||
## 4.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/rsc",
|
||||
"version": "4.4.2",
|
||||
"version": "4.4.3",
|
||||
"description": "trigger.dev rsc",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
@@ -37,14 +37,14 @@
|
||||
"check-exports": "attw --pack ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/core": "workspace:^4.4.2",
|
||||
"@trigger.dev/core": "workspace:^4.4.3",
|
||||
"mlly": "^1.7.1",
|
||||
"react": "19.0.0-rc.1",
|
||||
"react-dom": "19.0.0-rc.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arethetypeswrong/cli": "^0.15.4",
|
||||
"@trigger.dev/build": "workspace:^4.4.2",
|
||||
"@trigger.dev/build": "workspace:^4.4.3",
|
||||
"@types/node": "^20.14.14",
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @trigger.dev/schema-to-json
|
||||
|
||||
## 4.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@4.4.3`
|
||||
|
||||
## 4.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/schema-to-json",
|
||||
"version": "4.4.2",
|
||||
"version": "4.4.3",
|
||||
"description": "Convert various schema validation libraries to JSON Schema",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @trigger.dev/sdk
|
||||
|
||||
## 4.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@4.4.3`
|
||||
|
||||
## 4.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/sdk",
|
||||
"version": "4.4.2",
|
||||
"version": "4.4.3",
|
||||
"description": "trigger.dev Node.JS SDK",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
@@ -52,7 +52,7 @@
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"@opentelemetry/semantic-conventions": "1.36.0",
|
||||
"@trigger.dev/core": "workspace:4.4.2",
|
||||
"@trigger.dev/core": "workspace:4.4.3",
|
||||
"chalk": "^5.2.0",
|
||||
"cronstrue": "^2.21.0",
|
||||
"debug": "^4.3.4",
|
||||
|
||||
Generated
+12
-12
@@ -1382,7 +1382,7 @@ importers:
|
||||
specifier: ^6.10.0
|
||||
version: 6.19.0(magicast@0.3.5)
|
||||
'@trigger.dev/core':
|
||||
specifier: workspace:4.4.2
|
||||
specifier: workspace:4.4.3
|
||||
version: link:../core
|
||||
mlly:
|
||||
specifier: ^1.7.1
|
||||
@@ -1458,13 +1458,13 @@ importers:
|
||||
specifier: ^0.22.5
|
||||
version: 0.22.5(supports-color@10.0.0)
|
||||
'@trigger.dev/build':
|
||||
specifier: workspace:4.4.2
|
||||
specifier: workspace:4.4.3
|
||||
version: link:../build
|
||||
'@trigger.dev/core':
|
||||
specifier: workspace:4.4.2
|
||||
specifier: workspace:4.4.3
|
||||
version: link:../core
|
||||
'@trigger.dev/schema-to-json':
|
||||
specifier: workspace:4.4.2
|
||||
specifier: workspace:4.4.3
|
||||
version: link:../schema-to-json
|
||||
ansi-escapes:
|
||||
specifier: ^7.0.0
|
||||
@@ -1832,7 +1832,7 @@ importers:
|
||||
packages/python:
|
||||
dependencies:
|
||||
'@trigger.dev/core':
|
||||
specifier: workspace:4.4.2
|
||||
specifier: workspace:4.4.3
|
||||
version: link:../core
|
||||
tinyexec:
|
||||
specifier: ^0.3.2
|
||||
@@ -1842,10 +1842,10 @@ importers:
|
||||
specifier: ^0.15.4
|
||||
version: 0.15.4
|
||||
'@trigger.dev/build':
|
||||
specifier: workspace:4.4.2
|
||||
specifier: workspace:4.4.3
|
||||
version: link:../build
|
||||
'@trigger.dev/sdk':
|
||||
specifier: workspace:4.4.2
|
||||
specifier: workspace:4.4.3
|
||||
version: link:../trigger-sdk
|
||||
'@types/node':
|
||||
specifier: 20.14.14
|
||||
@@ -1869,7 +1869,7 @@ importers:
|
||||
packages/react-hooks:
|
||||
dependencies:
|
||||
'@trigger.dev/core':
|
||||
specifier: workspace:^4.4.2
|
||||
specifier: workspace:^4.4.3
|
||||
version: link:../core
|
||||
react:
|
||||
specifier: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
@@ -1903,7 +1903,7 @@ importers:
|
||||
packages/redis-worker:
|
||||
dependencies:
|
||||
'@trigger.dev/core':
|
||||
specifier: workspace:4.4.2
|
||||
specifier: workspace:4.4.3
|
||||
version: link:../core
|
||||
cron-parser:
|
||||
specifier: ^4.9.0
|
||||
@@ -1952,7 +1952,7 @@ importers:
|
||||
packages/rsc:
|
||||
dependencies:
|
||||
'@trigger.dev/core':
|
||||
specifier: workspace:^4.4.2
|
||||
specifier: workspace:^4.4.3
|
||||
version: link:../core
|
||||
mlly:
|
||||
specifier: ^1.7.1
|
||||
@@ -1968,7 +1968,7 @@ importers:
|
||||
specifier: ^0.15.4
|
||||
version: 0.15.4
|
||||
'@trigger.dev/build':
|
||||
specifier: workspace:^4.4.2
|
||||
specifier: workspace:^4.4.3
|
||||
version: link:../build
|
||||
'@types/node':
|
||||
specifier: 20.14.14
|
||||
@@ -2044,7 +2044,7 @@ importers:
|
||||
specifier: 1.36.0
|
||||
version: 1.36.0
|
||||
'@trigger.dev/core':
|
||||
specifier: workspace:4.4.2
|
||||
specifier: workspace:4.4.3
|
||||
version: link:../core
|
||||
chalk:
|
||||
specifier: ^5.2.0
|
||||
|
||||
Reference in New Issue
Block a user