Skip to content

Commit 53ef5e1

Browse files
kalyazinclaude
andcommitted
feat(orch): serve resume from a provisional header during dedup
A resume that overlaps an in-flight pause of the same sandbox can block in the storage-template-memfile span: the local memfile template can't be built until the deduped header resolves, which only happens after dedup's compare+defrag. The in-flight memfd serving added earlier removes the drain wait but not this header wait, because it is upstream of the diff source. At pause, build a provisional local header (identity storage offsets, dirty pages attributed to a fresh provisional build id) from the dirty bitmap and the parent header, and register a memfd-backed diff source for that build id. The local template is built from the provisional header immediately, so a concurrent resume serves dirty pages straight from the still-mapped memfd instead of blocking. Once the deduped header resolves it is swapped in (SwapHeader), routing dirty pages to the compacted deduped diff; the distinct build id makes the swap race-free since build.File resolves the source from the same header snapshot as the offset. The provisional header is local-only: the upload continues to use the deduped header, so the persisted artifact is unchanged. Gated by memfd-dedup-inflight-serve (default off); composes with the in-flight drain serving (provisional covers the compare window, in-flight the drain). Signed-off-by: Nikita Kalyazin <nikita.kalyazin@e2b.dev> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7ec57a9 commit 53ef5e1

5 files changed

Lines changed: 129 additions & 13 deletions

File tree

packages/orchestrator/pkg/sandbox/sandbox.go

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,6 +1435,13 @@ func (s *Sandbox) Pause(
14351435
type MemorySnapshot struct {
14361436
Diff build.Diff
14371437
DiffHeader *DiffHeader
1438+
// ProvisionalDiffHeader + ProvisionalDiff, when non-nil, let the local
1439+
// template serve immediately from the still-mapped memfd via a distinct
1440+
// provisional build id while dedup runs, instead of blocking a concurrent
1441+
// resume in storage-template-memfile on the deduped header. They feed only
1442+
// the local AddSnapshot path; the upload still uses DiffHeader (deduped).
1443+
ProvisionalDiffHeader *header.Header
1444+
ProvisionalDiff build.Diff
14381445
// BlockSize is captured synchronously at Pause time because NewUpload's
14391446
// compression validation needs it before the async dedup header resolves;
14401447
// the dedup memfile path produces a page-granular Diff.BlockSize() that
@@ -1481,7 +1488,7 @@ func (s *Sandbox) processMemorySnapshot(ctx context.Context, buildID uuid.UUID)
14811488
}
14821489
}
14831490

1484-
memfileDiff, memfileDiffHeader, err := pauseProcessMemory(
1491+
memfileDiff, memfileDiffHeader, provMemfileHeader, provMemfileDiff, err := pauseProcessMemory(
14851492
ctx,
14861493
buildID,
14871494
memfileHeader,
@@ -1501,11 +1508,13 @@ func (s *Sandbox) processMemorySnapshot(ctx context.Context, buildID uuid.UUID)
15011508
}
15021509

15031510
return MemorySnapshot{
1504-
Diff: memfileDiff,
1505-
DiffHeader: memfileDiffHeader,
1506-
BlockSize: memfileHeader.Metadata.BlockSize,
1507-
header: memfileHeader,
1508-
newBytes: memfileDiffMetadata.Dirty.GetCardinality() * uint64(memfileDiffMetadata.BlockSize),
1511+
Diff: memfileDiff,
1512+
DiffHeader: memfileDiffHeader,
1513+
ProvisionalDiffHeader: provMemfileHeader,
1514+
ProvisionalDiff: provMemfileDiff,
1515+
BlockSize: memfileHeader.Metadata.BlockSize,
1516+
header: memfileHeader,
1517+
newBytes: memfileDiffMetadata.Dirty.GetCardinality() * uint64(memfileDiffMetadata.BlockSize),
15091518
}, nil
15101519
}
15111520

