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
5 changes: 5 additions & 0 deletions .changeset/olive-clouds-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@e2b/code-interpreter-template': patch
---

replaced systemd and start-up.sh with process-compose, so the sandbox and Docker images are supervised the same way
81 changes: 81 additions & 0 deletions js/tests/supervision.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { expect } from 'vitest'
import { isDebug, sandboxTest, wait } from './setup'

async function waitForHealth(sandbox: any, maxRetries = 10, intervalMs = 100) {
for (let i = 0; i < maxRetries; i++) {
try {
const result = await sandbox.commands.run(
'curl -s -o /dev/null -w "%{http_code}" http://0.0.0.0:49999/health'
)
if (result.stdout.trim() === '200') {
return true
}
} catch {
// Connection refused or other error, retry
}
await wait(intervalMs)
}
return false
}

// Recovery has to be judged by running code, not by /health. Killing Jupyter
// leaves the code-interpreter server up but holding dead kernel handles, so it
// answers /health for a few seconds before the process manager recycles it —
// long enough for a health check to pass against a sandbox that cannot execute.
async function waitForWorkingSandbox(
sandbox: any,
maxRetries = 60,
intervalMs = 500
) {
for (let i = 0; i < maxRetries; i++) {
try {
const result = await sandbox.runCode('x = 1; x')
if (result.text === '1') {
return true
}
} catch {
// Still restarting, retry
}
await wait(intervalMs)
}
return false
}

sandboxTest.skipIf(isDebug)(
'restart after jupyter kill',
async ({ sandbox }) => {
// Verify health is up initially
const initialHealth = await waitForHealth(sandbox)
expect(initialHealth).toBe(true)

// Kill the jupyter process as root. The pattern is bracketed so it cannot
// match the shell running it — `pgrep -f 'jupyter server'` matched its own
// command line, so this only ever killed itself. pkill exits non-zero when
// nothing matched, which fails the test rather than passing it vacuously.
await sandbox.commands.run("pkill -9 -f '[j]upyter-server'", {
user: 'root',
})

// Wait for process-compose to restart both processes
const recovered = await waitForWorkingSandbox(sandbox)
expect(recovered).toBe(true)
}
)

sandboxTest.skipIf(isDebug)(
'restart after code-interpreter kill',
async ({ sandbox }) => {
// Verify health is up initially
const initialHealth = await waitForHealth(sandbox)
expect(initialHealth).toBe(true)

// Kill the code-interpreter process as root
await sandbox.commands.run("pkill -9 -f '[u]vicorn main:app'", {
user: 'root',
})

// Wait for process-compose to restart it
const recovered = await waitForWorkingSandbox(sandbox)
expect(recovered).toBe(true)
}
)
73 changes: 0 additions & 73 deletions js/tests/systemd.test.ts

This file was deleted.

67 changes: 67 additions & 0 deletions python/tests/async/test_async_supervision.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import asyncio

import pytest

from e2b_code_interpreter.code_interpreter_async import AsyncSandbox


async def wait_for_health(sandbox: AsyncSandbox, max_retries=10, interval_ms=100):
for _ in range(max_retries):
try:
result = await sandbox.commands.run(
'curl -s -o /dev/null -w "%{http_code}" http://0.0.0.0:49999/health'
)
if result.stdout.strip() == "200":
return True
except Exception:
pass
await asyncio.sleep(interval_ms / 1000)
return False


async def wait_for_working_sandbox(
sandbox: AsyncSandbox, max_retries=60, interval_ms=500
):
"""Recovery has to be judged by running code, not by /health.

Killing Jupyter leaves the code-interpreter server up but holding dead
kernel handles, so it answers /health for a few seconds before the process
manager recycles it — long enough for a health check to pass against a
sandbox that cannot execute.
"""
for _ in range(max_retries):
try:
result = await sandbox.run_code("x = 1; x")
if result.text == "1":
return True
except Exception:
pass
await asyncio.sleep(interval_ms / 1000)
return False


@pytest.mark.skip_debug
async def test_restart_after_jupyter_kill(async_sandbox: AsyncSandbox):
# Verify health is up initially
assert await wait_for_health(async_sandbox)

# Kill the jupyter process as root. The pattern is bracketed so it cannot
# match the shell running it — `pgrep -f 'jupyter server'` matched its own
# command line, so this only ever killed itself. pkill exits non-zero when
# nothing matched, which fails the test rather than passing it vacuously.
await async_sandbox.commands.run("pkill -9 -f '[j]upyter-server'", user="root")

# Wait for process-compose to restart both processes
assert await wait_for_working_sandbox(async_sandbox)


@pytest.mark.skip_debug
async def test_restart_after_code_interpreter_kill(async_sandbox: AsyncSandbox):
# Verify health is up initially
assert await wait_for_health(async_sandbox)

# Kill the code-interpreter process as root
await async_sandbox.commands.run("pkill -9 -f '[u]vicorn main:app'", user="root")

# Wait for process-compose to restart it
assert await wait_for_working_sandbox(async_sandbox)
63 changes: 0 additions & 63 deletions python/tests/async/test_async_systemd.py

This file was deleted.

65 changes: 65 additions & 0 deletions python/tests/sync/test_supervision.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import time

import pytest

from e2b_code_interpreter.code_interpreter_sync import Sandbox


def wait_for_health(sandbox: Sandbox, max_retries=10, interval_ms=100):
for _ in range(max_retries):
try:
result = sandbox.commands.run(
'curl -s -o /dev/null -w "%{http_code}" http://0.0.0.0:49999/health'
)
if result.stdout.strip() == "200":
return True
except Exception:
pass
time.sleep(interval_ms / 1000)
return False


def wait_for_working_sandbox(sandbox: Sandbox, max_retries=60, interval_ms=500):
"""Recovery has to be judged by running code, not by /health.
Killing Jupyter leaves the code-interpreter server up but holding dead
kernel handles, so it answers /health for a few seconds before the process
manager recycles it — long enough for a health check to pass against a
sandbox that cannot execute.
"""
for _ in range(max_retries):
try:
result = sandbox.run_code("x = 1; x")
if result.text == "1":
return True
except Exception:
pass
time.sleep(interval_ms / 1000)
return False


@pytest.mark.skip_debug
def test_restart_after_jupyter_kill(sandbox: Sandbox):
# Verify health is up initially
assert wait_for_health(sandbox)

# Kill the jupyter process as root. The pattern is bracketed so it cannot
# match the shell running it — `pgrep -f 'jupyter server'` matched its own
# command line, so this only ever killed itself. pkill exits non-zero when
# nothing matched, which fails the test rather than passing it vacuously.
sandbox.commands.run("pkill -9 -f '[j]upyter-server'", user="root")

# Wait for process-compose to restart both processes
assert wait_for_working_sandbox(sandbox)


@pytest.mark.skip_debug
def test_restart_after_code_interpreter_kill(sandbox: Sandbox):
# Verify health is up initially
assert wait_for_health(sandbox)

# Kill the code-interpreter process as root
sandbox.commands.run("pkill -9 -f '[u]vicorn main:app'", user="root")

# Wait for process-compose to restart it
assert wait_for_working_sandbox(sandbox)
Loading
Loading