diff --git a/apps/webapp/app/components/Feedback.tsx b/apps/webapp/app/components/Feedback.tsx index 3abf5d862..f7ad1b50f 100644 --- a/apps/webapp/app/components/Feedback.tsx +++ b/apps/webapp/app/components/Feedback.tsx @@ -2,10 +2,10 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { ChevronRightIcon } from "@heroicons/react/24/solid"; import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react"; -import { DiscordIcon } from "@trigger.dev/companyicons"; +import { DiscordIcon, GitHubLightIcon } from "@trigger.dev/companyicons"; import { ReactNode, useState } from "react"; import { FeedbackType, feedbackTypeLabel, schema } from "~/routes/resources.feedback"; -import { Button } from "./primitives/Buttons"; +import { Button, LinkButton } from "./primitives/Buttons"; import { Fieldset } from "./primitives/Fieldset"; import { FormButtons } from "./primitives/FormButtons"; import { FormError } from "./primitives/FormError"; @@ -21,8 +21,12 @@ import { SelectTrigger, SelectValue, } from "./primitives/Select"; -import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "./primitives/Sheet"; +import { Sheet, SheetBody, SheetContent, SheetTrigger } from "./primitives/Sheet"; import { TextArea } from "./primitives/TextArea"; +import { cn } from "~/utils/cn"; +import { BookOpenIcon } from "@heroicons/react/20/solid"; +import { ActivityIcon, HeartPulseIcon } from "lucide-react"; +import { docsPath } from "~/utils/pathBuilder"; type FeedbackProps = { button: ReactNode; @@ -56,12 +60,22 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) { return ( {button} - + - - How can we help? + Get help from the community + + The quickest way to get help and feedback or to provide advice to others is to join our + Discord. + +
+ + +
+
+ Send us an email + We read every message and respond quickly.
-
+
@@ -86,46 +100,92 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) { {message.error} {form.error} -
- - We read every message and respond quickly. - +
- Send + } />
+
+ Troubleshooting + + If you're having trouble, check out our documentation or the Trigger.dev Status page. + +
+ + Docs + + + v3 Docs (Developer preview) + + + Trigger.dev Status + +
); } -function DiscordBanner() { +function DiscordBanner({ className }: { className?: string }) { return (
- Join the Trigger.dev -
- Discord community + Join our Discord community
- + Get help or answer questions from the Trigger.dev community.
- + +
+ ); +} + +function GitHubDiscussionsBanner({ className }: { className?: string }) { + return ( + +
+ + + View our GitHub Discussions + + + Post your questions, feedback, and feature requests on GitHub. + +
+
); } diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 5f491e50a..70b96271e 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -14,6 +14,7 @@ import { UserGroupIcon, UserPlusIcon } from "@heroicons/react/24/solid"; import { useNavigation } from "@remix-run/react"; import { DiscordIcon, SlackIcon } from "@trigger.dev/companyicons"; import { Fragment, useEffect, useRef, useState } from "react"; +import { TaskIcon } from "~/assets/icons/TaskIcon"; import { useFeatures } from "~/hooks/useFeatures"; import { MatchedOrganization } from "~/hooks/useOrganizations"; import { MatchedProject } from "~/hooks/useProject"; @@ -54,6 +55,7 @@ import { LogoIcon } from "../LogoIcon"; import { StepContentContainer } from "../StepContentContainer"; import { UserProfilePhoto } from "../UserProfilePhoto"; import { FreePlanUsage } from "../billing/FreePlanUsage"; +import { Badge } from "../primitives/Badge"; import { Button } from "../primitives/Buttons"; import { ClipboardField } from "../primitives/ClipboardField"; import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "../primitives/Dialog"; @@ -70,8 +72,6 @@ import { import { StepNumber } from "../primitives/StepNumber"; import { SideMenuHeader } from "./SideMenuHeader"; import { MenuCount, SideMenuItem } from "./SideMenuItem"; -import { Badge } from "../primitives/Badge"; -import { TaskIcon } from "~/assets/icons/TaskIcon"; type SideMenuUser = Pick & { isImpersonating: boolean }; type SideMenuProject = Pick< @@ -269,7 +269,6 @@ export function SideMenu({ user, project, organization, organizations }: SideMen target="_blank" /> )} - - - - Help & Feedback - - } - /> + {project.version === "V2" ? ( + + Help & Feedback + + } + /> + ) : ( + + Give feedback on v3 + + } + /> + )} {currentPlan && !currentPlan.subscription?.isPaying && currentPlan.usage.runCountCap && ( diff --git a/apps/webapp/app/components/primitives/Select.tsx b/apps/webapp/app/components/primitives/Select.tsx index d69459477..61ea34133 100644 --- a/apps/webapp/app/components/primitives/Select.tsx +++ b/apps/webapp/app/components/primitives/Select.tsx @@ -66,7 +66,7 @@ const SelectContent = React.forwardRef< > , - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - - - +type SelectItemProps = React.ComponentPropsWithoutRef & { + contentClassName?: string; +}; - {children} - -)); +const SelectItem = React.forwardRef, SelectItemProps>( + ({ className, children, contentClassName, ...props }, ref) => ( + + + + + + + + {children} + + ) +); SelectItem.displayName = SelectPrimitive.Item.displayName; const SelectSeparator = React.forwardRef< diff --git a/apps/webapp/app/components/primitives/Tooltip.tsx b/apps/webapp/app/components/primitives/Tooltip.tsx index fd0ded4a3..468bc1f2d 100644 --- a/apps/webapp/app/components/primitives/Tooltip.tsx +++ b/apps/webapp/app/components/primitives/Tooltip.tsx @@ -4,7 +4,7 @@ import { cn } from "~/utils/cn"; const variantClasses = { basic: - "bg-background-dimmed border border-charcoal-800 rounded-md px-3 py-1.5 text-sm text-text-bright shadow-md fade-in-50", + "bg-background-dimmed border border-charcoal-700 rounded-md px-3 py-1.5 text-sm text-text-bright shadow-md fade-in-50", dark: "bg-background-dimmed border border-grid-bright rounded px-3 py-2 text-sm text-text-bright shadow-md fade-in-50", }; @@ -55,6 +55,7 @@ function SimpleTooltip({ side, hidden, variant, + disableHoverableContent = false, className, }: { button: React.ReactNode; @@ -62,10 +63,11 @@ function SimpleTooltip({ side?: React.ComponentProps["side"]; hidden?: boolean; variant?: Variant; + disableHoverableContent?: boolean; className?: string; }) { return ( - + {button} - - {children} - - {content} - - - - ); -} - export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider, TooltipArrow, SimpleTooltip }; diff --git a/apps/webapp/app/components/runs/v3/DeploymentStatus.tsx b/apps/webapp/app/components/runs/v3/DeploymentStatus.tsx index 175b0d1f4..78e3f222e 100644 --- a/apps/webapp/app/components/runs/v3/DeploymentStatus.tsx +++ b/apps/webapp/app/components/runs/v3/DeploymentStatus.tsx @@ -1,16 +1,10 @@ import { - ArrowPathIcon, - BoltSlashIcon, - BugAntIcon, CheckCircleIcon, - ClockIcon, + ExclamationTriangleIcon, NoSymbolIcon, - PauseCircleIcon, - RectangleStackIcon, - ServerStackIcon, XCircleIcon, } from "@heroicons/react/20/solid"; -import { TaskRunStatus, WorkerDeploymentStatus } from "@trigger.dev/database"; +import { WorkerDeploymentStatus } from "@trigger.dev/database"; import { Spinner } from "~/components/primitives/Spinner"; import { cn } from "~/utils/cn"; @@ -53,6 +47,12 @@ export function DeploymentStatusIcon({ return ; case "FAILED": return ; + case "TIMED_OUT": + return ( + + ); default: { const _exhaustiveCheck: never = status; throw new Error(`Non-exhaustive match for value: ${status}`); @@ -66,6 +66,7 @@ export function deploymentStatusClassNameColor(status: WorkerDeploymentStatus): case "BUILDING": case "DEPLOYING": return "text-pending"; + case "TIMED_OUT": case "CANCELED": return "text-charcoal-500"; case "DEPLOYED": @@ -91,6 +92,8 @@ export function deploymentStatusTitle(status: WorkerDeploymentStatus): string { return "Deployed"; case "CANCELED": return "Canceled"; + case "TIMED_OUT": + return "Timed out"; case "FAILED": return "Failed"; default: { diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index f543e3d7d..2e28d379a 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -1,9 +1,15 @@ import { TrashIcon } from "@heroicons/react/20/solid"; import { useNavigate } from "@remix-run/react"; -import { RuntimeEnvironment, TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/database"; import type { TaskRunStatus as TaskRunStatusType } from "@trigger.dev/database"; +import { RuntimeEnvironment, TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/database"; import { useCallback } from "react"; import { z } from "zod"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "~/components/primitives/Tooltip"; import { useOptimisticLocation } from "~/hooks/useOptimisticLocation"; import { EnvironmentLabel } from "../../environments/EnvironmentLabel"; import { Button } from "../../primitives/Buttons"; @@ -17,9 +23,20 @@ import { SelectValue, } from "../../primitives/Select"; import { TimeFrameFilter } from "../TimeFrameFilter"; -import { TaskRunStatusCombo } from "./TaskRunStatus"; +import { TaskRunStatusCombo, descriptionForTaskRunStatus } from "./TaskRunStatus"; + +export const allTaskRunStatuses = [ + "PENDING", + "EXECUTING", + "RETRYING_AFTER_FAILURE", + "WAITING_TO_RESUME", + "COMPLETED_SUCCESSFULLY", + "CANCELED", + "COMPLETED_WITH_ERRORS", + "INTERRUPTED", + "SYSTEM_FAILURE", +] as TaskRunStatusType[]; -export const allTaskRunStatuses = Object.values(TaskRunStatus) as TaskRunStatusType[]; export const TaskAttemptStatus = z.nativeEnum(TaskRunStatus); export const TaskRunListSearchFilters = z.object({ @@ -151,10 +168,20 @@ export function RunsFilters({ possibleEnvironments, possibleTasks }: RunFiltersP diff --git a/apps/webapp/app/components/runs/v3/TaskRunAttemptStatus.tsx b/apps/webapp/app/components/runs/v3/TaskRunAttemptStatus.tsx index 5ca180655..a70f7ac5a 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunAttemptStatus.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunAttemptStatus.tsx @@ -8,6 +8,7 @@ import { } from "@heroicons/react/20/solid"; import type { TaskRunAttemptStatus as TaskRunAttemptStatusType } from "@trigger.dev/database"; import { TaskRunAttemptStatus } from "@trigger.dev/database"; +import { SnowflakeIcon } from "lucide-react"; import { Spinner } from "~/components/primitives/Spinner"; import { cn } from "~/utils/cn"; @@ -63,7 +64,7 @@ export function TaskRunAttemptStatusIcon({ case "EXECUTING": return ; case "PAUSED": - return ; + return ; case "FAILED": return ; case "CANCELED": @@ -90,7 +91,7 @@ export function runAttemptStatusClassNameColor(status: ExtendedTaskAttemptStatus case "EXECUTING": return "text-pending"; case "PAUSED": - return "text-amber-300"; + return "text-sky-300"; case "FAILED": return "text-error"; case "CANCELED": @@ -117,7 +118,7 @@ export function runAttemptStatusTitle(status: ExtendedTaskAttemptStatus | null): case "EXECUTING": return "Executing"; case "PAUSED": - return "Paused"; + return "Frozen"; case "FAILED": return "Failed"; case "CANCELED": diff --git a/apps/webapp/app/components/runs/v3/TaskRunStatus.tsx b/apps/webapp/app/components/runs/v3/TaskRunStatus.tsx index c4bc09988..3d7287a42 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunStatus.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunStatus.tsx @@ -10,19 +10,39 @@ import { XCircleIcon, } from "@heroicons/react/20/solid"; import { TaskRunStatus } from "@trigger.dev/database"; +import { SnowflakeIcon } from "lucide-react"; import { Spinner } from "~/components/primitives/Spinner"; import { cn } from "~/utils/cn"; +const taskRunStatusDescriptions: Record = { + PENDING: "Task is waiting to be executed", + EXECUTING: "Task is currently being executed", + RETRYING_AFTER_FAILURE: "Task is being reattempted after a failure", + WAITING_TO_RESUME: "Task has been frozen and is waiting to be resumed", + COMPLETED_SUCCESSFULLY: "Task has been successfully completed", + CANCELED: "Task has been canceled", + COMPLETED_WITH_ERRORS: "Task has failed with errors", + INTERRUPTED: "Task has failed because it was interrupted", + SYSTEM_FAILURE: "Task has failed due to a system failure", + PAUSED: "Task has been paused by the user", +}; + +export function descriptionForTaskRunStatus(status: TaskRunStatus): string { + return taskRunStatusDescriptions[status]; +} + export function TaskRunStatusCombo({ status, className, + iconClassName, }: { status: TaskRunStatus; className?: string; + iconClassName?: string; }) { return ( - + ); @@ -45,7 +65,7 @@ export function TaskRunStatusIcon({ case "EXECUTING": return ; case "WAITING_TO_RESUME": - return ; + return ; case "RETRYING_AFTER_FAILURE": return ; case "PAUSED": @@ -73,11 +93,10 @@ export function runStatusClassNameColor(status: TaskRunStatus): string { case "PENDING": return "text-charcoal-500"; case "EXECUTING": + case "RETRYING_AFTER_FAILURE": return "text-pending"; case "WAITING_TO_RESUME": - return "text-charcoal-500"; - case "RETRYING_AFTER_FAILURE": - return "text-charcoal-500"; + return "text-sky-300"; case "PAUSED": return "text-amber-300"; case "CANCELED": @@ -100,13 +119,13 @@ export function runStatusClassNameColor(status: TaskRunStatus): string { export function runStatusTitle(status: TaskRunStatus): string { switch (status) { case "PENDING": - return "Enqueued"; + return "Queued"; case "EXECUTING": return "Executing"; case "WAITING_TO_RESUME": - return "Waiting"; + return "Frozen"; case "RETRYING_AFTER_FAILURE": - return "Retrying"; + return "Reattempting"; case "PAUSED": return "Paused"; case "CANCELED": diff --git a/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts b/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts index 323ee8d42..81d71d244 100644 --- a/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts @@ -71,6 +71,7 @@ export class DeploymentPresenter { }, status: true, deployedAt: true, + createdAt: true, promotions: { select: { label: true, @@ -107,6 +108,7 @@ export class DeploymentPresenter { shortCode: deployment.shortCode, version: deployment.version, status: deployment.status, + createdAt: deployment.createdAt, deployedAt: deployment.deployedAt, tasks: deployment.worker?.tasks, label: deployment.promotions?.[0]?.label, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations/route.tsx index 78c5a2531..6a790b717 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations/route.tsx @@ -191,7 +191,7 @@ function PossibleIntegrationsList({ /> } - defaultValue="integration" + defaultValue="feature" /> Create an Integration diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.deployments.$deploymentParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.deployments.$deploymentParam/route.tsx index 92856accb..81d0ad437 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.deployments.$deploymentParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.deployments.$deploymentParam/route.tsx @@ -1,53 +1,32 @@ -import { CommandLineIcon, ServerIcon } from "@heroicons/react/20/solid"; import { useLocation } from "@remix-run/react"; import { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { TerminalIcon, TerminalSquareIcon } from "lucide-react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; -import { z } from "zod"; import { ExitIcon } from "~/assets/icons/ExitIcon"; -import { BlankstateInstructions } from "~/components/BlankstateInstructions"; import { UserAvatar } from "~/components/UserProfilePhoto"; import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; -import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Badge } from "~/components/primitives/Badge"; import { LinkButton } from "~/components/primitives/Buttons"; -import { DateTime, DateTimeAccurate } from "~/components/primitives/DateTime"; +import { DateTimeAccurate } from "~/components/primitives/DateTime"; import { Header2 } from "~/components/primitives/Headers"; -import { NavBar, PageTitle } from "~/components/primitives/PageHeader"; -import { PaginationControls } from "~/components/primitives/Pagination"; import { Paragraph } from "~/components/primitives/Paragraph"; import { Property, PropertyTable } from "~/components/primitives/PropertyTable"; -import { ResizablePanel, ResizablePanelGroup } from "~/components/primitives/Resizable"; import { Table, - TableBlankRow, TableBody, TableCell, - TableCellChevron, TableHeader, TableHeaderCell, TableRow, } from "~/components/primitives/Table"; -import { TextLink } from "~/components/primitives/TextLink"; import { DeploymentStatus } from "~/components/runs/v3/DeploymentStatus"; import { TaskFunctionName } from "~/components/runs/v3/TaskPath"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useUser } from "~/hooks/useUser"; -import { DeploymentListPresenter } from "~/presenters/v3/DeploymentListPresenter.server"; import { DeploymentPresenter } from "~/presenters/v3/DeploymentPresenter.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; -import { - ProjectParamSchema, - docsPath, - runParam, - v3DeploymentParams, - v3DeploymentPath, - v3DeploymentsPath, - v3RunPath, -} from "~/utils/pathBuilder"; -import { createSearchParams } from "~/utils/searchParams"; +import { v3DeploymentParams, v3DeploymentsPath } from "~/utils/pathBuilder"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); @@ -111,6 +90,11 @@ export default function Page() { {deployment.tasks ? deployment.tasks.length : "–"} + + + UTC + + {deployment.deployedAt ? ( diff --git a/apps/webapp/app/routes/resources.feedback.ts b/apps/webapp/app/routes/resources.feedback.ts index 110a4e813..abc3227aa 100644 --- a/apps/webapp/app/routes/resources.feedback.ts +++ b/apps/webapp/app/routes/resources.feedback.ts @@ -13,8 +13,8 @@ export const feedbackTypeLabel = { bug: "Bug report", feature: "Feature request", help: "Help me out", - integration: "Request an Integration", enterprise: "Enterprise enquiry", + "developer preview": "Developer preview feedback", }; export type FeedbackType = keyof typeof feedbackTypeLabel; diff --git a/apps/webapp/app/routes/storybook.loading-bar-divider/route.tsx b/apps/webapp/app/routes/storybook.loading-bar-divider/route.tsx new file mode 100644 index 000000000..ec29274c7 --- /dev/null +++ b/apps/webapp/app/routes/storybook.loading-bar-divider/route.tsx @@ -0,0 +1,11 @@ +import { LoadingBarDivider } from "~/components/primitives/LoadingBarDivider"; + +const isLoading = true; + +export default function Story() { + return ( +
+ +
+ ); +} diff --git a/apps/webapp/app/routes/storybook/route.tsx b/apps/webapp/app/routes/storybook/route.tsx index b7077a87e..4bf84c5c9 100644 --- a/apps/webapp/app/routes/storybook/route.tsx +++ b/apps/webapp/app/routes/storybook/route.tsx @@ -52,6 +52,10 @@ const stories: Story[] = [ name: "Inline code", slug: "inline-code", }, + { + name: "Loading bar divider", + slug: "loading-bar-divider", + }, { name: "NamedIcon", slug: "named-icon", diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index 954ed20cb..e75aea1a8 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -33,6 +33,7 @@ import { ResumeTaskService } from "./tasks/resumeTask.server"; import { ResumeTaskRunDependenciesService } from "~/v3/services/resumeTaskRunDependencies.server"; import { ResumeBatchRunService } from "~/v3/services/resumeBatchRun.server"; import { ResumeTaskDependencyService } from "~/v3/services/resumeTaskDependency.server"; +import { TimeoutDeploymentService } from "~/v3/services/timeoutDeployment.server"; const workerCatalog = { indexEndpoint: z.object({ @@ -121,6 +122,11 @@ const workerCatalog = { dependencyId: z.string(), sourceTaskAttemptId: z.string(), }), + "v3.timeoutDeployment": z.object({ + deploymentId: z.string(), + fromStatus: z.string(), + errorMessage: z.string(), + }), }; const executionWorkerCatalog = { @@ -295,8 +301,8 @@ function getWorkerQueue() { graphileJob.id, payload.orphanedEvents ? { - event: payload.orphanedEvents, - } + event: payload.orphanedEvents, + } : undefined ); break; @@ -484,6 +490,15 @@ function getWorkerQueue() { return await service.call(payload.dependencyId, payload.sourceTaskAttemptId); }, }, + "v3.timeoutDeployment": { + priority: 0, + maxAttempts: 5, + handler: async (payload, job) => { + const service = new TimeoutDeploymentService(); + + return await service.call(payload.deploymentId, payload.fromStatus, payload.errorMessage); + }, + }, }, }); } diff --git a/apps/webapp/app/v3/marqs/sharedQueueConsumer.server.ts b/apps/webapp/app/v3/marqs/sharedQueueConsumer.server.ts index f95a560e9..e60838faa 100644 --- a/apps/webapp/app/v3/marqs/sharedQueueConsumer.server.ts +++ b/apps/webapp/app/v3/marqs/sharedQueueConsumer.server.ts @@ -21,6 +21,7 @@ import { CancelAttemptService } from "../services/cancelAttempt.server"; import { socketIo } from "../handleSocketIo.server"; import { singleton } from "~/utils/singleton"; import { RestoreCheckpointService } from "../services/restoreCheckpoint.server"; +import { findCurrentWorkerDeployment } from "../models/workerDeployment.server"; const tracer = trace.getTracer("sharedQueueConsumer"); @@ -179,7 +180,7 @@ export class SharedQueueConsumer { this._taskFailures = 0; this._taskSuccesses = 0; - this.#doWork().finally(() => {}); + this.#doWork().finally(() => { }); } async #doWork() { @@ -316,26 +317,7 @@ export class SharedQueueConsumer { return; } - const deployment = await prisma.workerDeployment.findFirst({ - where: { - environmentId: existingTaskRun.runtimeEnvironmentId, - projectId: existingTaskRun.projectId, - status: "DEPLOYED", - imageReference: { - not: null, - }, - }, - orderBy: { - updatedAt: "desc", - }, - include: { - worker: { - include: { - tasks: true, - }, - }, - }, - }); + const deployment = await findCurrentWorkerDeployment(existingTaskRun.runtimeEnvironmentId); if (!deployment || !deployment.worker) { logger.error("No matching deployment found for task run", { diff --git a/apps/webapp/app/v3/models/workerDeployment.server.ts b/apps/webapp/app/v3/models/workerDeployment.server.ts new file mode 100644 index 000000000..e9e19c1b5 --- /dev/null +++ b/apps/webapp/app/v3/models/workerDeployment.server.ts @@ -0,0 +1,29 @@ +import type { Prettify } from "@trigger.dev/core"; +import { CURRENT_DEPLOYMENT_LABEL } from "~/consts"; +import { prisma } from "~/db.server"; + +export type CurrentWorkerDeployment = Prettify>>>; + +export async function findCurrentWorkerDeployment(environmentId: string) { + const promotion = await prisma.workerDeploymentPromotion.findUnique({ + where: { + environmentId_label: { + environmentId, + label: CURRENT_DEPLOYMENT_LABEL, + } + }, + include: { + deployment: { + include: { + worker: { + include: { + tasks: true, + }, + }, + } + } + } + }); + + return promotion?.deployment; +} \ No newline at end of file diff --git a/apps/webapp/app/v3/services/createCheckpointRestoreEvent.server.ts b/apps/webapp/app/v3/services/createCheckpointRestoreEvent.server.ts index 16a9f4a71..f4acf5adc 100644 --- a/apps/webapp/app/v3/services/createCheckpointRestoreEvent.server.ts +++ b/apps/webapp/app/v3/services/createCheckpointRestoreEvent.server.ts @@ -1,41 +1,34 @@ import type { CheckpointRestoreEvent, CheckpointRestoreEventType } from "@trigger.dev/database"; -import { $transaction, PrismaClient, prisma } from "~/db.server"; import { logger } from "~/services/logger.server"; +import { BaseService } from "./baseService.server"; -export class CreateCheckpointRestoreEventService { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } +export class CreateCheckpointRestoreEventService extends BaseService { public async call(params: { checkpointId: string; type: CheckpointRestoreEventType; }): Promise { - return await $transaction(this.#prismaClient, async (tx) => { - const checkpoint = await this.#prismaClient.checkpoint.findUniqueOrThrow({ - where: { - id: params.checkpointId, - }, - }); - - logger.debug(`Creating checkpoint/restore event`, params); - - const checkpointEvent = await this.#prismaClient.checkpointRestoreEvent.create({ - data: { - checkpointId: checkpoint.id, - runtimeEnvironmentId: checkpoint.runtimeEnvironmentId, - projectId: checkpoint.projectId, - attemptId: checkpoint.attemptId, - runId: checkpoint.runId, - type: params.type, - reason: checkpoint.reason, - metadata: checkpoint.metadata, - }, - }); - - return checkpointEvent; + const checkpoint = await this._prisma.checkpoint.findUniqueOrThrow({ + where: { + id: params.checkpointId, + }, }); + + logger.debug(`Creating checkpoint/restore event`, params); + + const checkpointEvent = await this._prisma.checkpointRestoreEvent.create({ + data: { + checkpointId: checkpoint.id, + runtimeEnvironmentId: checkpoint.runtimeEnvironmentId, + projectId: checkpoint.projectId, + attemptId: checkpoint.attemptId, + runId: checkpoint.runId, + type: params.type, + reason: checkpoint.reason, + metadata: checkpoint.metadata, + }, + }); + + return checkpointEvent; } } diff --git a/apps/webapp/app/v3/services/createDeployedBackgroundWorker.server.ts b/apps/webapp/app/v3/services/createDeployedBackgroundWorker.server.ts index 1acaf9379..931a3ba8f 100644 --- a/apps/webapp/app/v3/services/createDeployedBackgroundWorker.server.ts +++ b/apps/webapp/app/v3/services/createDeployedBackgroundWorker.server.ts @@ -27,6 +27,10 @@ export class CreateDeployedBackgroundWorkerService extends BaseService { return; } + if (deployment.status !== "DEPLOYING") { + return; + } + const backgroundWorker = await this._prisma.backgroundWorker.create({ data: { friendlyId: generateFriendlyId("worker"), diff --git a/apps/webapp/app/v3/services/indexDeployment.server.ts b/apps/webapp/app/v3/services/indexDeployment.server.ts index 74e6dc012..854dcb034 100644 --- a/apps/webapp/app/v3/services/indexDeployment.server.ts +++ b/apps/webapp/app/v3/services/indexDeployment.server.ts @@ -1,8 +1,10 @@ +import { env } from "~/env.server"; import { logger } from "~/services/logger.server"; import { socketIo } from "../handleSocketIo.server"; import { BaseService } from "./baseService.server"; -import { env } from "~/env.server"; import { DeploymentIndexFailed } from "./deploymentIndexFailed.server"; +import { TimeoutDeploymentService } from "./timeoutDeployment.server"; +import { workerQueue } from "~/services/worker.server"; export class IndexDeploymentService extends BaseService { public async call(id: string) { @@ -43,7 +45,7 @@ export class IndexDeploymentService extends BaseService { // just broadcast for now - there should only ever be one provider connected try { - const responses = await socketIo.providerNamespace.timeout(10_000).emitWithAck("INDEX", { + const responses = await socketIo.providerNamespace.timeout(30_000).emitWithAck("INDEX", { version: "v1", shortCode: deployment.shortCode, imageTag: deployment.imageReference, @@ -52,15 +54,42 @@ export class IndexDeploymentService extends BaseService { apiUrl: env.APP_ORIGIN, }); - const indexFailed = new DeploymentIndexFailed(); + logger.debug("Index ACK received", { responses }); - for (const response of responses) { - if (!response.success) { - await indexFailed.call(deployment.friendlyId, response.error); + if (responses.length === 0) { + // timeout the deployment if 50 seconds have passed and the deployment is still not indexed + await TimeoutDeploymentService.enqueue( + deployment.id, + "DEPLOYING", + "Could not index deployment in time", + new Date(Date.now() + 50_000) + ); + } else { + const indexFailed = new DeploymentIndexFailed(); + + for (const response of responses) { + if (!response.success) { + await indexFailed.call(deployment.friendlyId, response.error); + } } } } catch (error) { logger.error("No index ACK received within timeout", { error }); + + const indexFailed = new DeploymentIndexFailed(); + + await indexFailed.call( + deployment.friendlyId, + error instanceof Error + ? { message: error.message, name: error.name } + : { message: "Could not index deployment in time", name: "TimeoutError" } + ); } } + + static async enqueue(id: string) { + const runAt = new Date(Date.now() + 1000); // 1 second from now (give eventually-consistent DO time) + + await workerQueue.enqueue("v3.indexDeployment", { id }, { runAt }); + } } diff --git a/apps/webapp/app/v3/services/initializeDeployment.server.ts b/apps/webapp/app/v3/services/initializeDeployment.server.ts index a6d3980b5..249e89607 100644 --- a/apps/webapp/app/v3/services/initializeDeployment.server.ts +++ b/apps/webapp/app/v3/services/initializeDeployment.server.ts @@ -1,10 +1,11 @@ -import { AuthenticatedEnvironment } from "~/services/apiAuth.server"; -import { BaseService } from "./baseService.server"; -import { calculateNextBuildVersion } from "../utils/calculateNextBuildVersion"; -import { generateFriendlyId } from "../friendlyIdentifiers"; -import { customAlphabet } from "nanoid"; -import { createRemoteImageBuild } from "../remoteImageBuilder.server"; import { InitializeDeploymentRequestBody } from "@trigger.dev/core/v3"; +import { customAlphabet } from "nanoid"; +import { AuthenticatedEnvironment } from "~/services/apiAuth.server"; +import { generateFriendlyId } from "../friendlyIdentifiers"; +import { createRemoteImageBuild } from "../remoteImageBuilder.server"; +import { calculateNextBuildVersion } from "../utils/calculateNextBuildVersion"; +import { BaseService } from "./baseService.server"; +import { TimeoutDeploymentService } from "./timeoutDeployment.server"; const nanoid = customAlphabet("1234567890abcdefghijklmnopqrstuvwxyz", 8); @@ -56,6 +57,13 @@ export class InitializeDeploymentService extends BaseService { }, }); + await TimeoutDeploymentService.enqueue( + deployment.id, + "BUILDING", + "Building timed out", + new Date(Date.now() + 180_000) // 3 minutes + ); + const imageTag = `trigger/${environment.project.externalRef}:${deployment.version}.${environment.slug}`; return { deployment, imageTag }; diff --git a/apps/webapp/app/v3/services/startDeploymentIndexing.server.ts b/apps/webapp/app/v3/services/startDeploymentIndexing.server.ts index 6370297f9..9a0156dba 100644 --- a/apps/webapp/app/v3/services/startDeploymentIndexing.server.ts +++ b/apps/webapp/app/v3/services/startDeploymentIndexing.server.ts @@ -1,8 +1,8 @@ import { StartDeploymentIndexingRequestBody } from "@trigger.dev/core/v3"; import { AuthenticatedEnvironment } from "~/services/apiAuth.server"; -import { workerQueue } from "~/services/worker.server"; -import { BaseService } from "./baseService.server"; import { registryProxy } from "../registryProxy.server"; +import { BaseService } from "./baseService.server"; +import { IndexDeploymentService } from "./indexDeployment.server"; export class StartDeploymentIndexing extends BaseService { public async call( @@ -22,7 +22,7 @@ export class StartDeploymentIndexing extends BaseService { }, }); - await workerQueue.enqueue("v3.indexDeployment", { id: deployment.id }); + await IndexDeploymentService.enqueue(deployment.id); return deployment; } diff --git a/apps/webapp/app/v3/services/timeoutDeployment.server.ts b/apps/webapp/app/v3/services/timeoutDeployment.server.ts new file mode 100644 index 000000000..d269a750f --- /dev/null +++ b/apps/webapp/app/v3/services/timeoutDeployment.server.ts @@ -0,0 +1,57 @@ +import { logger } from "~/services/logger.server"; +import { BaseService } from "./baseService.server"; +import { workerQueue } from "~/services/worker.server"; + +export class TimeoutDeploymentService extends BaseService { + public async call(id: string, fromStatus: string, errorMessage: string) { + const deployment = await this._prisma.workerDeployment.findUnique({ + where: { + id, + }, + include: { + environment: true, + }, + }); + + if (!deployment) { + logger.error(`No worker deployment with this ID: ${id}`); + return; + } + + if (deployment.status !== fromStatus) { + return; + } + + await this._prisma.workerDeployment.update({ + where: { + id: deployment.id, + }, + data: { + status: "TIMED_OUT", + failedAt: new Date(), + errorData: { message: errorMessage, name: "TimeoutError" }, + }, + }); + } + + static async enqueue( + deploymentId: string, + fromStatus: string, + errorMessage: string, + runAt: Date + ) { + await workerQueue.enqueue( + "v3.timeoutDeployment", + { + deploymentId: deploymentId, + fromStatus, + errorMessage, + }, + { + runAt, + jobKey: `timeoutDeployment:${deploymentId}`, + jobKeyMode: "replace", + } + ); + } +} diff --git a/packages/cli-v3/src/commands/deploy.ts b/packages/cli-v3/src/commands/deploy.ts index 6ef01a7f2..85b13fbfd 100644 --- a/packages/cli-v3/src/commands/deploy.ts +++ b/packages/cli-v3/src/commands/deploy.ts @@ -387,6 +387,11 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { throw new SkipLoggingError("Deployment was canceled"); } + case "TIMED_OUT": { + deploymentSpinner.stop(`Deployment timed out. ${deploymentLink}`); + + throw new SkipLoggingError("Deployment timed out"); + } } } @@ -479,7 +484,8 @@ async function waitForDeploymentToFinish( if ( deployment.data.status === "DEPLOYED" || deployment.data.status === "FAILED" || - deployment.data.status === "CANCELED" + deployment.data.status === "CANCELED" || + deployment.data.status === "TIMED_OUT" ) { span.setAttributes({ "deployment.status": deployment.data.status, diff --git a/packages/cli-v3/src/commands/whoami.ts b/packages/cli-v3/src/commands/whoami.ts index f5d9e0494..9e94d52c2 100644 --- a/packages/cli-v3/src/commands/whoami.ts +++ b/packages/cli-v3/src/commands/whoami.ts @@ -48,7 +48,9 @@ export async function whoAmI( options?: WhoamiCommandOptions, embedded: boolean = false ): Promise { - intro(`Displaying your account details [${options?.profile ?? "default"}]`); + if (!embedded) { + intro(`Displaying your account details [${options?.profile ?? "default"}]`); + } const loadingSpinner = spinner(); loadingSpinner.start("Checking your account details"); diff --git a/packages/core/src/v3/schemas/api.ts b/packages/core/src/v3/schemas/api.ts index 60348c82a..1f2f2d960 100644 --- a/packages/core/src/v3/schemas/api.ts +++ b/packages/core/src/v3/schemas/api.ts @@ -155,7 +155,7 @@ export type InitializeDeploymentRequestBody = z.infer