@@ -1539,7 +1548,7 @@ func pauseProcessMemory(
15391548
dedupDirectIO bool,
15401549
dedupBudget block.DedupBudget,
15411550
dedupInflightServe bool,
1542-
) (d build.Diff, h *DiffHeader, e error) {
1551+
) (d build.Diff, h *DiffHeader, provisionalHeader *header.Header, provisionalDiff build.Diff, e error) {
15431552
ctx, span := tracer.Start(ctx, "process-memory")
15441553
defer span.End()
15451554

@@ -1552,17 +1561,24 @@ func pauseProcessMemory(
15521561
dedupInflightServe,
15531562
)
15541563
if err != nil {
1555-
return nil, nil, fmt.Errorf("failed to export memory: %w", err)
1564+
return nil, nil, nil, nil, fmt.Errorf("failed to export memory: %w", err)
15561565
}
15571566

15581567
diff, err := build.NewLocalDiffFromCache(
15591568
build.GetDiffStoreKey(buildID.String(), build.Memfile),
15601569
cache,
15611570
)
15621571
if err != nil {
1563-
return nil, nil, fmt.Errorf("failed to create local diff from cache: %w", errors.Join(err, cache.Close()))
1572+
return nil, nil, nil, nil, fmt.Errorf("failed to create local diff from cache: %w", errors.Join(err, cache.Close()))
15641573
}
15651574

1575+
// Provisional local header: while the deduped header is still
1576+
// being computed, let a same-node resume serve dirty pages from the memfd via
1577+
// a distinct provisional build id at identity offsets. Gated on the memfd
1578+
// dedup path + the inflight-serve flag; best-effort (fall back to the deduped
1579+
// header on any error). The upload always uses the deduped header below.
1580+
provisionalHeader, provisionalDiff = buildProvisionalMemfile(ctx, cache, dedupInflightServe, originalMemfile, originalHeader, diffMetadata)
1581+
15661582
// Build the diff header on a goroutine so Pause returns without waiting
15671583
// on memfd-dedup compare. ExportMemory resolves metaOut sync for every
15681584
// other path, so Wait there is non-blocking; the goroutine is harmless.
@@ -1589,7 +1605,44 @@ func pauseProcessMemory(
15891605
setHeader(meta.ToDiffHeader(ctx, originalHeader, buildID))
15901606
}()
15911607

1592-
return diff, headerOut, nil
1608+
return diff, headerOut, provisionalHeader, provisionalDiff, nil
1609+
}
1610+
1611+
// buildProvisionalMemfile builds the provisional local header + its memfd-backed
1612+
// diff source. Returns (nil, nil) — falling back to the deduped
1613+
// header — when the path doesn't apply or on any error, so it never blocks a
1614+
// pause. The provisional source is keyed by a fresh build id so a header swap to
1615+
// the deduped header (after dedup) is race-free (see MemfdIdentitySource).
1616+
func buildProvisionalMemfile(
1617+
ctx context.Context,
1618+
cache block.DiffSource,
1619+
enabled bool,
1620+
originalMemfile block.ReadonlyDevice,
1621+
originalHeader *header.Header,
1622+
diffMetadata *header.DiffMetadata,
1623+
) (*header.Header, build.Diff) {
1624+
dc, ok := cache.(*block.DedupedMemfdCache)
1625+
if !ok || !enabled || originalMemfile == nil || originalHeader == nil || diffMetadata == nil {
1626+
return nil, nil
1627+
}
1628+
1629+
provisionalBuildID := uuid.New()
1630+
provisionalHeader, err := diffMetadata.ToProvisionalDiffHeader(ctx, originalHeader, provisionalBuildID)
1631+
if err != nil {
1632+
logger.L().Warn(ctx, "build provisional memfile header; using deduped header", zap.Error(err))
1633+
1634+
return nil, nil
1635+
}
1636+
1637+
provisionalSource := block.NewMemfdIdentitySource(dc, int64(originalHeader.Metadata.Size))
1638+
provisionalDiff, err := build.NewLocalDiffFromCache(build.GetDiffStoreKey(provisionalBuildID.String(), build.Memfile), provisionalSource)
1639+
if err != nil {
1640+
logger.L().Warn(ctx, "build provisional memfile diff; using deduped header", zap.Error(err))
1641+
1642+
return nil, nil
1643+
}
1644+
1645+
return provisionalHeader, provisionalDiff
15931646
}
15941647

