Skip to content
Merged
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
30 changes: 30 additions & 0 deletions apps/sim/lib/execution/remote-sandbox/e2b-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,36 @@ describe('E2B session recovery', () => {
expect(stopCommand).toHaveBeenCalledOnce()
expect(killSandbox).not.toHaveBeenCalled()
})

it('returns what a session command printed before its own timeout stopped it', async () => {
runCommand.mockImplementation(
async (
_command: string,
options: { onStdout?: (chunk: string) => void; onStderr?: (chunk: string) => void }
) => {
options.onStdout?.('rendered 1 of 3\n')
options.onStderr?.('slow frame\n')
options.onStdout?.('rendered 2 of 3\n')
return {
pid: 7,
wait: async () => {
throw Object.assign(
new Error("deadline_exceeded: This error is likely due to exceeding 'timeoutMs'"),
{ name: 'TimeoutError' }
)
},
}
}
)
const sandbox = await sessionSandbox('reconnected')
const result = await sandbox?.runCommand('render', { timeoutMs: 1000 })
expect(result).toMatchObject({
timedOut: true,
exitCode: 124,
stdout: 'rendered 1 of 3\nrendered 2 of 3\n',
stderr: 'slow frame\n',
})
})
})

describe('E2B session lease', () => {
Expand Down
27 changes: 17 additions & 10 deletions apps/sim/lib/execution/remote-sandbox/e2b.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,14 @@ export async function stopE2BSessionProcess(
if (sandbox) await stopSessionCommand(sandbox, process.id, signal)
}

/** Joins what the stream callbacks observed with the streams the SDK returned, without repeating either. */
function mergeObservedOutput(observed: string, returned: string): string {
if (!observed) return returned
if (!returned || observed.endsWith(returned)) return observed
if (returned.startsWith(observed)) return returned
return observed + returned
}

class E2BSandboxHandle implements SandboxHandle {
private killed = false
private killPromise: Promise<void> | null = null
Expand Down Expand Up @@ -676,12 +684,6 @@ class E2BSandboxHandle implements SandboxHandle {
// process handle because `background: true` is fixed above.
result = started
}
const mergeObservedOutput = (observed: string, returned: string): string => {
if (!observed) return returned
if (!returned || observed.endsWith(returned)) return observed
if (returned.startsWith(observed)) return returned
return observed + returned
}
const stdout = mergeObservedOutput(observedStdout, result.stdout ?? '')
const stderr = mergeObservedOutput(observedStderr, result.stderr ?? '')
assertSandboxProcessOutputWithinLimit([stdout, stderr], options.maxOutputBytes)
Expand Down Expand Up @@ -727,16 +729,21 @@ class E2BSandboxHandle implements SandboxHandle {
)
const tailIfStreamed = (value: string | undefined, retain: boolean) =>
retain || value === undefined ? value : tailStreamedSandboxOutput(value)
const failureStdout = tailIfStreamed(failure.stdout, retainStdout)
const failureStderr = tailIfStreamed(failure.stderr, retainStderr)
if (isE2BExecutionTimeout(error)) {
// The SDK's timeout error carries no streams; the callbacks observed the run's output.
const stdout = mergeObservedOutput(observedStdout, failure.stdout ?? '')
const stderr = mergeObservedOutput(observedStderr, failure.stderr ?? '')
return {
stdout: failureStdout ?? '',
stderr: failureStderr ?? failure.message ?? '',
stdout: retainStdout ? stdout : tailStreamedSandboxOutput(stdout),
stderr:
(retainStderr ? stderr : tailStreamedSandboxOutput(stderr)) ||
`Command timed out after ${options.timeoutMs}ms`,
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
exitCode: 124,
timedOut: true,
}
}
const failureStdout = tailIfStreamed(failure.stdout, retainStdout)
const failureStderr = tailIfStreamed(failure.stderr, retainStderr)
return {
stdout: failureStdout ?? '',
stderr: failureStderr ?? failure.message ?? getErrorMessage(error),
Expand Down
61 changes: 57 additions & 4 deletions apps/sim/lib/execution/remote-sandbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
isTimeoutAbortReason,
} from '@/lib/core/execution-limits'
import { recordSandboxTeardownFailure } from '@/lib/core/execution-limits/metrics'
import { withinDeadline } from '@/lib/core/utils/deadline'
import { buildJavaScriptRuntimeBindingsSource } from '@/lib/execution/code-placeholders/javascript-runtime'
import { SANDBOX_SYSTEM_PATH } from '@/lib/execution/remote-sandbox/cli-tools.server'
import { describeProcessExit } from '@/lib/execution/remote-sandbox/code-failure'
Expand Down Expand Up @@ -65,6 +66,10 @@ import {
recordSessionFileInput,
} from '@/lib/execution/remote-sandbox/session-file-provenance'
import { withSandboxSessionLock } from '@/lib/execution/remote-sandbox/session-lock'
import {
SandboxExecutionTimeoutError,
sandboxTimeoutProgress,
} from '@/lib/execution/remote-sandbox/timeout'
import type {
CreateSandboxOptions,
SandboxCodeResult,
Expand Down Expand Up @@ -258,6 +263,40 @@ function throwIfSandboxTimedOut(result: { timedOut?: boolean }): void {
if (result.timedOut) throw new DOMException('timeout', 'AbortError')
}

/** Bounds the output-directory listing a timeout report adds; a slow listing reports no files. */
const TIMED_OUT_OUTPUT_LISTING_MS = 3_000

/**
* 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.
*/
async function throwIfRunTimedOut(
result: { timedOut?: boolean },
streams: Array<string | undefined>,
run: {
sandbox: SandboxHandle
lease: SandboxLease
outputSandboxDir: string | undefined
timeoutMs: number
signal: AbortSignal
}
): Promise<void> {
if (!result.timedOut) return
if (!run.lease.session) throw new DOMException('timeout', 'AbortError')
const { outputSandboxDir, sandbox } = run
const listed = outputSandboxDir
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
? await withinDeadline(
() => sandbox.listFiles(outputSandboxDir, { depth: SANDBOX_OUTPUT_DIR_MAX_DEPTH }),
Date.now() + TIMED_OUT_OUTPUT_LISTING_MS,
run.signal
).catch(() => [])
: []
// A cancellation that arrives while listing stays a cancellation.
throwIfAborted(run.signal)
throw new SandboxExecutionTimeoutError(sandboxTimeoutProgress(streams, run.timeoutMs, listed))
}

function bindSandboxAbort(
sandbox: SandboxHandle,
provider: SandboxProviderId,
Expand Down Expand Up @@ -969,10 +1008,11 @@ async function executeInSandboxWithinBudget(
Object.keys(privateInputFiles.environment).length > 0

let execution: SandboxCodeResult
const runTimeoutMs = codeBudgetMs(req, signal)
try {
const endRun = phases.start('run')
execution = await sandbox.runCode(code, {
timeoutMs: codeBudgetMs(req, signal),
timeoutMs: runTimeoutMs,
javascriptPreload: buildJavaScriptRuntimeBindingsSource(req.runtimeBindings ?? []),
maxOutputBytes: MAX_SANDBOX_PROCESS_OUTPUT_BYTES,
signal,
Expand All @@ -984,7 +1024,13 @@ async function executeInSandboxWithinBudget(
throw error
}
throwIfAborted(signal)
throwIfSandboxTimedOut(execution)
await throwIfRunTimedOut(execution, [execution.text, execution.stdout, execution.stderr], {
sandbox,
lease,
outputSandboxDir: req.outputSandboxDir,
timeoutMs: runTimeoutMs,
signal,
})

if (execution.error) {
const errorMessage = `${execution.error.name}: ${execution.error.value}`
Expand Down Expand Up @@ -1166,6 +1212,7 @@ async function executeShellInSandboxWithinBudget(
endPrepare()

let result: SandboxCommandResult
const runTimeoutMs = codeBudgetMs(req, signal)
try {
const endRun = phases.start('run')
result = await sandbox.runCommand(code, {
Expand All @@ -1177,7 +1224,7 @@ async function executeShellInSandboxWithinBudget(
...privateInputFiles.environment,
...(req.session && req.outputSandboxDir ? { SIM_OUTPUT_DIR: req.outputSandboxDir } : {}),
},
timeoutMs: codeBudgetMs(req, signal),
timeoutMs: runTimeoutMs,
maxOutputBytes: MAX_SANDBOX_PROCESS_OUTPUT_BYTES,
signal,
rootUser: !lease.session,
Expand All @@ -1189,7 +1236,13 @@ async function executeShellInSandboxWithinBudget(
throw error
}
throwIfAborted(signal)
throwIfSandboxTimedOut(result)
await throwIfRunTimedOut(result, [result.stdout, result.stderr], {
sandbox,
lease,
outputSandboxDir: req.outputSandboxDir,
timeoutMs: runTimeoutMs,
signal,
})

const stdout = [result.stdout, result.stderr].filter(Boolean).join('\n')

Expand Down
5 changes: 3 additions & 2 deletions apps/sim/lib/execution/remote-sandbox/output-limits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,16 @@ const STREAMED_OUTPUT_TRUNCATION_NOTE =
*/
export function tailStreamedSandboxOutput(
value: string | undefined,
limitBytes = MAX_SANDBOX_STREAMED_OUTPUT_TAIL_BYTES
limitBytes = MAX_SANDBOX_STREAMED_OUTPUT_TAIL_BYTES,
truncationNote = STREAMED_OUTPUT_TRUNCATION_NOTE
): string {
if (!value) return ''
const buffer = Buffer.from(value, 'utf8')
if (buffer.length <= limitBytes) return value

let start = buffer.length - limitBytes
while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) start += 1
return `${STREAMED_OUTPUT_TRUNCATION_NOTE}${buffer.subarray(start).toString('utf8')}`
return `${truncationNote}${buffer.subarray(start).toString('utf8')}`
}

/**
Expand Down
Loading
Loading