feat: keep containers alive for 60s of inactivity instead of killing them on disconnect

This commit is contained in:
James Murdza 2024-09-30 03:41:33 -07:00
parent 09b3cf1862
commit 01fb3ab921
2 changed files with 22 additions and 20 deletions

View File

@ -34,6 +34,9 @@ import {
saveFileRL, saveFileRL,
} from "./ratelimit"; } from "./ratelimit";
// The amount of time in ms that a container will stay alive without a hearbeat.
const CONTAINER_TIMEOUT = 60_000;
dotenv.config(); dotenv.config();
const app: Express = express(); const app: Express = express();
@ -161,7 +164,7 @@ io.on("connection", async (socket) => {
try { try {
// Start a new container if the container doesn't exist or it timed out. // Start a new container if the container doesn't exist or it timed out.
if (!containers[data.sandboxId] || !(await containers[data.sandboxId].isRunning())) { if (!containers[data.sandboxId] || !(await containers[data.sandboxId].isRunning())) {
containers[data.sandboxId] = await Sandbox.create({ timeoutMs: 1200_000 }); containers[data.sandboxId] = await Sandbox.create({ timeoutMs: CONTAINER_TIMEOUT });
console.log("Created container ", data.sandboxId); console.log("Created container ", data.sandboxId);
} }
} catch (e: any) { } catch (e: any) {
@ -198,6 +201,17 @@ io.on("connection", async (socket) => {
socket.emit("loaded", sandboxFiles.files); socket.emit("loaded", sandboxFiles.files);
socket.on("heartbeat", async () => {
try {
// This keeps the container alive for another CONTAINER_TIMEOUT seconds.
// The E2B docs are unclear, but the timeout is relative to the time of this method call.
await containers[data.sandboxId].setTimeout(CONTAINER_TIMEOUT);
} catch (e: any) {
console.error("Error setting timeout:", e);
io.emit("error", `Error: set timeout. ${e.message ?? e}`);
}
});
socket.on("getFile", (fileId: string, callback) => { socket.on("getFile", (fileId: string, callback) => {
console.log(fileId); console.log(fileId);
try { try {
@ -656,25 +670,6 @@ io.on("connection", async (socket) => {
} }
if (data.isOwner && connections[data.sandboxId] <= 0) { if (data.isOwner && connections[data.sandboxId] <= 0) {
await Promise.all(
Object.entries(terminals).map(async ([key, terminal]) => {
await terminal.close();
delete terminals[key];
})
);
await lockManager.acquireLock(data.sandboxId, async () => {
try {
if (containers[data.sandboxId]) {
await containers[data.sandboxId].kill();
delete containers[data.sandboxId];
console.log("Closed container", data.sandboxId);
}
} catch (error) {
console.error("Error closing container ", data.sandboxId, error);
}
});
socket.broadcast.emit( socket.broadcast.emit(
"disableAccess", "disableAccess",
"The sandbox owner has disconnected." "The sandbox owner has disconnected."

View File

@ -59,6 +59,13 @@ export default function CodeEditor({
} }
}, [socket, userData.id, sandboxData.id, setUserAndSandboxId]) }, [socket, userData.id, sandboxData.id, setUserAndSandboxId])
// This heartbeat is critical to preventing the E2B sandbox from timing out
useEffect(() => {
// 10000 ms = 10 seconds
const interval = setInterval(() => socket?.emit("heartbeat"), 10000);
return () => clearInterval(interval);
}, [socket]);
//Preview Button state //Preview Button state
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true) const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
const [disableAccess, setDisableAccess] = useState({ const [disableAccess, setDisableAccess] = useState({