Skip to content

Commit 6294610

Browse files
kalyazinclaude
andcommitted
test(orch): cover MemfdIdentitySource.Slice and provisional fallback
Extend the MemfdIdentitySource test to exercise Slice (identity read while mapped, and BytesNotAvailableError after release), and add a test that buildProvisionalMemfile declines (returns nil, nil, nil) for a non-memfd-dedup source or when disabled, so the caller falls back to the deduped header. Also cover build.File.DurableHeader: it returns the live header when no durable future is set and otherwise waits for the deduped future, which is what keeps a pause from parenting off a provisional header; that a pinned build-store entry is skipped by disk-pressure eviction; and SwapHeaderIfCurrent, which swaps only when the current header still matches so a late provisional swap can't clobber a header the upload already finalized. Cover the peer-to-peer header source too: it serves the durable (deduped) header, so a peer never receives provisional synthetic-build-id mappings, and that storageTemplate.SchedulingMetadata reads the durable header non-blockingly (DurableHeaderNow) so memfile_build_ids exclude the provisional build id and never block the create/resume response — reporting rootfs-only metadata while the deduped header is still pending. Signed-off-by: Nikita Kalyazin <nikita.kalyazin@e2b.dev> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a3922e4 commit 6294610

6 files changed

Lines changed: 314 additions & 1 deletion

File tree

packages/orchestrator/pkg/sandbox/block/memfd_test.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,12 +464,18 @@ func TestMemfdIdentitySource_ServesThenReleases(t *testing.T) {
464464
require.Equal(t, int(ps), n)
465465
require.Equal(t, data[page*ps:(page+1)*ps], b)
466466
}
467+
// Slice takes the same identity path and returns a copy of the memfd bytes.
468+
sl, err := src.Slice(2*ps, ps)
469+
require.NoError(t, err)
470+
require.Equal(t, data[2*ps:3*ps], sl)
467471
require.True(t, src.IsCached(t.Context(), 0, ps*4))
468472

469473
require.NoError(t, d.releaseMemfd())
470474

471-
_, err := src.ReadAt(make([]byte, ps), 0)
472475
var bna BytesNotAvailableError
476+
_, err = src.ReadAt(make([]byte, ps), 0)
477+
require.ErrorAs(t, err, &bna)
478+
_, err = src.Slice(0, ps)
473479
require.ErrorAs(t, err, &bna)
474480
require.False(t, src.IsCached(t.Context(), 0, ps))
475481
}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,37 @@ func TestDiffStoreDelayEvictionAbort(t *testing.T) { //nolint:paralleltest // ve
271271
assert.True(t, found)
272272
}
273273

