ソースを参照

feat(server): run modal sandboxes on the vm runtime (#41177)

Kit Langton 1 週間 前
コミット
9b7b402737
1 ファイル変更36 行追加11 行削除
  1. 36 11
      packages/server/src/workspace/modal.ts

+ 36 - 11
packages/server/src/workspace/modal.ts

@@ -13,15 +13,30 @@ trap 'rm -f -- "$pidfile"' EXIT
 "$@"
 `
 
+// Modal's VM runtime accepts process-group signals without delivering them
+// (kill(-pgid) returns 0 and nothing dies; direct-pid signals work), so the
+// group is enumerated from /proc and each member is signalled directly. The
+// second pass catches children forked between scan and signal.
 const KILL = `
+pidfile=$1
+sig=$2
 i=0
-while [ ! -s "$1" ] && [ "$i" -lt 250 ]; do sleep 0.02; i=$((i + 1)); done
-if [ -s "$1" ]; then
-  pid=$(cat "$1")
-  /bin/kill "-$2" "-$pid" 2>/dev/null || true
-else
-  exit 47
-fi
+while [ ! -s "$pidfile" ] && [ "$i" -lt 250 ]; do sleep 0.02; i=$((i + 1)); done
+[ -s "$pidfile" ] || exit 47
+target=$(cat "$pidfile")
+pass=0
+while [ "$pass" -lt 2 ]; do
+  for stat in /proc/[0-9]*/stat; do
+    [ -e "$stat" ] || continue
+    pid=\${stat#/proc/}
+    pid=\${pid%/stat}
+    set -- $(sed "s/.*) //" "$stat" 2>/dev/null)
+    if [ "\${3:-}" = "$target" ]; then
+      /bin/kill "-$sig" "$pid" 2>/dev/null || true
+    fi
+  done
+  pass=$((pass + 1))
+done
 `
 
 export interface ModalImageSpec {
@@ -54,7 +69,15 @@ export const createModalSandbox = async (options: ModalSandboxOptions) => {
   const app = await client.apps.fromName(options.app, { createIfMissing: true })
   const imageSpec = options.image ?? ubuntuImage
   const image = client.images.fromRegistry(imageSpec.registry).dockerfileCommands([...imageSpec.dockerfileCommands])
-  const sandbox = await client.sandboxes.create(app, image, options.sandbox)
+  // Always Modal's Full-VM runtime (beta, enabled per account): a real kernel
+  // with real device nodes, so workspaces can run Docker and other
+  // kernel-dependent workloads. Costs versus gVisor, measured Aug 2026:
+  // per-exec floor ~285-535ms versus ~90-165ms, and filesystem snapshots only
+  // (no memory snapshots — acceptable; fs-snapshot is the persistence design).
+  const sandbox = await client.sandboxes.create(app, image, {
+    ...options.sandbox,
+    experimentalOptions: { ...options.sandbox?.experimentalOptions, vm_runtime: true },
+  })
   return {
     driver: makeModalDriver(sandbox),
     sandbox,
@@ -64,12 +87,14 @@ export const createModalSandbox = async (options: ModalSandboxOptions) => {
 
 /**
  * Adapts Modal exec to the Environment driver. Files intentionally has no native
- * overrides: Modal exec and filesystem tools share the same roughly 175ms floor,
- * so the derived exec defaults are the simplest implementation with no measured loss.
+ * overrides: exec latency dominates payload work (VM runtime floor measured
+ * ~285-535ms per exec, Aug 2026), so the derived exec defaults are the simplest
+ * implementation with no measured loss.
  *
  * Modal cannot signal a ContainerProcess. Each command therefore starts a new
  * process group and records its leader in a unique pid file; kill runs a second
- * sandbox command that signals that group. Pid files are removed best-effort.
+ * sandbox command that enumerates that group from /proc and signals each member
+ * directly (see KILL). Pid files are removed best-effort.
  */
 export const makeModalDriver = (sandbox: Sandbox): Driver => {
   const spawn = Effect.fnUntraced(function* (command: Command) {