15951648
func pauseProcessRootfs(

packages/orchestrator/pkg/sandbox/template/cache.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,23 +224,52 @@ func (c *Cache) AddSnapshot(
224224
localMetafile File,
225225
memfileDiff build.Diff,
226226
rootfsDiff build.Diff,
227+
// provisionalMemfileHeader/Diff: when non-nil the local memfile
228+
// template is built from the provisional header (which attributes dirty pages
229+
// to provisionalMemfileDiff's build id at identity offsets) so a concurrent
230+
// resume serves immediately from the memfd; once memfileHeader (the deduped
231+
// header) resolves it is swapped in. When nil, the template is built directly
232+
// from memfileHeader as before. The upload always uses memfileHeader.
233+
provisionalMemfileHeader *header.Header,
234+
provisionalMemfileDiff build.Diff,
227235
) error {
228236
switch memfileDiff.(type) {
229237
case *build.NoDiff:
230238
default:
231239
c.buildStore.Add(memfileDiff)
232240
}
241+
if provisionalMemfileDiff != nil {
242+
if _, ok := provisionalMemfileDiff.(*build.NoDiff); !ok {
243+
// Assumption: this provisional entry stays resident in the build store
244+
// until the SwapHeader below (i.e. for the provisional window — the
245+
// dedup compare). It is keyed by a synthetic build id with no GCS
246+
// object, so if it were evicted mid-window a resume read routed to it
247+
// would miss and could not be rebuilt (createDiff has nothing to fetch),
248+
// failing the read. This holds because the store TTL (hours) dwarfs the
249+
// window (seconds) and eviction is LRU-by-TTL, so this freshly-added
250+
// entry is evicted last. We rely on that rather than guarding the miss:
251+
// the eviction can't realistically occur, so a fallback isn't warranted.
252+
c.buildStore.Add(provisionalMemfileDiff)
253+
}
254+
}
233255

234256
switch rootfsDiff.(type) {
235257
case *build.NoDiff:
236258
default:
237259
c.buildStore.Add(rootfsDiff)
238260
}
239261

262+
// Build the local template from the provisional header (resolved now) so
263+
// Memfile() doesn't block on dedup; fall back to the deduped header future.
264+
localMemfileHeader := memfileHeader
265+
if provisionalMemfileHeader != nil {
266+
localMemfileHeader = resolvedHeader(provisionalMemfileHeader)
267+
}
268+
240269
storageTemplate, err := newTemplateFromStorage(
241270
c.config.BuilderConfig,
242271
buildId,
243-
memfileHeader,
272+
localMemfileHeader,
244273
rootfsHeader,
245274
c.persistence,
246275
c.blockMetrics,
@@ -253,6 +282,33 @@ func (c *Cache) AddSnapshot(
253282

254283
c.getTemplateWithFetch(ctx, storageTemplate, 0)
255284

285+
// Swap the provisional header for the deduped one once dedup finishes, so
286+
// subsequent reads route dirty pages to the (compacted) deduped diff and the
287+
// provisional memfd source is no longer referenced.
288+
if provisionalMemfileHeader != nil {
289+
swapCtx := context.WithoutCancel(ctx)
290+
go func() {
291+
deduped, err := memfileHeader.Wait()
292+
if err != nil {
293+
logger.L().Warn(swapCtx, "provisional memfile header swap: deduped header failed", zap.Error(err))
294+
295+
return
296+
}
297+
mem, err := storageTemplate.Memfile(swapCtx)
298+
if err != nil {
299+
logger.L().Warn(swapCtx, "provisional memfile header swap: get memfile", zap.Error(err))
300+
301+
return
302+
}
303+
if mem == nil {
304+
logger.L().Warn(swapCtx, "provisional memfile header swap: memfile is nil")
305+
306+
return
307+
}
308+
mem.SwapHeader(deduped)
309+
}()
310+
}
311+
256312
return nil
257313
}
258314

packages/orchestrator/pkg/server/sandboxes.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,8 @@ func (s *Server) snapshotAndCacheSandbox(
936936
snapshot.Metafile,
937937
snapshot.MemorySnapshot.Diff,
938938
snapshot.RootfsDiff,
939+
snapshot.MemorySnapshot.ProvisionalDiffHeader,
940+
snapshot.MemorySnapshot.ProvisionalDiff,
939941
)
940942
if err != nil {
941943
return nil, fmt.Errorf("error adding snapshot to template cache: %w", err)

packages/orchestrator/pkg/template/build/layer/layer_executor.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,8 @@ func (lb *LayerExecutor) PauseAndUpload(
283283
snapshot.Metafile,
284284
snapshot.MemorySnapshot.Diff,
285285
snapshot.RootfsDiff,
286+
snapshot.MemorySnapshot.ProvisionalDiffHeader,
287+
snapshot.MemorySnapshot.ProvisionalDiff,
286288
)
287289
if err != nil {
288290
err = errors.Join(err, snapshot.Close(context.WithoutCancel(ctx)))

packages/shared/pkg/featureflags/flags.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,11 @@ var (
162162

163163
// MemfdDedupInflightServeFlag lets a resume that overlaps an in-flight
164164
// memfile dedup serve dirty pages straight from the still-mapped memfd
165-
// instead of blocking on the dedup drain. Only affects the memfd-dedup
166-
// path; off restores the prior wait-for-drain behavior.
165+
// instead of blocking until dedup finishes. It gates both windows: serving
166+
// via a provisional local header while dedup is still computing the deduped
167+
// header, and serving during the dedup drain before the compacted diff is
168+
// ready. Only affects the memfd-dedup path; off restores the prior
169+
// wait-for-dedup behavior.
167170
MemfdDedupInflightServeFlag = NewBoolFlag("memfd-dedup-inflight-serve", false)
168171

169172
// PeerToPeerChunkTransferFlag enables peer-to-peer chunk routing.

0 commit comments

Comments
 (0)