Skip to content

feat: configurable Modal sandbox CPU and memory#693

Merged
ColeMurray merged 10 commits into
ColeMurray:mainfrom
vladgurovich:feat/sandbox-cpu-memory
Jun 9, 2026
Merged

feat: configurable Modal sandbox CPU and memory#693
ColeMurray merged 10 commits into
ColeMurray:mainfrom
vladgurovich:feat/sandbox-cpu-memory

Conversation

@vladgurovich

@vladgurovich vladgurovich commented May 31, 2026

Copy link
Copy Markdown
Contributor

What

Adds two optional fields to SandboxSettingscpuCores and memoryMib — letting an operator reserve CPU and memory per sandbox. They map to Modal's cpu / memory kwargs on Sandbox.create.

Why

Sandboxes previously ran at Modal's ~0.125-core floor with bursting, with no way to guarantee resources for heavier install/build/test workloads. This is particularly important when trying to build/test complex applications in compiled languages such as Java in the sandbox.

Behavior & design decisions

  • No default reservation. Unset → the provider's own default applies (Modal's floor), so existing sandboxes see no cost change. Modal bills max(reservation, actual usage), so a default would have raised the billed floor for every sandbox; I intentionally avoided that.
  • Positivity-only validation. cpuCores must be a positive number (fractional allowed); memoryMib a positive integer. I do not impose arbitrary min/max ranges — Modal is the source of truth for real limits and rejects anything it can't honor at create time.
  • Advisory / provider-dependent. Modal honors the fields; providers without resource reservations (Daytona) ignore them.

What About Daytona

I havent used Daytona before, so i interrogated Claude and there is an eventual path to have Daytona respect cpu/memory config via hot-resize. The only wrinkle is that Daytona ties resources to the snapshot, so the cpu/memory config would be applied with a POST /sandbox/:id/resize AFTER create rather than at create time. CPU/Memory increases in Daytona are "hot" -- they work on a running sandbox with no stop.

How settings flow

You set CPU/memory the same way as any other sandbox setting — globally or per-repo through the integration-settings API, which stores them in D1.

When a session starts, we resolve the effective settings (repo overrides on top of global defaults) and save the whole object on the session row as-is. The values aren't touched again until the sandbox actually spins up: at that point we re-validate them — the stored blob can come from older or untrusted writes, so the spawn path doesn't trust it — and pass anything still valid to Modal as cpu / memory.

Snapshot restores take the same path, so a resumed session gets the same reservation as a fresh one.

Changes by layer

Layer File Change
Type shared/.../integrations.ts cpuCores / memoryMib on SandboxSettings, doc'd as advisory
Validation control-plane/.../db/integration-settings.ts positive-number / positive-integer validators
Resolved API control-plane/.../routes/integration-settings.ts exposes both (null when unset)
Boundary control-plane/.../lifecycle/manager.ts parseSandboxSettings forwards positive values
Data plane modal-infra/.../manager.py _resource_kwargs → Modal kwargs at both create sites
UI web/.../sandbox-settings.tsx CPU/Memory inputs; "provider default" when blank

Inheritance guard

Per-repo saves only persist a resource value when it's global, explicitly edited, or already an override — so saving unrelated repo settings no longer pins an inherited global default as a repo override (mirrors the existing child-session-limit behavior). Covered by two regression tests.

Testing

  • control-plane unit (validation, parse forward/drop) and integration
    (resolved-config defaults) — pass
  • web (render, save payload, positivity-block, inheritance) — pass
  • modal-infra (_resource_kwargs mapping + create/restore forwarding) — pass
  • lint, typecheck, ruff — clean

Notes / out of scope

  • Daytona ignores the fields (no resource API); intentional.
  • Warm-pool path is dormant (sessions always create fresh) — not threaded.

Summary by CodeRabbit

  • New Features

    • Added CPU (fractional) and Memory (MiB, integer) sandbox resource reservations with a "Resources" editor; supports global defaults, per-repo overrides, and explicit null to indicate no override.
    • Resolved sandbox config now surfaces cpuCores and memoryMib (or null when not overridden).
    • Provider integration now forwards CPU/memory to sandbox creation when set.
  • Bug Fixes / Validation

    • Stronger validation: CPU must be positive finite, Memory must be positive integer; invalid values are omitted or rejected and not persisted.
  • Tests

    • Expanded unit and integration tests covering UI, validation, persistence, resolved config, and provider-forwarding.

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds optional sandbox resource reservation fields cpuCores and memoryMib, validates them across DB and parsing layers, exposes them on the resolved-config API (null when unset), maps them into Modal create kwargs, and provides UI editor + tests for global and per-repo overrides.