274+
// A pinned entry must be skipped by disk-pressure eviction (the next-oldest is
275+
// chosen instead), and become eligible again once unpinned.
276+
func TestDiffStorePinnedSkippedByEviction(t *testing.T) {
277+
t.Parallel()
278+
cachePath := t.TempDir()
279+
280+
c, err := cfg.Parse()
281+
require.NoError(t, err)
282+
flags := flagsWithMaxBuildCachePercentage(t, 100)
283+
store, err := NewDiffStore(c, flags, cachePath, 60*time.Second, 4*time.Second)
284+
require.NoError(t, err)
285+
286+
oldest := newRootFSDiff(t, cachePath, "pin-oldest")
287+
store.Add(oldest)
288+
newer := newRootFSDiff(t, cachePath, "pin-newer")
289+
store.Add(newer)
290+
291+
// Pin the oldest → eviction skips it and schedules the next-oldest.
292+
store.Pin(oldest.CacheKey())
293+
_, err = store.deleteOldestFromCache(t.Context())
294+
require.NoError(t, err)
295+
assert.False(t, store.isBeingDeleted(oldest.CacheKey()))
296+
assert.True(t, store.isBeingDeleted(newer.CacheKey()))
297+
298+
// Unpin → the oldest is eligible again.
299+
store.Unpin(oldest.CacheKey())
300+
_, err = store.deleteOldestFromCache(t.Context())
301+
require.NoError(t, err)
302+
assert.True(t, store.isBeingDeleted(oldest.CacheKey()))
303+
}
304+
274305
func TestDiffStoreOldestFromCache(t *testing.T) {
275306
t.Parallel()
276307
cachePath := t.TempDir()
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
//go:build linux
2+
3+
package build
4+
5+
import (
6+
"testing"
7+
"time"
8+
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/e2b-dev/infra/packages/shared/pkg/storage/header"
12+
"github.com/e2b-dev/infra/packages/shared/pkg/utils"
13+
)
14+
15+
// DurableHeader must return the live header when no durable future is set, and
16+
// otherwise wait for and return the durable (deduped) future's value — this is
17+
// what keeps a Pause from parenting a snapshot off a provisional header.
18+
func TestFile_DurableHeaderPrefersDurableFuture(t *testing.T) {
19+
t.Parallel()
20+
21+
live := &header.Header{}
22+
var f File
23+
f.header.Store(live)
24+
25+
// No durable future: returns the live header immediately.
26+
got, err := f.DurableHeader(t.Context())
27+
require.NoError(t, err)
28+
require.Same(t, live, got)
29+
30+
// With a durable future: blocks until it resolves, then returns it (not live).
31+
durable := utils.NewSetOnce[*header.Header]()
32+
f.SetDurableHeader(durable)
33+
34+
type res struct {
35+
h *header.Header
36+
err error
37+
}
38+
ch := make(chan res, 1)
39+
go func() {
40+
h, e := f.DurableHeader(t.Context())
41+
ch <- res{h, e}
42+
}()
43+
44+
select {
45+
case <-ch:
46+
t.Fatal("DurableHeader returned before the durable future resolved")
47+
case <-time.After(50 * time.Millisecond):
48+
}
49+
50+
deduped := &header.Header{}
51+
require.NoError(t, durable.SetValue(deduped))
52+
53+
r := <-ch
54+
require.NoError(t, r.err)
55+
require.Same(t, deduped, r.h)
56+
require.NotSame(t, live, r.h)
57+
}
58+
59+
// SwapHeaderIfCurrent must replace the header only when it still matches, so a
60+
// late provisional→deduped swap cannot clobber a header another writer (e.g. an
61+
// upload finalizing the build) has already installed.
62+
func TestFile_SwapHeaderIfCurrent(t *testing.T) {
63+
t.Parallel()
64+
65+
provisional := &header.Header{}
66+
var f File
67+
f.header.Store(provisional)
68+
69+
deduped := &header.Header{}
70+
finalized := &header.Header{}
71+
72+
// Current still matches → swaps.
73+
require.True(t, f.SwapHeaderIfCurrent(provisional, deduped))
74+
require.Same(t, deduped, f.Header())
75+
76+
// Current already advanced (upload finalized it) → no-op, newer header kept.
77+
f.header.Store(finalized)
78+
require.False(t, f.SwapHeaderIfCurrent(provisional, deduped))
79+
require.Same(t, finalized, f.Header())
80+
}
81+
82+
// DurableHeaderNow is the non-blocking form: (Header, true) when the durable
83+
// header is known (no swap pending, or the deduped future already resolved) and
84+
// (nil, false) while a swap is still pending — used on latency-sensitive paths.
85+
func TestFile_DurableHeaderNow(t *testing.T) {
86+
t.Parallel()
87+
88+
live := &header.Header{}
89+
var f File
90+
f.header.Store(live)
91+
92+
// No durable future: known immediately, returns the live header.
93+
h, ok := f.DurableHeaderNow()
94+
require.True(t, ok)
95+
require.Same(t, live, h)
96+
97+
// Durable future set but unresolved: not known yet, non-blocking (nil, false).
98+
durable := utils.NewSetOnce[*header.Header]()
99+
f.SetDurableHeader(durable)
100+
h, ok = f.DurableHeaderNow()
101+
require.False(t, ok)
102+
require.Nil(t, h)
103+
104+
// Once resolved: returns the durable (deduped) header.
105+
deduped := &header.Header{}
106+
require.NoError(t, durable.SetValue(deduped))
107+
h, ok = f.DurableHeaderNow()
108+
require.True(t, ok)
109+
require.Same(t, deduped, h)
110+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//go:build linux
2+
3+
package sandbox
4+
5+
import (
6+
"context"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// fakeDiffSource is a block.DiffSource that is not a *block.DedupedMemfdCache,
13+
// so buildProvisionalMemfile must decline to produce a provisional header.
14+
type fakeDiffSource struct{}
15+
16+
func (fakeDiffSource) Close() error { return nil }
17+
func (fakeDiffSource) ReadAt([]byte, int64) (int, error) { return 0, nil }
18+
func (fakeDiffSource) Slice(int64, int64) ([]byte, error) { return nil, nil }
19+
func (fakeDiffSource) Size() (int64, error) { return 0, nil }
20+
func (fakeDiffSource) FileSize(context.Context) (int64, error) { return 0, nil }
21+
func (fakeDiffSource) BlockSize() int64 { return 0 }
22+
func (fakeDiffSource) Path(context.Context) (string, error) { return "", nil }
23+
24+
// buildProvisionalMemfile must fall back (return nil, nil, nil) when it can't
25+
// apply, so the caller keeps using the deduped header: a non-memfd-dedup source,
26+
// and a disabled flag.
27+
func TestBuildProvisionalMemfile_FallsBack(t *testing.T) {
28+
t.Parallel()
29+
30+
// Source is not a *block.DedupedMemfdCache → no provisional serving.
31+
h, d, swapDone := buildProvisionalMemfile(t.Context(), fakeDiffSource{}, true, nil, nil, nil)
32+
require.Nil(t, h)
33+
require.Nil(t, d)
34+
require.Nil(t, swapDone)
35+
36+
// Disabled flag → no provisional serving.
37+
h, d, swapDone = buildProvisionalMemfile(t.Context(), fakeDiffSource{}, false, nil, nil, nil)
38+
require.Nil(t, h)
39+
require.Nil(t, d)
40+
require.Nil(t, swapDone)
41+
}

packages/orchestrator/pkg/sandbox/template/peerserver/header_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package peerserver
44

55
import (
6+
"context"
67
"testing"
78

89
"github.com/stretchr/testify/assert"
@@ -16,6 +17,20 @@ import (
1617
storageheader "github.com/e2b-dev/infra/packages/shared/pkg/storage/header"
1718
)
1819

20+
// durableDevice is a ReadonlyDevice that also exposes DurableHeader, like the
21+
// real *template.Storage. Header() stands in for the live (provisional) header,
22+
// DurableHeader for the deduped one a pause/peer must use instead.
23+
type durableDevice struct {
24+
*blockmocks.MockReadonlyDevice
25+
26+
durable *storageheader.Header
27+
durableErr error
28+
}
29+
30+
func (d durableDevice) DurableHeader(context.Context) (*storageheader.Header, error) {
31+
return d.durable, d.durableErr
32+
}
33+
1934
func TestHeaderSource_Stream(t *testing.T) {
2035
t.Parallel()
2136

@@ -59,6 +74,34 @@ func TestHeaderSource_Stream_NilHeader(t *testing.T) {
5974
assert.ErrorIs(t, err, ErrNotAvailable)
6075
}
6176

77+
// When the device exposes DurableHeader, Stream must serve that (the deduped
78+
// header) rather than the live one, so a peer never receives provisional
79+
// synthetic-build-id mappings it can't resolve. The live Header() returns nil
80+
// here, so the stream succeeds only if DurableHeader is used.
81+
func TestHeaderSource_Stream_ServesDurableHeader(t *testing.T) {
82+
t.Parallel()
83+
84+
deduped, err := storageheader.NewHeader(&storageheader.Metadata{Version: 3, BlockSize: 4096, Size: 4096}, nil)
85+
require.NoError(t, err)
86+
87+
mockDev := blockmocks.NewMockReadonlyDevice(t)
88+
mockDev.EXPECT().Header().Return(nil).Maybe()
89+
dev := durableDevice{MockReadonlyDevice: mockDev, durable: deduped}
90+
91+
tmplMock := templatemocks.NewMockTemplate(t)
92+
tmplMock.EXPECT().Memfile(mock.Anything).Return(dev, nil)
93+
94+
cache := peerservermocks.NewMockCache(t)
95+
cache.EXPECT().GetCachedTemplate("build-1").Return(tmplMock, true)
96+
97+
src, err := ResolveBlob(cache, "build-1", storage.MemfileName+storage.HeaderSuffix)
98+
require.NoError(t, err)
99+
100+
sender := &collectSender{}
101+
require.NoError(t, src.Stream(t.Context(), sender))
102+
assert.NotEmpty(t, sender.data)
103+
}
104+
62105
func TestHeaderSource_Stream_Rootfs(t *testing.T) {
63106
t.Parallel()
64107

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//go:build linux
2+
3+
package template
4+
5+
import (
6+
"testing"
7+
8+
"github.com/google/uuid"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/block"
13+
blockmocks "github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/block/mocks"
14+
"github.com/e2b-dev/infra/packages/shared/pkg/storage/header"
15+
"github.com/e2b-dev/infra/packages/shared/pkg/utils"
16+
)
17+
18+
// durableMemfileDevice is a ReadonlyDevice that also exposes DurableHeaderNow,
19+
// like the real *Storage. Header() stands in for the live (provisional) header;
20+
// DurableHeaderNow returns the deduped header scheduling metadata must use, and
21+
// its ready flag models whether the deduped header has resolved yet.
22+
type durableMemfileDevice struct {
23+
*blockmocks.MockReadonlyDevice
24+
25+
durable *header.Header
26+
ready bool
27+
}
28+
29+
func (d durableMemfileDevice) DurableHeaderNow() (*header.Header, bool) {
30+
return d.durable, d.ready
31+
}
32+
33+
func schedulingTemplate(t *testing.T, mem block.ReadonlyDevice, rootfsBase uuid.UUID) *storageTemplate {
34+
t.Helper()
35+
rootfsHdr, err := header.NewHeader(&header.Metadata{Version: 3, BlockSize: 4096, Size: 4096, BaseBuildId: rootfsBase}, nil)
36+
require.NoError(t, err)
37+
rootfsDev := blockmocks.NewMockReadonlyDevice(t)
38+
rootfsDev.EXPECT().Header().Return(rootfsHdr)
39+
40+
tmpl := &storageTemplate{
41+
memfile: utils.NewSetOnce[block.ReadonlyDevice](),
42+
rootfs: utils.NewSetOnce[block.ReadonlyDevice](),
43+
}
44+
require.NoError(t, tmpl.rootfs.SetValue(rootfsDev))
45+
require.NoError(t, tmpl.memfile.SetValue(mem))
46+
47+
return tmpl
48+
}
49+
50+
// When the deduped header has resolved, SchedulingMetadata reports it (never the
51+
// live/provisional header) — so memfile build ids reflect the real build id.
52+
func TestStorageTemplate_SchedulingMetadataUsesDurableHeader(t *testing.T) {
53+
t.Parallel()
54+
55+
dedupedBase := uuid.New()
56+
dedupedHdr, err := header.NewHeader(&header.Metadata{Version: 3, BlockSize: 4096, Size: 4096, BaseBuildId: dedupedBase}, nil)
57+
require.NoError(t, err)
58+
memMock := blockmocks.NewMockReadonlyDevice(t)
59+
memMock.EXPECT().Header().Return(nil).Maybe()
60+
memDev := durableMemfileDevice{MockReadonlyDevice: memMock, durable: dedupedHdr, ready: true}
61+
62+
md := schedulingTemplate(t, memDev, uuid.New()).SchedulingMetadata(t.Context())
63+
require.NotNil(t, md)
64+
assert.Equal(t, dedupedBase.String(), md.GetMemfileBaseBuildId())
65+
}
66+
67+
// While the deduped header is still pending (provisional window), SchedulingMetadata
68+
// must NOT block and must NOT emit the provisional build id: it reports rootfs-only
69+
// metadata (empty memfile base build id).
70+
func TestStorageTemplate_SchedulingMetadataSkipsPendingMemfile(t *testing.T) {
71+
t.Parallel()
72+
73+
rootfsBase := uuid.New()
74+
memMock := blockmocks.NewMockReadonlyDevice(t)
75+
memMock.EXPECT().Header().Return(nil).Maybe()
76+
memDev := durableMemfileDevice{MockReadonlyDevice: memMock, durable: nil, ready: false}
77+
78+
md := schedulingTemplate(t, memDev, rootfsBase).SchedulingMetadata(t.Context())
79+
require.NotNil(t, md)
80+
assert.Empty(t, md.GetMemfileBaseBuildId())
81+
assert.Equal(t, rootfsBase.String(), md.GetRootfsBaseBuildId())
82+
}

0 commit comments

Comments
 (0)