Skip to content

Commit 0aabc3f

Browse files
committed
feat(api,orch): add reboot escape hatch for sandbox resume
Adds an optional `reboot` flag on the resume endpoint that boots the sandbox from its snapshot filesystem with fresh memory, discarding the memory snapshot. Useful when memfile restore is slow or corrupted. The flag flows via gRPC metadata to the orchestrator, which creates an empty memfile and cold-boots the VM through systemd. Existing paths are unchanged when the flag is not set.
1 parent b1646ca commit 0aabc3f

11 files changed

Lines changed: 277 additions & 179 deletions

File tree

‎packages/api/internal/api/api.gen.go‎

Lines changed: 173 additions & 169 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/api/internal/handlers/proxy_grpc.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ func (s *SandboxService) ResumeSandbox(ctx context.Context, req *proxygrpc.Sandb
257257
s.api.buildResumeSandboxData(sandboxID, nil),
258258
&headers,
259259
true,
260+
false,
260261
nil, // mcp
261262
)
262263
if apiErr != nil {

‎packages/api/internal/handlers/sandbox.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ func (a *APIStore) startSandbox(
2828
getSandboxData orchestrator.SandboxDataFetcher,
2929
requestHeader *http.Header,
3030
isResume bool,
31+
rebootFromRootfs bool,
3132
mcp api.Mcp,
3233
) (*api.Sandbox, *api.APIError) {
3334
sbx, apiErr := a.startSandboxInternal(
@@ -38,6 +39,7 @@ func (a *APIStore) startSandbox(
3839
getSandboxData,
3940
requestHeader,
4041
isResume,
42+
rebootFromRootfs,
4143
mcp,
4244
)
4345
if apiErr != nil {
@@ -56,6 +58,7 @@ func (a *APIStore) startSandboxInternal(
5658
getSandboxData orchestrator.SandboxDataFetcher,
5759
requestHeader *http.Header,
5860
isResume bool,
61+
rebootFromRootfs bool,
5962
mcp api.Mcp,
6063
) (sandbox.Sandbox, *api.APIError) {
6164
startTime := time.Now()
@@ -77,6 +80,7 @@ func (a *APIStore) startSandboxInternal(
7780
timeout,
7881
isResume,
7982
creationMeta,
83+
rebootFromRootfs,
8084
)
8185
if instanceErr != nil {
8286
telemetry.ReportError(ctx, "error when creating instance", instanceErr.Err)

‎packages/api/internal/handlers/sandbox_connect.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ func (a *APIStore) PostSandboxesSandboxIDConnect(c *gin.Context, sandboxID api.S
152152
a.buildResumeSandboxData(sandboxID, nil),
153153
&c.Request.Header,
154154
true,
155+
false,
155156
nil, // mcp
156157
)
157158
if createErr != nil {

‎packages/api/internal/handlers/sandbox_create.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ func (a *APIStore) PostSandboxes(c *gin.Context) {
265265
getSandboxData,
266266
&c.Request.Header,
267267
false,
268+
false,
268269
mcp,
269270
)
270271
if createErr != nil {

‎packages/api/internal/handlers/sandbox_resume.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func (a *APIStore) PostSandboxesSandboxIDResume(c *gin.Context, sandboxID api.Sa
5454
}
5555

5656
telemetry.ReportEvent(ctx, "Parsed body")
57+
rebootFromRootfs := body.Reboot != nil && *body.Reboot
5758

5859
timeout := sandbox.SandboxTimeoutDefault
5960
if body.Timeout != nil {
@@ -160,6 +161,7 @@ func (a *APIStore) PostSandboxesSandboxIDResume(c *gin.Context, sandboxID api.Sa
160161
a.buildResumeSandboxData(sandboxID, body.AutoPause),
161162
&c.Request.Header,
162163
true,
164+
rebootFromRootfs,
163165
nil, // mcp
164166
)
165167
if createErr != nil {

‎packages/api/internal/orchestrator/create_instance.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"go.opentelemetry.io/otel/attribute"
1111
"go.opentelemetry.io/otel/metric"
1212
"go.uber.org/zap"
13+
"google.golang.org/grpc/metadata"
1314
"google.golang.org/protobuf/types/known/timestamppb"
1415

1516
"github.com/e2b-dev/infra/packages/api/internal/api"
@@ -127,6 +128,7 @@ func (o *Orchestrator) CreateSandbox(
127128
timeout time.Duration,
128129
isResume bool,
129130
creationMeta sandbox.CreationMetadata,
131+
rebootFromRootfsOpt ...bool,
130132
) (sbx sandbox.Sandbox, apiErr *api.APIError) {
131133
ctx, childSpan := tracer.Start(ctx, "create-sandbox")
132134
defer childSpan.End()
@@ -296,6 +298,10 @@ func (o *Orchestrator) CreateSandbox(
296298
}
297299
}
298300

301+
if len(rebootFromRootfsOpt) > 0 && rebootFromRootfsOpt[0] {
302+
ctx = metadata.AppendToOutgoingContext(ctx, orchestrator.SandboxRebootFromRootfsGRPCMetadataKey, "true")
303+
}
304+
299305
nodeClusterID := clusters.WithClusterFallback(team.ClusterID)
300306
clusterNodes := o.GetClusterNodes(nodeClusterID)
301307

‎packages/orchestrator/pkg/server/sandboxes.go‎

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,16 @@ import (
2424
"google.golang.org/protobuf/types/known/emptypb"
2525
"google.golang.org/protobuf/types/known/timestamppb"
2626

27+
grpcmetadata "google.golang.org/grpc/metadata"
28+
2729
"github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox"
30+
"github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/block"
2831
"github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/fc"
2932
sbxtemplate "github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/template"
33+
configpkg "github.com/e2b-dev/infra/packages/orchestrator/pkg/template/build/config"
34+
"github.com/e2b-dev/infra/packages/orchestrator/pkg/template/constants"
3035
"github.com/e2b-dev/infra/packages/orchestrator/pkg/template/metadata"
36+
"github.com/e2b-dev/infra/packages/orchestrator/pkg/units"
3137
"github.com/e2b-dev/infra/packages/shared/pkg/events"
3238
"github.com/e2b-dev/infra/packages/shared/pkg/featureflags"
3339
"github.com/e2b-dev/infra/packages/shared/pkg/grpc/orchestrator"
@@ -58,6 +64,66 @@ const (
5864
killReasonUnknown = "unknown"
5965
)
6066

67+
func rebootFromRootfsEnabled(ctx context.Context) bool {
68+
values := grpcmetadata.ValueFromIncomingContext(ctx, orchestrator.SandboxRebootFromRootfsGRPCMetadataKey)
69+
return len(values) > 0 && values[0] == "true"
70+
}
71+
72+
func (s *Server) createSandboxFromRootfs(
73+
ctx context.Context,
74+
template sbxtemplate.Template,
75+
config *sandbox.Config,
76+
runtime sandbox.RuntimeMetadata,
77+
req *orchestrator.SandboxCreateRequest,
78+
) (*sandbox.Sandbox, error) {
79+
memfile, err := block.NewEmpty(
80+
units.MBToBytes(config.RamMB),
81+
configpkg.MemfilePageSize(config.HugePages),
82+
uuid.MustParse(template.Files().BuildID),
83+
)
84+
if err != nil {
85+
return nil, fmt.Errorf("create empty memfile: %w", err)
86+
}
87+
defer memfile.Close()
88+
89+
maskedTemplate := sbxtemplate.NewMaskTemplate(template, sbxtemplate.WithMemfile(memfile))
90+
91+
kvmClock, err := utils.IsGTEVersion(config.Envd.Version, "0.2.11")
92+
if err != nil {
93+
return nil, fmt.Errorf("compare envd version: %w", err)
94+
}
95+
96+
timeout := req.GetEndTime().AsTime().Sub(req.GetStartTime().AsTime())
97+
if timeout <= 0 {
98+
timeout = s.config.EnvdTimeout
99+
}
100+
101+
sbx, err := s.sandboxFactory.CreateSandbox(
102+
ctx,
103+
config,
104+
runtime,
105+
maskedTemplate,
106+
timeout,
107+
"",
108+
fc.ProcessOptions{
109+
InitScriptPath: constants.SystemdInitPath,
110+
KvmClock: kvmClock,
111+
},
112+
req.GetSandbox(),
113+
nil,
114+
)
115+
if err != nil {
116+
return nil, err
117+
}
118+
119+
if err := sbx.WaitForEnvd(ctx, timeout); err != nil {
120+
closeErr := sbx.Close(context.WithoutCancel(ctx))
121+
return nil, errors.Join(fmt.Errorf("wait for envd after rootfs reboot: %w", err), closeErr)
122+
}
123+
124+
return sbx, nil
125+
}
126+
61127
func (s *Server) Create(ctx context.Context, req *orchestrator.SandboxCreateRequest) (_ *orchestrator.SandboxCreateResponse, createErr error) {
62128
// set max request timeout for this request
63129
ctx, cancel := context.WithTimeoutCause(ctx, requestTimeout, errors.New("request timed out"))
@@ -187,18 +253,22 @@ func (s *Server) Create(ctx context.Context, req *orchestrator.SandboxCreateRequ
187253
SandboxType: sandbox.SandboxTypeSandbox,
188254
}
189255

190-
sbx, err := s.sandboxFactory.ResumeSandbox(
191-
ctx,
192-
template,
193-
config,
194-
runtime,
195-
req.GetStartTime().AsTime(),
196-
req.GetEndTime().AsTime(),
197-
req.GetSandbox(),
198-
)
256+
var sbx *sandbox.Sandbox
257+
if rebootFromRootfsEnabled(ctx) {
258+
sbx, err = s.createSandboxFromRootfs(ctx, template, config, runtime, req)
259+
} else {
260+
sbx, err = s.sandboxFactory.ResumeSandbox(
261+
ctx,
262+
template,
263+
config,
264+
runtime,
265+
req.GetStartTime().AsTime(),
266+
req.GetEndTime().AsTime(),
267+
req.GetSandbox(),
268+
)
269+
}
199270
if err != nil {
200271
if errors.Is(err, storage.ErrObjectNotExist) {
201-
// Snapshot data not found, let the API know the data aren't probably upload yet
202272
telemetry.ReportError(ctx, "sandbox files not found", err, telemetry.WithSandboxID(req.GetSandbox().GetSandboxId()))
203273

204274
return nil, status.Errorf(codes.FailedPrecondition, "sandbox files for '%s' not found", req.GetSandbox().GetSandboxId())
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package orchestrator
2+
3+
const SandboxRebootFromRootfsGRPCMetadataKey = "x-e2b-reboot-from-rootfs"

‎spec/openapi.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,9 @@ components:
724724
type: boolean
725725
deprecated: true
726726
description: Automatically pauses the sandbox after the timeout
727+
reboot:
728+
type: boolean
729+
description: Boot from the snapshot filesystem with fresh memory instead of restoring the memory snapshot.
727730

728731
ConnectSandbox:
729732
type: object

0 commit comments

Comments
 (0)