Skip to content

Commit 4b5b520

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, 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. Signed-off-by: Nikita Kalyazin <nikita.kalyazin@e2b.dev> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 37aa06d commit 4b5b520

4 files changed

Lines changed: 171 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
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
}
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

0 commit comments

Comments
 (0)