diff --git a/.changeset/daytona-request-timeouts.md b/.changeset/daytona-request-timeouts.md new file mode 100644 index 000000000..94c1a73c7 --- /dev/null +++ b/.changeset/daytona-request-timeouts.md @@ -0,0 +1,6 @@ +--- +"@truefoundry/trueforge": patch +"@truefoundry/trueforge-core": patch +--- + +Cap Daytona status-refresh calls at 1 minute so a stalled provider cannot hang request handlers. diff --git a/packages/trueforge/src/sandbox/providerUtils.ts b/packages/trueforge/src/sandbox/providerUtils.ts index 9189f7742..65974eb36 100644 --- a/packages/trueforge/src/sandbox/providerUtils.ts +++ b/packages/trueforge/src/sandbox/providerUtils.ts @@ -1,6 +1,11 @@ /** Daytona provider construction + persisted build-status refresh (see checkSnapshotStatus). */ import { Daytona, DaytonaError } from '@daytona/sdk'; -import { DaytonaSandboxProvider, SANDBOX_IMAGE_URI, type SandboxBuild } from '@truefoundry/trueforge-core/core'; +import { + DaytonaSandboxProvider, + SANDBOX_IMAGE_URI, + withTimeout, + type SandboxBuild, +} from '@truefoundry/trueforge-core/core'; import type { Logger } from 'winston'; import configuration from '../config'; import type { ISandboxProviderStore, SandboxProviderRecord } from '../db/sandboxProviderStore'; @@ -68,6 +73,9 @@ function sandboxStatusFromRecord(record: SandboxProviderRecord): SandboxStatus { // Daytona deactivates idle snapshots after 14 days; revalidate at 13 to stay a day ahead. const READY_REVALIDATE_INTERVAL_MS = 13 * 24 * 60 * 60 * 1000; +/** Cap the Daytona round-trip for the refresh, which runs outside a transaction. */ +const STATUS_REFRESH_TIMEOUT_MS = 60_000; + export async function checkSnapshotStatus({ store, tenant_id, @@ -99,9 +107,9 @@ export async function checkSnapshotStatus({ let build: SandboxBuild; if (record.status === 'ready') { // this is because image may have deactivated - build = await provider.buildImage(); + build = await withTimeout(provider.buildImage(), STATUS_REFRESH_TIMEOUT_MS, 'sandbox buildImage'); } else { - build = await provider.getImageBuildStatus(); + build = await withTimeout(provider.getImageBuildStatus(), STATUS_REFRESH_TIMEOUT_MS, 'sandbox getImageBuildStatus'); } const next = toSandboxStatus(build); const updated = await store.updateSandboxStatus({ tenant_id, ...next });