Documentation
¶
Overview ¶
Package sandbox owns the in-memory registry of active sandboxes and drives their lifecycle: minting IDs and tokens, delegating container creation to the runtime, handshaking with the in-sandbox agent, and reaping sandboxes whose TTL has lapsed.
The Manager deliberately does not own HTTP — control-plane handlers (internal/api/control) call its exported methods, and the reverse proxy (internal/api/proxy) consults it for sandbox lookups. Keeping HTTP and state separate makes both easier to unit-test.
Index ¶
- Constants
- Variables
- func NewEnvdToken() string
- func NewSandboxID() string
- func NewTrafficToken() string
- type Clock
- type CreateOptions
- type Manager
- func (m *Manager) Connect(ctx context.Context, id string, timeout time.Duration) (*Sandbox, error)
- func (m *Manager) Create(ctx context.Context, opts CreateOptions) (*Sandbox, error)
- func (m *Manager) Destroy(ctx context.Context, id string) error
- func (m *Manager) Domain() string
- func (m *Manager) EnforceTimeouts(ctx context.Context) []string
- func (m *Manager) Get(id string) (*Sandbox, error)
- func (m *Manager) List() []*Sandbox
- func (m *Manager) Pause(ctx context.Context, id string) error
- func (m *Manager) PausePolicy() PausePolicy
- func (m *Manager) Rehydrate(ctx context.Context, defaultTimeout time.Duration) (int, error)
- func (m *Manager) Resume(ctx context.Context, id string) error
- func (m *Manager) Run(ctx context.Context, interval time.Duration)
- func (m *Manager) SetTimeout(id string, timeout time.Duration) error
- func (m *Manager) Snapshot(ctx context.Context, id, name string) (*SnapshotInfo, error)
- func (m *Manager) Stop(ctx context.Context, id string) error
- type OnTimeoutMode
- type Options
- type PauseMode
- type PausePolicy
- type Sandbox
- type SnapshotInfo
- type State
- type TemplateResolution
- type TemplateResolver
Constants ¶
const ( LabelTemplateID = "edvabe.sandbox.template.id" LabelTemplateAlias = "edvabe.sandbox.template.alias" LabelTokenEnvd = "edvabe.sandbox.token.envd" LabelTokenTraffic = "edvabe.sandbox.token.traffic" LabelOnTimeout = "edvabe.sandbox.ontimeout" )
Docker labels edvabe stamps at Create time so Rehydrate can reconstruct the in-memory Sandbox after edvabe restarts. Only immutable facts belong here — Docker labels can't be modified after a container is created. Mutable state (PauseMode, PausedAt) is derived from the live container state instead.
const ( // DefaultImage is the image tag used when CreateOptions.TemplateID // is empty. Phase 1 resolves every templateID to this. DefaultImage = "edvabe/base:latest" // DefaultDomain is the host:port edvabe reports back in the // Sandbox.domain field so SDKs route data-plane calls back to us. DefaultDomain = "localhost:3000" // DefaultTimeout is applied when CreateOptions.Timeout <= 0. DefaultTimeout = 5 * time.Minute // WatchdogInterval is the ticker cadence of Run's EnforceTimeouts // loop. Chosen to be much smaller than the smallest realistic // timeout while still being cheap. WatchdogInterval = 1 * time.Second // DefaultFreezeDuration caps how long a paused sandbox stays in // `docker pause` (holding RAM) before being demoted to `docker // stop` to free host memory. A day covers the "pause overnight, // resume next morning" case without letting forgotten sandboxes // hog RAM forever. DefaultFreezeDuration = 24 * time.Hour // DefaultMaxFrozen caps how many sandboxes can hold RAM via docker // pause at once. Further pauses demote the oldest frozen sandbox // first (LRU by PausedAt). Zero disables the cap. DefaultMaxFrozen = 10 // DefaultStoppedGCAfter is how long a stopped (demoted) sandbox // lingers before the reaper destroys it to reclaim disk. A month // is generous; users who pause work for longer than that can // snapshot explicitly. DefaultStoppedGCAfter = 30 * 24 * time.Hour )
Variables ¶
var ( // ErrNotFound is returned when a lookup targets a sandbox ID the // Manager has no record of. ErrNotFound = errors.New("sandbox: not found") // ErrExpired is returned when a sandbox is still in the map but its // ExpiresAt has lapsed — the next EnforceTimeouts pass will reap it. ErrExpired = errors.New("sandbox: expired") )
Sentinel errors so handlers can discriminate without string matching.
var ErrTemplateNotFound = errors.New("sandbox: template not found")
ErrTemplateNotFound signals the resolver has no record of the given template. The manager treats this as "use the base image" so Phase 1 sandbox IDs like "base" and empty strings keep working.
Functions ¶
func NewEnvdToken ¶
func NewEnvdToken() string
NewEnvdToken returns "ea_" + 22 random base64url characters. Handed to envd via /init and echoed back to the SDK as envdAccessToken.
func NewSandboxID ¶
func NewSandboxID() string
NewSandboxID returns "isb_" followed by 16 random base32 characters. Not a formal ULID — E2B clients only check the prefix, not the structure, and the prefix alone is enough insurance against parsing code that trims it.
func NewTrafficToken ¶
func NewTrafficToken() string
NewTrafficToken returns "ta_" + 22 random base64url characters. Reported to the SDK as trafficAccessToken; edvabe does not enforce it (no real auth in v1) but SDKs expect it to be present.
Types ¶
type Clock ¶
Clock is a tiny injection point so tests can drive EnforceTimeouts deterministically without wall-clock sleeping. Production uses realClock which defers to time.Now.
type CreateOptions ¶
type CreateOptions struct {
TemplateID string
Alias string
Metadata map[string]string
EnvVars map[string]string
Timeout time.Duration
// OnTimeout controls what EnforceTimeouts does when this sandbox
// expires. Empty string defaults to OnTimeoutKill (Phase 1 behaviour).
OnTimeout OnTimeoutMode
// CPUCount / MemoryMB override the template's resource limits for
// this one sandbox. Zero leaves the template value in place; to
// explicitly request unlimited on a capped template, pass a
// negative number.
CPUCount int
MemoryMB int
}
CreateOptions is the subset of the NewSandbox request body the Manager cares about. The control-plane handler translates HTTP JSON into this.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager holds the in-memory sandbox registry and drives create / destroy / timeout enforcement. It owns no HTTP machinery — callers in internal/api consume it through its exported methods.
func NewManager ¶
NewManager constructs a Manager. Runtime and Agent are required.
func (*Manager) Connect ¶
Connect renews the TTL on a live sandbox and returns the current snapshot. A paused sandbox is resumed through the runtime: frozen sandboxes get `docker unpause` (instant), stopped ones get `docker start` followed by a fresh agent Ping + InitAgent so envd has the access token again.
func (*Manager) Create ¶
Create mints a fresh sandbox, starts its container via the runtime, pings and initializes the in-sandbox agent, and registers the result. On any mid-flight failure the container is force-removed so nothing leaks to the runtime.
func (*Manager) Destroy ¶
Destroy removes the sandbox from the registry and stops its container. Registry is the source of truth: if runtime.Destroy fails after the sandbox is removed from the map, the error propagates but the Manager's state is still coherent. Stray containers can be reaped later via the edvabe.sandbox.id label.
func (*Manager) EnforceTimeouts ¶
EnforceTimeouts walks the registry and applies the three scheduled transitions:
- Running sandboxes whose ExpiresAt has lapsed: OnTimeoutKill destroys them, OnTimeoutPause freezes them via runtime.Pause.
- Frozen sandboxes paused longer than FreezeDuration, or any excess beyond MaxFrozen (oldest PausedAt first): demoted via runtime.Stop to free host RAM.
- Stopped sandboxes paused longer than StoppedGCAfter: destroyed to reclaim disk.
Returns the IDs touched by this sweep (killed + paused + demoted + gc'd). Runtime errors are logged implicitly by being returned from the underlying calls; failures don't abort the sweep so one stuck sandbox can't starve the others.
func (*Manager) List ¶
List returns a snapshot slice of all registered sandboxes. Order is unspecified — callers that need stable ordering should sort.
func (*Manager) Pause ¶
Pause freezes the container via runtime.Pause and flips the sandbox's State to paused with PauseMode=frozen. The sandbox stays in the registry — Connect unpauses it. Long-paused sandboxes are demoted to PauseStopped by the reaper; see EnforceTimeouts.
func (*Manager) PausePolicy ¶
func (m *Manager) PausePolicy() PausePolicy
PausePolicy returns the limits the reaper enforces.
func (*Manager) Rehydrate ¶ added in v0.2.0
Rehydrate repopulates the sandbox registry from containers already on the runtime. Called once on startup so paused / running sandboxes survive edvabe restarts. Sandbox-level metadata (template id, tokens, on-timeout) is recovered from Docker labels stamped at Create time; Mutable fields that aren't persisted (ExpiresAt, PausedAt) get defaults: ExpiresAt = now + defaultTimeout for running sandboxes (SDK typically re-extends via SetTimeout anyway), PausedAt = now for paused sandboxes (the reaper will then hold them for FreezeDuration before demoting, which is conservative but safe).
Per-container failures are logged and skipped — one orphan must not prevent edvabe from starting. Containers labeled edvabe.managed=true but missing the sandbox-id label are left alone; the operator can reap them by hand.
func (*Manager) Resume ¶
Resume brings a paused sandbox back to running without touching its TTL — unlike Connect, which also renews the deadline. Exists for dashboard / ops flows that want to inspect a paused sandbox without silently extending its life. No-op for already-running sandboxes.
func (*Manager) Run ¶
Run drives a ticker-based timeout watchdog until ctx is cancelled. Intended to be launched as a goroutine from the serve subcommand. Pass 0 for interval to use WatchdogInterval.
func (*Manager) SetTimeout ¶
SetTimeout resets the sandbox TTL from the current clock. Returns ErrNotFound if the sandbox is unknown or ErrExpired if a *running* sandbox's TTL already lapsed. Paused sandboxes are exempt — same reasoning as Manager.Connect: they live on the pause-cycle reaper (FreezeDuration → demote, StoppedGCAfter → destroy), not the running-TTL. Extending a paused sandbox's timeout is normal SDK flow (e.g. `sandbox.setTimeout(...)` ahead of `connect()`).
func (*Manager) Snapshot ¶
Snapshot captures a container's writable filesystem layer as a new image tag via runtime.Commit. It is NOT a memory snapshot — running processes are not preserved. Pausing the sandbox first gives a consistent filesystem view but is not required.
func (*Manager) Stop ¶
Stop forces a sandbox into the stopped (docker stop) paused substate regardless of current state. Running sandboxes skip the freeze and go straight to stop; frozen sandboxes are unpaused first so `docker stop` can send signals to the processes; already-stopped sandboxes are a no-op. Exists so dashboard / ops flows can reclaim RAM without waiting for FreezeDuration.
type OnTimeoutMode ¶
type OnTimeoutMode string
OnTimeoutMode controls what EnforceTimeouts does to a sandbox once its ExpiresAt has lapsed. The default (OnTimeoutKill) destroys the container; OnTimeoutPause freezes it via runtime.Pause and leaves it in the registry for a later /connect to resume. Values are the same strings the E2B SDK sends in NewSandbox.lifecycle.onTimeout.
const ( OnTimeoutKill OnTimeoutMode = "kill" OnTimeoutPause OnTimeoutMode = "pause" )
type Options ¶
type Options struct {
Runtime runtime.Runtime
Agent agent.AgentProvider
Clock Clock
BaseImage string
Domain string
// Resolver maps templateID → (image, startCmd, readyCmd). Optional:
// when nil, every create resolves to BaseImage (Phase 1 behaviour).
Resolver TemplateResolver
// FreezeDuration is how long Pause keeps a sandbox in `docker pause`
// before the reaper demotes it to `docker stop`. Zero defaults to
// DefaultFreezeDuration. Negative disables demotion entirely.
FreezeDuration time.Duration
// MaxFrozen caps how many sandboxes hold RAM in docker-pause state
// at once. Zero defaults to DefaultMaxFrozen; negative disables the
// cap.
MaxFrozen int
// StoppedGCAfter is how long a demoted (stopped) sandbox lingers
// before the reaper destroys it. Zero defaults to
// DefaultStoppedGCAfter. Negative disables GC.
StoppedGCAfter time.Duration
}
Options configures NewManager.
type PauseMode ¶
type PauseMode string
PauseMode distinguishes the two kinds of paused container edvabe manages. Frozen holds RAM and resumes instantly; stopped releases RAM and requires a cold boot + agent re-init on resume. Meaningful only when State == StatePaused.
const ( // PauseFrozen means the container is held via `docker pause` — // processes are suspended, memory is resident, resume is a cheap // `docker unpause`. PauseFrozen PauseMode = "frozen" // PauseStopped means the container was demoted to `docker stop` to // free host memory. Resume requires `docker start` + agent re-init, // and in-memory process state is lost. PauseStopped PauseMode = "stopped" )
type PausePolicy ¶
PausePolicy reports the configured limits used by the reaper. Exposed so the dashboard / doctor can surface the effective policy.
type Sandbox ¶
type Sandbox struct {
ID string
TemplateID string
Alias string
ContainerID string
AgentHost string
AgentPort int
EnvdToken string
TrafficToken string
State State
// PauseMode is the substate when State == StatePaused. Empty
// otherwise. See PauseMode for the tradeoffs.
PauseMode PauseMode
// PausedAt records when the sandbox was most recently paused. Used
// by the reaper to demote long-frozen containers to stopped and to
// GC long-stopped containers. Zero when State != StatePaused.
PausedAt time.Time
OnTimeout OnTimeoutMode
Metadata map[string]string
EnvVars map[string]string
CreatedAt time.Time
ExpiresAt time.Time
// CPUCount / MemoryMB are the resource caps applied to the
// container. Zero means unlimited (Docker default). Sourced from
// the template resolution and per-sandbox overrides.
CPUCount int
MemoryMB int
}
Sandbox is edvabe's view of one active sandbox. Fields mutated over the sandbox's lifetime (State, ExpiresAt) are guarded by Manager.mu. Callers that receive a *Sandbox from the Manager MUST treat it as read-only — use Manager methods to mutate.
type SnapshotInfo ¶
SnapshotInfo is the return shape for Snapshot — the caller needs the tag to reference later and the point-in-time the snapshot was taken at for audit/logging.
type TemplateResolution ¶
type TemplateResolution struct {
ImageTag string
StartCmd string
ReadyCmd string
// CPUCount caps the sandbox to this many CPU cores via cgroup
// quota. Zero means unlimited (Docker default) — matching the
// pre-resource-limits behaviour.
CPUCount int
// MemoryMB caps the sandbox's RSS. Zero means unlimited.
MemoryMB int
}
TemplateResolution is the resolver's output.
type TemplateResolver ¶
type TemplateResolver interface {
Resolve(idOrAlias string) (TemplateResolution, error)
}
TemplateResolver maps a client-facing template identifier (alias or UUID) onto a concrete image tag plus the template's startCmd / readyCmd. The sandbox manager consults it at Create time — in Phase 1 this returns the base image unconditionally; Phase 3 supplies an adapter backed by the template store so user templates resolve transparently. Returning ErrTemplateNotFound falls back to the base image for backward compatibility with Phase 1 callers.