Skip to content

Commit babcac3

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. A pause must never parent a snapshot off the provisional header: it maps dirty pages to a synthetic build id with no storage object, so an uploaded header that inherited those mappings (from a resume paused again before the swap) would be unreadable on a cold or cross-node resume. build.File carries the deduped header as a durable-header future, and processMemorySnapshot resolves the parent via DurableHeader (waiting for the deduped header if a swap is still pending) rather than the live, possibly-provisional header. 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 2043ff4 commit babcac3

8 files changed

Lines changed: 224 additions & 14 deletions

File tree

packages/orchestrator/pkg/sandbox/build/build.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,12 @@ var (
5252
)
5353

5454
type File struct {
55-
header atomic.Pointer[header.Header]
55+
header atomic.Pointer[header.Header]
56+
// durable, when set, is the future for the header this file will eventually
57+
// settle on (the deduped header while a provisional header is being served).
58+
// DurableHeader waits on it so a pause never parents a snapshot off a
59+
// provisional (local-only) header — see SetDurableHeader.
60+
durable atomic.Pointer[utils.SetOnce[*header.Header]]
5661
store *DiffStore
5762
fileType DiffType
5863
persistence storage.StorageProvider
@@ -91,6 +96,26 @@ func (b *File) SwapHeader(h *header.Header) {
9196
b.header.Store(h)
9297
}
9398

99+
// SetDurableHeader records the future for the durable header this file will
100+
// settle on, so DurableHeader can return it instead of the currently-installed
101+
// (possibly provisional) header. Set once, before the file is shared.
102+
func (b *File) SetDurableHeader(f *utils.SetOnce[*header.Header]) {
103+
b.durable.Store(f)
104+
}
105+
106+
// DurableHeader returns the header safe to persist as a parent: the durable
107+
// future's value if one was set (waiting for it), otherwise the currently
108+
// installed header. Callers building an *uploaded* snapshot header (a Pause)
109+
// must use this, never Header(), so they never inherit a provisional header's
110+
// synthetic build-id mappings, which have no storage object.
111+
func (b *File) DurableHeader(ctx context.Context) (*header.Header, error) {
112+
if f := b.durable.Load(); f != nil {
113+
return f.WaitWithContext(ctx)
114+
}
115+
116+
return b.Header(), nil
117+
}
118+
94119
// ReadAt times file.read_at around readAt; Slice composes via readAt directly to
95120
// avoid double counting.
96121
func (b *File) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,13 @@ func (s *DiffStore) Has(d Diff) bool {
176176
return s.cache.Has(d.CacheKey())
177177
}
178178

179+
// Delete evicts a single entry by key, running the eviction callback (which
180+
// closes the Diff). Used to drop the short-lived provisional memfile entry once
181+
// the deduped header has been swapped in, so it doesn't linger for the TTL.
182+
func (s *DiffStore) Delete(key DiffStoreKey) {
183+
s.cache.Delete(key)
184+
}
185+
179186
// Lookup returns the cached Diff for the given key without initialising a new one.
180187
// Returns (nil, false) if the key is not present in the cache.
181188
func (s *DiffStore) Lookup(key DiffStoreKey) (Diff, bool) {

packages/orchestrator/pkg/sandbox/sandbox.go

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,6 +1435,17 @@ 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
1445+
// ProvisionalSwapDone, when non-nil, is invoked by the AddSnapshot swap
1446+
// goroutine once it has swapped the deduped header in; it lets the dedup
1447+
// goroutine release the memfd the provisional source was serving from.
1448+
ProvisionalSwapDone func()
14381449
// BlockSize is captured synchronously at Pause time because NewUpload's
14391450
// compression validation needs it before the async dedup header resolves;
14401451
// the dedup memfile path produces a page-granular Diff.BlockSize() that
@@ -1456,7 +1467,21 @@ func (s *Sandbox) processMemorySnapshot(ctx context.Context, buildID uuid.UUID)
14561467
if err != nil {
14571468
return MemorySnapshot{}, fmt.Errorf("failed to get original memfile: %w", err)
14581469
}
1470+
// Parent off the durable (deduped) header, never a provisional (local-only)
1471+
// one: a provisional header maps dirty pages to a synthetic build id with no
1472+
// storage object, so an uploaded header inheriting those mappings would be
1473+
// unreadable on a cold or cross-node resume. DurableHeader waits for the
1474+
// deduped header if a provisional swap is still pending; devices without one
1475+
// return their current header immediately.
14591476
memfileHeader := originalMemfile.Header()
1477+
if dh, ok := originalMemfile.(interface {
1478+
DurableHeader(ctx context.Context) (*header.Header, error)
1479+
}); ok {
1480+
memfileHeader, err = dh.DurableHeader(ctx)
1481+
if err != nil {
1482+
return MemorySnapshot{}, fmt.Errorf("failed to resolve durable memfile header: %w", err)
1483+
}
1484+
}
14601485

14611486
memfileDiffMetadata, err := s.Resources.memory.DiffMetadata(ctx, s.process)
14621487
if err != nil {
@@ -1481,7 +1506,7 @@ func (s *Sandbox) processMemorySnapshot(ctx context.Context, buildID uuid.UUID)
14811506
}
14821507
}
14831508

1484-
memfileDiff, memfileDiffHeader, err := pauseProcessMemory(
1509+
memfileDiff, memfileDiffHeader, provMemfileHeader, provMemfileDiff, provMemfileSwapDone, err := pauseProcessMemory(
14851510
ctx,
14861511
buildID,
14871512
memfileHeader,
@@ -1501,11 +1526,14 @@ func (s *Sandbox) processMemorySnapshot(ctx context.Context, buildID uuid.UUID)
15011526
}
15021527

15031528
return MemorySnapshot{
1504-
Diff: memfileDiff,
1505-
DiffHeader: memfileDiffHeader,
1506-
BlockSize: memfileHeader.Metadata.BlockSize,
1507-
header: memfileHeader,
1508-
newBytes: memfileDiffMetadata.Dirty.GetCardinality() * uint64(memfileDiffMetadata.BlockSize),
1529+
Diff: memfileDiff,
1530+
DiffHeader: memfileDiffHeader,
1531+
ProvisionalDiffHeader: provMemfileHeader,
1532+
ProvisionalDiff: provMemfileDiff,
1533+
ProvisionalSwapDone: provMemfileSwapDone,
1534+
BlockSize: memfileHeader.Metadata.BlockSize,
1535+
header: memfileHeader,
1536+
newBytes: memfileDiffMetadata.Dirty.GetCardinality() * uint64(memfileDiffMetadata.BlockSize),
15091537
}, nil
15101538
}
15111539

@@ -1539,7 +1567,7 @@ func pauseProcessMemory(
15391567
dedupDirectIO bool,
15401568
dedupBudget block.DedupBudget,
15411569
dedupInflightServe bool,
1542-
) (d build.Diff, h *DiffHeader, e error) {
1570+
) (d build.Diff, h *DiffHeader, provisionalHeader *header.Header, provisionalDiff build.Diff, provisionalSwapDone func(), e error) {
15431571
ctx, span := tracer.Start(ctx, "process-memory")
15441572
defer span.End()
15451573

@@ -1552,17 +1580,24 @@ func pauseProcessMemory(
15521580
dedupInflightServe,
15531581
)
15541582
if err != nil {
1555-
return nil, nil, fmt.Errorf("failed to export memory: %w", err)
1583+
return nil, nil, nil, nil, nil, fmt.Errorf("failed to export memory: %w", err)
15561584
}
15571585

15581586
diff, err := build.NewLocalDiffFromCache(
15591587
build.GetDiffStoreKey(buildID.String(), build.Memfile),
15601588
cache,
15611589
)
15621590
if err != nil {
1563-
return nil, nil, fmt.Errorf("failed to create local diff from cache: %w", errors.Join(err, cache.Close()))
1591+
return nil, nil, nil, nil, nil, fmt.Errorf("failed to create local diff from cache: %w", errors.Join(err, cache.Close()))
15641592
}
15651593

1594+
// Provisional local header: while the deduped header is still
1595+
// being computed, let a same-node resume serve dirty pages from the memfd via
1596+
// a distinct provisional build id at identity offsets. Gated on the memfd
1597+
// dedup path + the inflight-serve flag; best-effort (fall back to the deduped
1598+
// header on any error). The upload always uses the deduped header below.
1599+
provisionalHeader, provisionalDiff, provisionalSwapDone = buildProvisionalMemfile(ctx, cache, dedupInflightServe, originalMemfile, originalHeader, diffMetadata)
1600+
15661601
// Build the diff header on a goroutine so Pause returns without waiting
15671602
// on memfd-dedup compare. ExportMemory resolves metaOut sync for every
15681603
// other path, so Wait there is non-blocking; the goroutine is harmless.
@@ -1589,7 +1624,46 @@ func pauseProcessMemory(
15891624
setHeader(meta.ToDiffHeader(ctx, originalHeader, buildID))
15901625
}()
15911626

1592-
return diff, headerOut, nil
1627+
return diff, headerOut, provisionalHeader, provisionalDiff, provisionalSwapDone, nil
1628+
}
1629+
1630+
// buildProvisionalMemfile builds the provisional local header + its memfd-backed
1631+
// diff source, plus a swap-done callback the AddSnapshot swap goroutine invokes
1632+
// once it has swapped the deduped header in (it lets the dedup goroutine release
1633+
// the memfd). Returns (nil, nil, nil) — falling back to the deduped header —
1634+
// when the path doesn't apply or on any error, so it never blocks a pause. The
1635+
// provisional source is keyed by a fresh build id so a header swap to the
1636+
// deduped header (after dedup) is race-free (see MemfdIdentitySource).
1637+
func buildProvisionalMemfile(
1638+
ctx context.Context,
1639+
cache block.DiffSource,
1640+
enabled bool,
1641+
originalMemfile block.ReadonlyDevice,
1642+
originalHeader *header.Header,
1643+
diffMetadata *header.DiffMetadata,
1644+
) (*header.Header, build.Diff, func()) {
1645+
dc, ok := cache.(*block.DedupedMemfdCache)
1646+
if !ok || !enabled || originalMemfile == nil || originalHeader == nil || diffMetadata == nil {
1647+
return nil, nil, nil
1648+
}
1649+
1650+
provisionalBuildID := uuid.New()
1651+
provisionalHeader, err := diffMetadata.ToProvisionalDiffHeader(ctx, originalHeader, provisionalBuildID)
1652+
if err != nil {
1653+
logger.L().Warn(ctx, "build provisional memfile header; using deduped header", zap.Error(err))
1654+
1655+
return nil, nil, nil
1656+
}
1657+
1658+
provisionalSource := block.NewMemfdIdentitySource(dc, int64(originalHeader.Metadata.Size))
1659+
provisionalDiff, err := build.NewLocalDiffFromCache(build.GetDiffStoreKey(provisionalBuildID.String(), build.Memfile), provisionalSource)
1660+
if err != nil {
1661+
logger.L().Warn(ctx, "build provisional memfile diff; using deduped header", zap.Error(err))
1662+
1663+
return nil, nil, nil
1664+
}
1665+
1666+
return provisionalHeader, provisionalDiff, dc.MarkSwapped
15931667
}
15941668

15951669
func pauseProcessRootfs(

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

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,23 +224,56 @@ 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,
235+
// provisionalSwapDone, when non-nil, is called once the deduped header has
236+
// been swapped in; it lets the dedup goroutine release the memfd the
237+
// provisional source was serving from.
238+
provisionalSwapDone func(),
227239
) error {
228240
switch memfileDiff.(type) {
229241
case *build.NoDiff:
230242
default:
231243
c.buildStore.Add(memfileDiff)
232244
}
245+
if provisionalMemfileDiff != nil {
246+
if _, ok := provisionalMemfileDiff.(*build.NoDiff); !ok {
247+
// Assumption: this provisional entry stays resident in the build store
248+
// until the SwapHeader below (i.e. for the provisional window — the
249+
// dedup compare). It is keyed by a synthetic build id with no GCS
250+
// object, so if it were evicted mid-window a resume read routed to it
251+
// would miss and could not be rebuilt (createDiff has nothing to fetch),
252+
// failing the read. This holds because the store TTL (hours) dwarfs the
253+
// window (seconds) and eviction is LRU-by-TTL, so this freshly-added
254+
// entry is evicted last. We rely on that rather than guarding the miss:
255+
// the eviction can't realistically occur, so a fallback isn't warranted.
256+
c.buildStore.Add(provisionalMemfileDiff)
257+
}
258+
}
233259

234260
switch rootfsDiff.(type) {
235261
case *build.NoDiff:
236262
default:
237263
c.buildStore.Add(rootfsDiff)
238264
}
239265

266+
// Build the local template from the provisional header (resolved now) so
267+
// Memfile() doesn't block on dedup; fall back to the deduped header future.
268+
localMemfileHeader := memfileHeader
269+
if provisionalMemfileHeader != nil {
270+
localMemfileHeader = resolvedHeader(provisionalMemfileHeader)
271+
}
272+
240273
storageTemplate, err := newTemplateFromStorage(
241274
c.config.BuilderConfig,
242275
buildId,
243-
memfileHeader,
276+
localMemfileHeader,
244277
rootfsHeader,
245278
c.persistence,
246279
c.blockMetrics,
@@ -253,6 +286,59 @@ func (c *Cache) AddSnapshot(
253286

254287
c.getTemplateWithFetch(ctx, storageTemplate, 0)
255288

289+
// Swap the provisional header for the deduped one once dedup finishes, so
290+
// subsequent reads route dirty pages to the (compacted) deduped diff and the
291+
// provisional memfd source is no longer referenced.
292+
if provisionalMemfileHeader != nil {
293+
// Record the deduped header as the memfile's durable header (before this
294+
// returns, i.e. before any resume can re-pause) so a pause that parents
295+
// off this still-provisional template waits for the deduped header rather
296+
// than persisting the provisional header's synthetic build-id mappings,
297+
// which have no storage object and are unreadable on a cold/cross-node
298+
// resume.
299+
if mem, memErr := storageTemplate.Memfile(ctx); memErr != nil {
300+
logger.L().Warn(ctx, "provisional memfile: set durable header: get memfile", zap.Error(memErr))
301+
} else if dh, ok := mem.(interface {
302+
SetDurableHeader(f *utils.SetOnce[*header.Header])
303+
}); ok {
304+
dh.SetDurableHeader(memfileHeader)
305+
}
306+
307+
swapCtx := context.WithoutCancel(ctx)
308+
go func() {
309+
deduped, err := memfileHeader.Wait()
310+
if err != nil {
311+
logger.L().Warn(swapCtx, "provisional memfile header swap: deduped header failed", zap.Error(err))
312+
313+
return
314+
}
315+
mem, err := storageTemplate.Memfile(swapCtx)
316+
if err != nil {
317+
logger.L().Warn(swapCtx, "provisional memfile header swap: get memfile", zap.Error(err))
318+
319+
return
320+
}
321+
if mem == nil {
322+
logger.L().Warn(swapCtx, "provisional memfile header swap: memfile is nil")
323+
324+
return
325+
}
326+
mem.SwapHeader(deduped)
327+
328+
// Reads now route to the deduped build id. Let the dedup goroutine
329+
// release the memfd (it keeps it mapped until this fires or a grace
330+
// elapses), and drop the provisional entry so it doesn't linger in the
331+
// store for the TTL. On an earlier error return the provisional entry
332+
// stays as the only memfile source until the grace releases the memfd.
333+
if provisionalSwapDone != nil {
334+
provisionalSwapDone()
335+
}
336+
if provisionalMemfileDiff != nil {
337+
c.buildStore.Delete(provisionalMemfileDiff.CacheKey())
338+
}
339+
}()
340+
}
341+
256342
return nil
257343
}
258344

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/e2b-dev/infra/packages/shared/pkg/logger"
1616
"github.com/e2b-dev/infra/packages/shared/pkg/storage"
1717
"github.com/e2b-dev/infra/packages/shared/pkg/storage/header"
18+
"github.com/e2b-dev/infra/packages/shared/pkg/utils"
1819
)
1920

2021
const (
@@ -156,6 +157,14 @@ func (d *Storage) SwapHeader(h *header.Header) {
156157
d.source.SwapHeader(h)
157158
}
158159

160+
func (d *Storage) SetDurableHeader(f *utils.SetOnce[*header.Header]) {
161+
d.source.SetDurableHeader(f)
162+
}
163+
164+
func (d *Storage) DurableHeader(ctx context.Context) (*header.Header, error) {
165+
return d.source.DurableHeader(ctx)
166+
}
167+
159168
func (d *Storage) Close() error {
160169
return nil
161170
}

packages/orchestrator/pkg/server/sandboxes.go

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

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,9 @@ func (lb *LayerExecutor) PauseAndUpload(
283283
snapshot.Metafile,
284284
snapshot.MemorySnapshot.Diff,
285285
snapshot.RootfsDiff,
286+
snapshot.MemorySnapshot.ProvisionalDiffHeader,
287+
snapshot.MemorySnapshot.ProvisionalDiff,
288+
snapshot.MemorySnapshot.ProvisionalSwapDone,
286289
)
287290
if err != nil {
288291
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)