Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions apps/sim/lib/execution/remote-sandbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,10 +308,17 @@ function throwIfSandboxTimedOut(result: { timedOut?: boolean }): void {
/** Bounds the output-directory listing a timeout report adds; a slow listing reports no files. */
const TIMED_OUT_OUTPUT_LISTING_MS = 3_000

/** Throws when the call was cancelled; a passed deadline is not a cancellation. */
function throwIfCancelled(signal: AbortSignal): void {
if (!isTimeoutAbortReason(signal.reason)) throwIfAborted(signal)
}

/**
* A workbench run stopped by its own timeout reports what it produced, because the workbench and
* its files outlive the call and the next call can resume from them. A one-shot sandbox is torn
* down with everything in it, so it keeps the plain timeout.
* Settles a finished run against the call's signal. A workbench run stopped by its own timeout
* reports what it produced, because the workbench and its files outlive the call and the next
* call can resume from them; the call's deadline passing while that stop settles keeps the
* report, and a cancellation at any point stays a cancellation. A one-shot sandbox is torn down
* with everything in it, so it keeps the plain timeout.
*/
async function throwIfRunTimedOut(
result: { timedOut?: boolean },
Expand All @@ -324,8 +331,12 @@ async function throwIfRunTimedOut(
signal: AbortSignal
}
): Promise<void> {
if (!result.timedOut) return
if (!run.lease.session) throw new DOMException('timeout', 'AbortError')
if (!result.timedOut || !run.lease.session) {
throwIfAborted(run.signal)
throwIfSandboxTimedOut(result)
return
}
throwIfCancelled(run.signal)
const { outputSandboxDir, sandbox } = run
// Half of what the call has left, so the report lands before the call's deadline.
const listingMs = Math.min(
Expand All @@ -339,8 +350,7 @@ async function throwIfRunTimedOut(
run.signal
).catch(() => [])
: []
// A cancellation that arrives while listing stays a cancellation.
throwIfAborted(run.signal)
throwIfCancelled(run.signal)
throw new SandboxExecutionTimeoutError(sandboxTimeoutProgress(streams, run.timeoutMs, listed))
}

Expand Down Expand Up @@ -1092,7 +1102,6 @@ async function executeInSandboxWithinBudget(
throwIfAborted(signal)
throw error
}
throwIfAborted(signal)
await throwIfRunTimedOut(execution, [execution.text, execution.stdout, execution.stderr], {
sandbox,
lease,
Expand Down Expand Up @@ -1316,7 +1325,6 @@ async function executeShellInSandboxWithinBudget(
throwIfAborted(signal)
throw error
}
throwIfAborted(signal)
await throwIfRunTimedOut(result, [result.stdout, result.stderr], {
sandbox,
lease,
Expand Down
46 changes: 46 additions & 0 deletions apps/sim/lib/execution/remote-sandbox/session-sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,52 @@ describe('session sandbox lease', () => {
}
})

it('reports a short run whose stop settles after the call deadline', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'Date'] })
try {
const { handle } = fakeSandbox('timeout-slow-stop')
mockFindSessionSandbox.mockResolvedValue(handle)
handle.listFiles = async () => []
// The provider's own timeout stops the command, and that stop settles after the call's
// deadline; an abort before the stop interrupts the command.
handle.runCommand = (_command, options) =>
new Promise<SandboxCommandResult>((resolve, reject) => {
let stopping = false
const stop = () => {
stopping = true
setTimeout(
() => resolve({ stdout: 'step 1', stderr: '', exitCode: 124, timedOut: true }),
3_000
)
}
setTimeout(() => setTimeout(stop, options.timeoutMs), 300)
options.signal?.addEventListener(
'abort',
() => {
if (!stopping) reject(options.signal?.reason)
},
{ once: true }
)
})
const pending = executeShellInSandbox({
...CODE_REQUEST,
timeoutMs: 10_000,
envs: {},
session: { key: 'timeout-slow-stop' },
outputSandboxDir: OUTPUT_DIR,
}).then(
() => undefined,
(rejection: unknown) => rejection
)
await vi.runAllTimersAsync()
const error = await pending
if (!(error instanceof SandboxExecutionTimeoutError)) throw error
expect(error.progress.output).toBe('step 1')
} finally {
vi.useRealTimers()
}
})

it('keeps a cancellation that arrives while listing the outputs a cancellation', async () => {
const { handle } = fakeSandbox('timeout-cancelled')
mockFindSessionSandbox.mockResolvedValue(handle)
Expand Down
Loading