Changes

Sandbox Resource Reservations

Layer / File(s) Summary
Type contract and documentation
packages/shared/src/types/integrations.ts
SandboxSettings adds optional cpuCores (number) and memoryMib (number) with JSDoc describing provider-agnostic advisory reservations.
Sandbox settings normalization module
packages/control-plane/src/sandbox/settings.ts, packages/control-plane/src/sandbox/settings.test.ts
New normalizeSandboxSettings with configurable invalid behavior (throw/omit), tunnel-port and positive-integer helpers, and tests covering throw/omit behavior and stored normalization.
Database validation and integration tests
packages/control-plane/src/db/integration-settings.ts, packages/control-plane/src/db/integration-settings.test.ts
DB read/write paths now normalize sandbox settings on read and validate/normalize on write (throwing on invalid when configured); tests cover fractional CPU round-trip, null repo overrides preservation, and write-time validation failures for cpu/memory.
Control-plane session lifecycle parsing
packages/control-plane/src/sandbox/lifecycle/manager.ts, packages/control-plane/src/sandbox/lifecycle/manager.test.ts
parseSandboxSettings() delegates to normalizeSandboxSettings(..., { invalid: "omit" }); lifecycle tests verify forwarding of valid cpu/memory and omission of non-positive values when spawning provider sandboxes.
API resolved-config endpoint response
packages/control-plane/src/routes/integration-settings.ts, packages/control-plane/test/integration/integration-settings.test.ts
handleGetResolvedConfig includes cpuCores and memoryMib for sandbox config, returning null when no override is configured; integration tests updated to assert the new fields.
Modal sandbox resource kwargs translation
packages/modal-infra/src/sandbox/manager.py, packages/modal-infra/tests/test_sandbox_resources.py
Adds _resource_kwargs() to map cpuCorescpu (float) and memoryMibmemory; create_sandbox and restore_from_snapshot forward these kwargs; unit and async tests added.
Web UI settings editor component logic
packages/web/src/components/settings/sandbox-settings.tsx
Adds validation helpers, local state and resolved strings for CPU/memory, save-time validation with error messages, inheritance-aware payload construction (avoid pinning inherited defaults), reset behavior, change detection, and a new Resources UI section.
Web UI resource reservations test suite
packages/web/src/components/settings/sandbox-settings.test.tsx
Tests cover empty defaults, rendering of global defaults, global save payload including resource fields, validation blocking for non-positive values, repo inheritance without persisting overrides, explicit repo override persistence, clearing overrides, and preservation of explicit null overrides.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SandboxSettingsComponent
  participant APIEndpoint
  participant SettingsStore

  User->>SandboxSettingsComponent: Edit CPU cores or memory
  SandboxSettingsComponent->>SandboxSettingsComponent: Update local state (resolvedCpuCores/resolvedMemoryMib)
  SandboxSettingsComponent->>SandboxSettingsComponent: Enable Save button when values differ
  User->>SandboxSettingsComponent: Click Save
  SandboxSettingsComponent->>SandboxSettingsComponent: Validate cpuCores positive and memoryMib positive integer
  alt Validation fails
    SandboxSettingsComponent->>User: Show error message
  else Validation passes
    SandboxSettingsComponent->>APIEndpoint: PUT with conditional cpuCores/memoryMib payload
    APIEndpoint->>SettingsStore: Persist resource settings
    APIEndpoint-->>SandboxSettingsComponent: Success response
    SandboxSettingsComponent->>SandboxSettingsComponent: Reset cpu/memory edit state
    SandboxSettingsComponent->>User: Show success feedback
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • ColeMurray

Poem

🐰 I nibble code in tidy rows,
CPU and memory in my nose,
From types to Modal, UI in sight,
Tests hop in—everything’s right,
A short swift hop, and we ship tonight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding configurable CPU and memory settings for Modal sandboxes. It accurately reflects the primary purpose of the changeset across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/control-plane/test/integration/integration-settings.test.ts (1)

578-602: ⚡ Quick win

Consider adding a positive test case for configured resource reservations.

The test correctly validates that unset resource reservations resolve to null. However, it would strengthen coverage to also test the case where cpuCores and memoryMib are explicitly configured (e.g., via global or repo settings) and verify they're correctly returned in the resolved config response.

