diff --git a/frontend/src/app/data-providers/default-data-provider.tsx b/frontend/src/app/data-providers/default-data-provider.tsx index be3b3c2499..f1ee216d9c 100644 --- a/frontend/src/app/data-providers/default-data-provider.tsx +++ b/frontend/src/app/data-providers/default-data-provider.tsx @@ -189,12 +189,20 @@ const defaultContext = { actorStatusAdditionalInfoQueryOptions(actorId: ActorId) { return queryOptions({ ...this.actorQueryOptions(actorId), - select: ({ rescheduleAt }) => ({ + select: ({ rescheduleAt, error }) => ({ rescheduleAt, + error, }), }); }, + actorErrorQueryOptions(actorId: ActorId) { + return queryOptions({ + ...this.actorQueryOptions(actorId), + select: (data) => data.error, + }); + }, + actorFeaturesQueryOptions(actorId: ActorId) { return queryOptions({ ...this.actorQueryOptions(actorId), diff --git a/frontend/src/app/data-providers/engine-data-provider.tsx b/frontend/src/app/data-providers/engine-data-provider.tsx index 3a651e6fc3..1cb3095eb5 100644 --- a/frontend/src/app/data-providers/engine-data-provider.tsx +++ b/frontend/src/app/data-providers/engine-data-provider.tsx @@ -697,6 +697,7 @@ function transformActor(a: Rivet.Actor): Actor { rescheduleAt: a.rescheduleTs ? new Date(a.rescheduleTs).toISOString() : undefined, + error: a.error, features: [ ActorFeature.Config, ActorFeature.Connections, diff --git a/frontend/src/app/runner-config-table.tsx b/frontend/src/app/runner-config-table.tsx index 5deff294dd..1c3d4cfb23 100644 --- a/frontend/src/app/runner-config-table.tsx +++ b/frontend/src/app/runner-config-table.tsx @@ -28,7 +28,7 @@ import { Text, WithTooltip, } from "@/components"; -import { ActorRegion } from "@/components/actors"; +import { ActorRegion, RunnerPoolError } from "@/components/actors"; import { REGION_LABEL } from "@/components/matchmaker/lobby-region"; import { hasMetadataProvider } from "./data-providers/engine-data-provider"; @@ -256,48 +256,7 @@ function Row({ ); } -function RunnerPoolError({ - error, -}: { - error: Rivet.RunnerPoolError | undefined; -}) { - return match(error) - .with(P.nullish, () => null) - .with(P.string, (errStr) => - match(errStr) - .with( - "internal_error", - () => "Internal error occurred in runner pool", - ) - .with( - "serverless_invalid_base64", - () => "Invalid base64 encoding in serverless response", - ) - .with( - "serverless_stream_ended_early", - () => "Connection terminated unexpectedly", - ) - .otherwise(() => "Unknown runner pool error"), - ) - .with(P.shape({ serverlessHttpError: P.any }), (errObj) => { - const { statusCode, body } = errObj.serverlessHttpError; - const code = statusCode ?? "unknown"; - return body ? `HTTP ${code} error: ${body}` : `HTTP ${code} error`; - }) - .with(P.shape({ serverlessConnectionError: P.any }), (errObj) => { - const message = errObj.serverlessConnectionError?.message; - return message - ? `Connection failed: ${message}` - : "Unable to connect to serverless endpoint"; - }) - .with(P.shape({ serverlessInvalidPayload: P.any }), (errObj) => { - const message = errObj.serverlessInvalidPayload?.message; - return message - ? `Invalid request payload: ${message}` - : "Request payload validation failed"; - }) - .otherwise(() => "Unexpected runner pool error"); -} + function StatusCell({ errors, diff --git a/frontend/src/components/actors/actor-status-label.tsx b/frontend/src/components/actors/actor-status-label.tsx index 2faac42c55..25047cb655 100644 --- a/frontend/src/components/actors/actor-status-label.tsx +++ b/frontend/src/components/actors/actor-status-label.tsx @@ -1,5 +1,7 @@ +import type { Rivet } from "@rivetkit/engine-api-full"; import { useQuery } from "@tanstack/react-query"; import { formatISO } from "date-fns"; +import { match, P } from "ts-pattern"; import { RelativeTime } from "../relative-time"; import { useDataProvider } from "./data-provider"; import type { ActorId, ActorStatus } from "./queries"; @@ -8,7 +10,7 @@ export const ACTOR_STATUS_LABEL_MAP = { unknown: "Unknown", starting: "Starting", running: "Running", - stopped: "Stopped", + stopped: "Destroyed", crashed: "Crashed", sleeping: "Sleeping", pending: "Pending", @@ -48,7 +50,7 @@ export function QueriedActorStatusAdditionalInfo({ }: { actorId: ActorId; }) { - const { data: { rescheduleAt } = {} } = useQuery( + const { data: { rescheduleAt, error } = {} } = useQuery( useDataProvider().actorStatusAdditionalInfoQueryOptions(actorId), ); @@ -64,5 +66,88 @@ export function QueriedActorStatusAdditionalInfo({ ); } + if (error) { + return ; + } + return null; } + +export function ActorError({ error }: { error: Rivet.ActorError }) { + return match(error) + .with(P.string, (errMsg) => + match(errMsg) + .with("no_capacity", () => ( + No capacity available to start Actor. + )) + .exhaustive(), + ) + .with(P.shape({ runnerPoolError: P.any }), (err) => ( + + Runner Pool Error:{" "} + + + )) + .with(P.shape({ runnerNoResponse: P.any }), (err) => ( + + Runner ({err.runnerNoResponse.runnerId}) was allocated but Actor + did not respond. + + )) + .exhaustive(); +} + +export function QueriedActorError({ actorId }: { actorId: ActorId }) { + const { data: error, isError } = useQuery( + useDataProvider().actorErrorQueryOptions(actorId), + ); + + if (isError || !error) { + return null; + } + + return ; +} + +export function RunnerPoolError({ + error, +}: { + error: Rivet.RunnerPoolError | undefined; +}) { + return match(error) + .with(P.nullish, () => null) + .with(P.string, (errStr) => + match(errStr) + .with( + "internal_error", + () => "Internal error occurred in runner pool", + ) + .with( + "serverless_invalid_base64", + () => "Invalid base64 encoding in serverless response", + ) + .with( + "serverless_stream_ended_early", + () => "Connection terminated unexpectedly", + ) + .otherwise(() => "Unknown runner pool error"), + ) + .with(P.shape({ serverlessHttpError: P.any }), (errObj) => { + const { statusCode, body } = errObj.serverlessHttpError; + const code = statusCode ?? "unknown"; + return body ? `HTTP ${code} error: ${body}` : `HTTP ${code} error`; + }) + .with(P.shape({ serverlessConnectionError: P.any }), (errObj) => { + const message = errObj.serverlessConnectionError?.message; + return message + ? `Connection failed: ${message}` + : "Unable to connect to serverless endpoint"; + }) + .with(P.shape({ serverlessInvalidPayload: P.any }), (errObj) => { + const message = errObj.serverlessInvalidPayload?.message; + return message + ? `Invalid request payload: ${message}` + : "Request payload validation failed"; + }) + .exhaustive(); +} diff --git a/frontend/src/components/actors/guard-connectable-inspector.tsx b/frontend/src/components/actors/guard-connectable-inspector.tsx index 22950a1145..dcfbf0f98f 100644 --- a/frontend/src/components/actors/guard-connectable-inspector.tsx +++ b/frontend/src/components/actors/guard-connectable-inspector.tsx @@ -1,5 +1,10 @@ /** biome-ignore-all lint/correctness/useHookAtTopLevel: safe guarded by build consts */ -import { faPowerOff, faSpinnerThird, Icon } from "@rivet-gg/icons"; +import { + faExclamationTriangle, + faPowerOff, + faSpinnerThird, + Icon, +} from "@rivet-gg/icons"; import { useInfiniteQuery, useMutation, @@ -8,6 +13,7 @@ import { } from "@tanstack/react-query"; import { useMatch, useRouteContext } from "@tanstack/react-router"; import { createContext, type ReactNode, useContext, useMemo } from "react"; +import { match, P } from "ts-pattern"; import { useLocalStorage } from "usehooks-ts"; import { useInspectorCredentials } from "@/app/credentials-context"; import { createInspectorActorContext } from "@/queries/actor-inspector"; @@ -19,8 +25,9 @@ import { Button } from "../ui/button"; import { useFiltersValue } from "./actor-filters-context"; import { ActorProvider, useActor } from "./actor-queries-context"; import { Info } from "./actor-state-tab"; +import { QueriedActorError } from "./actor-status-label"; import { useDataProvider, useEngineCompatDataProvider } from "./data-provider"; -import type { ActorId } from "./queries"; +import type { ActorId, ActorStatus } from "./queries"; const InspectorGuardContext = createContext(null); @@ -35,41 +42,59 @@ export function GuardConnectableInspector({ actorId, children, }: GuardConnectableInspectorProps) { - const { data: { destroyedAt, pendingAllocationAt, startedAt } = {} } = - useQuery({ - ...useDataProvider().actorQueryOptions(actorId), - refetchInterval: 1000, - select: (data) => ({ - destroyedAt: data.destroyedAt, - sleepingAt: data.sleepingAt, - pendingAllocationAt: data.pendingAllocationAt, - startedAt: data.startedAt, - }), - }); + const { data: status } = useQuery({ + ...useDataProvider().actorStatusQueryOptions(actorId), + refetchInterval: 1000, + }); - if (destroyedAt) { - return ( + return match(status) + .with(P.union("running", "sleeping"), () => ( + + {children} + + )) + .otherwise((status) => ( Unavailable for inactive Actors.} + value={} > {children} - ); - } - - if (pendingAllocationAt && !startedAt) { - return ( - }> - {children} - - ); - } + )); +} - return ( - - {children} - - ); +function UnavailableInfo({ + actorId, + status, +}: { + actorId: ActorId; + status?: ActorStatus; +}) { + return match(status) + .with("crashed", () => ( + + + Actor is unavailable. + + + + + )) + .with("pending", () => ) + .with("stopped", () => ( + + Actor has been destroyed. + + )) + .otherwise(() => { + return ( + + Actor is unavailable. + + ); + }); } function NoRunners() { diff --git a/frontend/src/components/actors/queries/index.ts b/frontend/src/components/actors/queries/index.ts index 383c469228..e9a7888109 100644 --- a/frontend/src/components/actors/queries/index.ts +++ b/frontend/src/components/actors/queries/index.ts @@ -1,3 +1,4 @@ +import type { Rivet } from "@rivetkit/engine-api-full"; import type { Actor as InspectorActor } from "rivetkit/inspector"; export type { ActorLogEntry } from "rivetkit/inspector"; @@ -64,6 +65,7 @@ export type Actor = Omit & { pendingAllocationAt?: string | null; datacenter?: string | null; rescheduleAt?: string | null; + error?: Rivet.ActorError | null; } & { id: ActorId }; export enum CrashPolicy { @@ -110,6 +112,7 @@ export function getActorStatus( | "sleepingAt" | "pendingAllocationAt" | "rescheduleAt" + | "error" >, ): ActorStatus { const { @@ -119,8 +122,13 @@ export function getActorStatus( sleepingAt, pendingAllocationAt, rescheduleAt, + error, } = actor; + if (error) { + return "crashed"; + } + if (rescheduleAt) { return "crash-loop"; }
Actor is unavailable.
+ +
Actor has been destroyed.