Skip to content
Draft
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
385 changes: 195 additions & 190 deletions packages/api/internal/api/api.gen.go

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions packages/api/internal/handlers/snapshot_template_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,12 @@ func (a *APIStore) PostSandboxesSandboxIDSnapshots(c *gin.Context, sandboxID api
return
}

// Build opts from the optional name
// Build opts from the optional name. memory defaults to true (full memory
// snapshot); memory:false requests a filesystem-only snapshot template,
// derived from the memory checkpoint the source sandbox resumes from.
opts := orchestrator.SnapshotTemplateOpts{
Tag: id.DefaultTag,
Tag: id.DefaultTag,
FilesystemOnly: body.Memory != nil && !*body.Memory,
}

if body.Name != nil {
Expand Down
106 changes: 80 additions & 26 deletions packages/api/internal/orchestrator/snapshot_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/google/uuid"

"github.com/e2b-dev/infra/packages/api/internal/orchestrator/nodemanager"
"github.com/e2b-dev/infra/packages/api/internal/sandbox"
"github.com/e2b-dev/infra/packages/db/pkg/types"
"github.com/e2b-dev/infra/packages/db/queries"
Expand All @@ -32,6 +33,11 @@ type SnapshotTemplateOpts struct {
Namespace *string
// Tag is the build tag parsed from the name, defaults to "default".
Tag string
// FilesystemOnly makes the snapshot template persist only the filesystem:
// its build is derived from the memory checkpoint (sharing its rootfs data)
// and sandboxes created from it cold-boot. The source sandbox still gets a
// full memory checkpoint and resumes with its memory intact.
FilesystemOnly bool
}

// CreateSnapshotTemplate creates a persistent snapshot template from a running sandbox and immediately resumes it.
Expand Down Expand Up @@ -69,16 +75,33 @@ func (o *Orchestrator) CreateSnapshotTemplate(ctx context.Context, teamID uuid.U
return SnapshotTemplateResult{}, fmt.Errorf("node '%s' not found", sbx.NodeID)
}

// Snapshot templates are always memory snapshots; filesystem-only checkpoint
// (resume-in-place would need a reboot) is not supported yet.
// The sandbox's own snapshot row is always a full memory snapshot, even for
// a filesystem-only template: the sandbox is memory-resumed from it and it
// can be memory-resumed again later (auto-resume included).
upsertResult, err := o.throttledUpsertSnapshot(ctx, buildUpsertSnapshotParams(sbx, node, false))
if err != nil {
return SnapshotTemplateResult{}, fmt.Errorf("error upserting snapshot: %w", err)
}
buildIDs := []uuid.UUID{upsertResult.BuildID}

snapshotTemplateEnvID, err := o.resolveOrCreateSnapshotTemplate(ctx, sandboxID, teamID, upsertResult.BuildID, sbx.NodeID, sbx.ClusterID, opts)
// A filesystem-only template gets its own derived build that shares the
// memory checkpoint's rootfs data via header references but carries no
// memory snapshot, so sandboxes created from it cold-boot. Otherwise the
// template reuses the memory build directly.
templateBuildID := upsertResult.BuildID
if opts.FilesystemOnly {
templateBuildID, err = o.sqlcDB.CreateSnapshotTemplateBuild(ctx, buildSnapshotTemplateBuildParams(sbx, node))
if err != nil {
o.failSnapshotBuilds(ctx, buildIDs, err)

return SnapshotTemplateResult{}, fmt.Errorf("error creating filesystem-only template build: %w", err)
}
buildIDs = append(buildIDs, templateBuildID)
}

snapshotTemplateEnvID, err := o.resolveOrCreateSnapshotTemplate(ctx, sandboxID, teamID, templateBuildID, sbx.NodeID, sbx.ClusterID, opts)
if err != nil {
o.failSnapshotBuild(ctx, upsertResult.BuildID, err)
o.failSnapshotBuilds(ctx, buildIDs, err)

return SnapshotTemplateResult{}, err
}
Expand All @@ -87,14 +110,23 @@ func (o *Orchestrator) CreateSnapshotTemplate(ctx context.Context, teamID uuid.U
// orchestrator with the same ExecutionID. On error the orchestrator
// kills the sandbox itself; RemoveSandbox is still needed to clean up
// API-side state (store, routing, analytics).
client, childCtx := node.GetClient(ctx)
_, err = client.Sandbox.Checkpoint(childCtx, &orchestrator.SandboxCheckpointRequest{
checkpointReq := &orchestrator.SandboxCheckpointRequest{
SandboxId: sbx.SandboxID,
BuildId: upsertResult.BuildID.String(),
Metadata: map[string]string{storageopts.ObjectMetadataTemplateID: snapshotTemplateEnvID},
})
}
if opts.FilesystemOnly {
// The memory build belongs to the sandbox's own snapshot env (like a
// pause build); the derived build is the one owned by the template env.
checkpointReq.Metadata = map[string]string{storageopts.ObjectMetadataTemplateID: upsertResult.TemplateID}
checkpointReq.FilesystemBuildId = templateBuildID.String()
checkpointReq.FilesystemMetadata = map[string]string{storageopts.ObjectMetadataTemplateID: snapshotTemplateEnvID}
}

client, childCtx := node.GetClient(ctx)
_, err = client.Sandbox.Checkpoint(childCtx, checkpointReq)
if err != nil {
o.failSnapshotBuild(ctx, upsertResult.BuildID, err)
o.failSnapshotBuilds(ctx, buildIDs, err)

// Complete the snapshotting transition with error — leaves state as
// Snapshotting (no restore to Running) and clears the transition key
Expand All @@ -109,14 +141,16 @@ func (o *Orchestrator) CreateSnapshotTemplate(ctx context.Context, teamID uuid.U
}

now := time.Now()
err = o.sqlcDB.UpdateEnvBuildStatus(ctx, queries.UpdateEnvBuildStatusParams{
Status: types.BuildStatusUploaded,
FinishedAt: &now,
Reason: types.BuildReason{},
BuildID: upsertResult.BuildID,
})
if err != nil {
return SnapshotTemplateResult{}, fmt.Errorf("error updating build status: %w", err)
for _, buildID := range buildIDs {
err = o.sqlcDB.UpdateEnvBuildStatus(ctx, queries.UpdateEnvBuildStatusParams{
Status: types.BuildStatusUploaded,
FinishedAt: &now,
Reason: types.BuildReason{},
BuildID: buildID,
})
if err != nil {
return SnapshotTemplateResult{}, fmt.Errorf("error updating build status: %w", err)
}
}

o.snapshotCache.Invalidate(context.WithoutCancel(ctx), sandboxID)
Expand All @@ -125,19 +159,39 @@ func (o *Orchestrator) CreateSnapshotTemplate(ctx context.Context, teamID uuid.U

return SnapshotTemplateResult{
TemplateID: snapshotTemplateEnvID,
BuildID: upsertResult.BuildID,
BuildID: templateBuildID,
}, nil
}

func (o *Orchestrator) failSnapshotBuild(ctx context.Context, buildID uuid.UUID, cause error) {
err := o.sqlcDB.UpdateEnvBuildStatus(ctx, queries.UpdateEnvBuildStatusParams{
Status: types.BuildStatusFailed,
FinishedAt: new(time.Now()),
Reason: types.BuildReason{Message: cause.Error()},
BuildID: buildID,
})
if err != nil {
telemetry.ReportError(ctx, "error failing build", err)
// buildSnapshotTemplateBuildParams mirrors buildUpsertSnapshotParams' build
// fields for the derived filesystem-only template build: same shape and CPU
// pinning to the source build, just not tied to the sandbox's snapshot env.
func buildSnapshotTemplateBuildParams(sbx sandbox.Sandbox, node *nodemanager.Node) queries.CreateSnapshotTemplateBuildParams {
return queries.CreateSnapshotTemplateBuildParams{
Vcpu: sbx.VCpu,
RamMb: sbx.RamMB,
FreeDiskSizeMb: 0,
KernelVersion: sbx.KernelVersion,
FirecrackerVersion: sbx.FirecrackerVersion,
EnvdVersion: &sbx.EnvdVersion,
Status: types.BuildStatusSnapshotting,
OriginNodeID: &node.ID,
TotalDiskSizeMb: &sbx.TotalDiskSizeMB,
SourceBuildID: sbx.BuildID,
}
}

func (o *Orchestrator) failSnapshotBuilds(ctx context.Context, buildIDs []uuid.UUID, cause error) {
for _, buildID := range buildIDs {
err := o.sqlcDB.UpdateEnvBuildStatus(ctx, queries.UpdateEnvBuildStatusParams{
Status: types.BuildStatusFailed,
FinishedAt: new(time.Now()),
Reason: types.BuildReason{Message: cause.Error()},
BuildID: buildID,
})
if err != nil {
telemetry.ReportError(ctx, "error failing build", err)
}
}
}

Expand Down
87 changes: 87 additions & 0 deletions packages/db/queries/create_snapshot_template_build.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- name: CreateSnapshotTemplateBuild :one
-- Creates a standalone env_builds row for a snapshot-template build that is
-- derived from (and shares data with) another build of the same checkpoint,
-- e.g. the filesystem-only sibling of a memory checkpoint. CPU info is copied
-- from the source build so the derived build keeps the snapshot's CPU
-- compatibility pinned to the original build, mirroring UpsertSnapshot.
INSERT INTO "public"."env_builds" (
vcpu,
ram_mb,
free_disk_size_mb,
kernel_version,
firecracker_version,
envd_version,
status,
cluster_node_id,
total_disk_size_mb,
updated_at,
cpu_architecture,
cpu_family,
cpu_model,
cpu_model_name,
cpu_flags
)
VALUES (
@vcpu,
@ram_mb,
@free_disk_size_mb,
@kernel_version,
@firecracker_version,
@envd_version,
@status,
@origin_node_id,
@total_disk_size_mb,
now(),
(SELECT eb.cpu_architecture FROM "public"."env_builds" eb WHERE eb.id = @source_build_id),
(SELECT eb.cpu_family FROM "public"."env_builds" eb WHERE eb.id = @source_build_id),
(SELECT eb.cpu_model FROM "public"."env_builds" eb WHERE eb.id = @source_build_id),
(SELECT eb.cpu_model_name FROM "public"."env_builds" eb WHERE eb.id = @source_build_id),
(SELECT eb.cpu_flags FROM "public"."env_builds" eb WHERE eb.id = @source_build_id)
)
RETURNING id as build_id;
2 changes: 2 additions & 0 deletions packages/orchestrator/cmd/create-build/proxyport_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build linux

package main

import "testing"
Expand Down
2 changes: 2 additions & 0 deletions packages/orchestrator/cmd/inspect-build/inspect.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build linux

package main

import (
Expand Down
2 changes: 2 additions & 0 deletions packages/orchestrator/cmd/inspect-build/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build linux

package main

import (
Expand Down
2 changes: 2 additions & 0 deletions packages/orchestrator/cmd/inspect-build/render_human.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build linux

package main

import (
Expand Down
2 changes: 2 additions & 0 deletions packages/orchestrator/cmd/inspect-build/render_json.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build linux

package main

import (
Expand Down
2 changes: 2 additions & 0 deletions packages/orchestrator/cmd/inspect-build/report.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build linux

package main

import (
Expand Down
2 changes: 2 additions & 0 deletions packages/orchestrator/cmd/inspect-build/report_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build linux

package main

import (
Expand Down
2 changes: 2 additions & 0 deletions packages/orchestrator/cmd/inspect-build/validate.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build linux

package main

import (
Expand Down
2 changes: 2 additions & 0 deletions packages/orchestrator/cmd/inspect-build/validate_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build linux

package main

import (
Expand Down
2 changes: 2 additions & 0 deletions packages/orchestrator/cmd/resume-build/gdb.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build linux

package main

import (
Expand Down
2 changes: 2 additions & 0 deletions packages/orchestrator/cmd/resume-build/gdb_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build linux

package main

import (
Expand Down
11 changes: 11 additions & 0 deletions packages/orchestrator/orchestrator.proto
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,17 @@ message SandboxCheckpointRequest {
// index (e.g. template_id). Opaque to the orchestrator, which just forwards
// it to object metadata.
map<string, string> metadata = 4;

// When set, the checkpoint additionally derives a filesystem-only sibling
// build under this ID: it shares the checkpoint's rootfs data via header
// references but carries no memory snapshot, so sandboxes created from it
// cold-boot (reboot) from the rootfs. The main build_id snapshot stays a
// full memory snapshot that the source sandbox is resumed from, exactly
// like a plain checkpoint. Unset = no derived build (existing behavior).
string filesystem_build_id = 5;

// Provenance for the sibling build's storage objects, analogous to metadata.
map<string, string> filesystem_metadata = 6;
}

message SandboxCheckpointResponse {
Expand Down
Loading
Loading