Other integration tests in this file follow this pattern—testing both unconfigured defaults and configured overrides (see lines 200-242 for the GitHub settings example).

📋 Example test case
it("returns configured resource reservations in resolved config", async () => {
  const headers = await authHeaders();

  // Configure global resource settings
  await SELF.fetch("https://test.local/integration-settings/sandbox", {
    method: "PUT",
    headers,
    body: JSON.stringify({
      settings: {
        defaults: { cpuCores: 2, memoryMib: 4096 },
      },
    }),
  });

  const res = await SELF.fetch(
    "https://test.local/integration-settings/sandbox/resolved/acme/widgets",
    { headers }
  );
  expect(res.status).toBe(200);
  const body = await res.json<{
    config: {
      cpuCores: number | null;
      memoryMib: number | null;
    };
  }>();
  expect(body.config.cpuCores).toBe(2);
  expect(body.config.memoryMib).toBe(4096);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/test/integration/integration-settings.test.ts` around
lines 578 - 602, Add a positive test that configures resource reservations and
verifies they appear in the resolved config: use authHeaders() and SELF.fetch to
PUT a settings payload to "https://test.local/integration-settings/sandbox" (set
defaults.cpuCores and defaults.memoryMib), then call SELF.fetch on the resolved
endpoint used in the existing test (e.g.,
"https://test.local/integration-settings/sandbox/resolved/testowner/testrepo"),
assert 200, parse JSON and assert body.config.cpuCores and body.config.memoryMib
match the configured values (similar to the existing assertions around
DEFAULT_MAX_CONCURRENT_CHILD_SESSIONS / DEFAULT_MAX_TOTAL_CHILD_SESSIONS).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/modal-infra/src/sandbox/manager.py`:
- Around line 42-63: The _resource_kwargs function currently accepts cpu_cores
without rejecting NaN/Infinity; update the cpu_cores validation in
_resource_kwargs to also require a finite value (use
math.isfinite(float(cpu_cores))) before setting kwargs["cpu"], and import math
at the top of the module; keep the existing checks for numeric types and
positivity (i.e., ensure isinstance(cpu_cores, (int, float)), not
isinstance(cpu_cores, bool), math.isfinite(...), and cpu_cores > 0).

---

Nitpick comments:
In `@packages/control-plane/test/integration/integration-settings.test.ts`:
- Around line 578-602: Add a positive test that configures resource reservations
and verifies they appear in the resolved config: use authHeaders() and
SELF.fetch to PUT a settings payload to
"https://test.local/integration-settings/sandbox" (set defaults.cpuCores and
defaults.memoryMib), then call SELF.fetch on the resolved endpoint used in the
existing test (e.g.,
"https://test.local/integration-settings/sandbox/resolved/testowner/testrepo"),
assert 200, parse JSON and assert body.config.cpuCores and body.config.memoryMib
match the configured values (similar to the existing assertions around
DEFAULT_MAX_CONCURRENT_CHILD_SESSIONS / DEFAULT_MAX_TOTAL_CHILD_SESSIONS).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 429844ff-2f27-4e1c-96f0-c88a935bc20a

📥 Commits

Reviewing files that changed from the base of the PR and between 8ceafe9 and 65f6337.

📒 Files selected for processing (11)
  • packages/control-plane/src/db/integration-settings.test.ts
  • packages/control-plane/src/db/integration-settings.ts
  • packages/control-plane/src/routes/integration-settings.ts
  • packages/control-plane/src/sandbox/lifecycle/manager.test.ts
  • packages/control-plane/src/sandbox/lifecycle/manager.ts
  • packages/control-plane/test/integration/integration-settings.test.ts
  • packages/modal-infra/src/sandbox/manager.py
  • packages/modal-infra/tests/test_sandbox_resources.py
  • packages/shared/src/types/integrations.ts
  • packages/web/src/components/settings/sandbox-settings.test.tsx
  • packages/web/src/components/settings/sandbox-settings.tsx

Comment thread packages/modal-infra/src/sandbox/manager.py
Comment thread packages/web/src/components/settings/sandbox-settings.tsx Outdated
Comment thread packages/modal-infra/src/sandbox/manager.py Outdated

@ColeMurray ColeMurray left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comments

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/control-plane/src/sandbox/settings.ts`:
- Around line 127-143: The current loop always assigns result.tunnelPorts =
ports.slice(0, MAX_TUNNEL_PORTS) even when the input array yields no valid
ports, causing malformed overrides like ["bad"] to become an explicit empty
array and override defaults; change the logic to only assign result.tunnelPorts
when there is at least one validated port (ports.length > 0) or when you
explicitly intend to allow empty overrides—i.e., after the validation and
MAX_TUNNEL_PORTS check, wrap the assignment in a conditional that checks
ports.length > 0 (and keep using ports.slice(0, MAX_TUNNEL_PORTS)) so
invalid-only inputs are omitted instead of normalized to [].
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 76d04a98-c4f3-4887-bb30-8122bc6b402e

📥 Commits

Reviewing files that changed from the base of the PR and between 8de49cc and b60a3c7.

📒 Files selected for processing (11)
  • packages/control-plane/src/db/integration-settings.test.ts
  • packages/control-plane/src/db/integration-settings.ts
  • packages/control-plane/src/sandbox/lifecycle/manager.ts
  • packages/control-plane/src/sandbox/settings.test.ts
  • packages/control-plane/src/sandbox/settings.ts
  • packages/control-plane/test/integration/integration-settings.test.ts
  • packages/modal-infra/src/sandbox/manager.py
  • packages/modal-infra/tests/test_sandbox_resources.py
  • packages/shared/src/types/integrations.ts
  • packages/web/src/components/settings/sandbox-settings.test.tsx
  • packages/web/src/components/settings/sandbox-settings.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/shared/src/types/integrations.ts
  • packages/web/src/components/settings/sandbox-settings.tsx
  • packages/control-plane/src/db/integration-settings.test.ts

Comment thread packages/control-plane/src/sandbox/settings.ts
@vladgurovich vladgurovich requested a review from ColeMurray June 5, 2026 04:22
@vladgurovich

Copy link
Copy Markdown
Contributor Author

I addressed your feedback @ColeMurray, please take a look again 🙏

@vladgurovich

Copy link
Copy Markdown
Contributor Author

checking back in @ColeMurray -- anythng missing from this PR?

@ColeMurray ColeMurray merged commit 95dfa04 into ColeMurray:main Jun 9, 2026
18 checks passed
ColeMurray added a commit that referenced this pull request Jun 9, 2026
…#720)

## What

Follow-up cleanup to #693 (configurable Modal sandbox CPU/memory). Two
behavior-preserving changes:

- **web (`sandbox-settings.tsx`):** the cpu/memory override logic was
implemented with a heavier, parallel mechanism than the adjacent
child-session-limit fields — four helpers including a 7-argument
`applyResourcePayload` and a 3-field intermediate object. Collapsed to
two small helpers (`resourceDisplayValue`, `resourcePayloadValue`); the
inherit / explicit-override / explicit-`null` (provider-default)
tri-state is unchanged.
- **control-plane (`integration-settings.test.ts`):** added a regression
test pinning the `getResolvedConfig` merge-time
`normalizeSandboxSettings` pass.

## Why

The resource fields and the child-session-limit fields solve the same
inheritance-guard problem; #693 introduced a second, heavier pattern for
it. This narrows them and removes the bespoke indirection (net **−47
lines**).

The regression test closes a gap: the merge-time normalize in
`getResolvedConfig` is the **only** enforcement of `maxConcurrent <=
maxTotal` when the global defaults and the repo override are each
individually valid but the *merged* result is not. It had no committed
test, so a future refactor could silently drop it. The test is
mutation-verified (it fails if that normalize pass is removed).

## Behavior

No behavior change. The `prior !== undefined` reasoning relies on stored
JSON values never being `undefined`, so an inherited-only field is
skipped and an explicit `null` override is preserved — exactly as
before. Modal's `_resource_kwargs` mapping and every normalization pass
are untouched.

## Testing

- web: 25/25 `sandbox-settings` tests pass unchanged
- control-plane: `integration-settings` unit suite passes, +1 new
regression test (mutation-verified)
- typecheck clean (against real shared types), eslint clean


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Tests**
* Added a sandbox integration test that verifies config normalization
handles cross-field validation after merging defaults and overrides.
* Ensures invalid inverted concurrent/total limit cases are dropped by
the merged configuration.

* **Refactor**
* Improved resource override/inheritance handling for CPU and memory in
sandbox settings.
* Updated editor logic to correctly display values and persist payloads
while preserving prior override semantics.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants