Skip to content

Commit 0722353

Browse files
committed
bring back old behavior, add tests to verify
1 parent 856b620 commit 0722353

File tree

2 files changed

+208
-0
lines changed

2 files changed

+208
-0
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,11 @@ func (c *Cache) isCached(off, length int64) bool {
258258
// Cap if the length goes beyond the cache size, so we don't check for blocks that are out of bounds.
259259
end := min(off+length, c.size)
260260

261+
// Handle zero-length or empty range (vacuously true - no blocks to check)
262+
if end <= off {
263+
return true
264+
}
265+
261266
// Check all blocks in the range
262267
startBlock := uint(header.BlockIdx(off, c.blockSize))
263268
endBlock := uint(header.BlockIdx(end-1, c.blockSize))
@@ -275,6 +280,11 @@ func (c *Cache) isCached(off, length int64) bool {
275280
}
276281

277282
func (c *Cache) setIsCached(off, length int64) {
283+
// Handle zero-length - nothing to mark
284+
if length <= 0 {
285+
return
286+
}
287+
278288
startBlock := uint(header.BlockIdx(off, c.blockSize))
279289
endBlock := uint(header.BlockIdx(off+length-1, c.blockSize))
280290

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package block
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/e2b-dev/infra/packages/shared/pkg/storage/header"
10+
)
11+
12+
func TestIsCached_ZeroSizeCache(t *testing.T) {
13+
t.Parallel()
14+
15+
// Create a zero-size cache (triggers the size == 0 branch in NewCache)
16+
cache, err := NewCache(0, header.PageSize, t.TempDir()+"/cache", false)
17+
require.NoError(t, err)
18+
defer cache.Close()
19+
20+
// With size=0, any isCached call should return false (or handle gracefully)
21+
// The bug: end = min(off+length, 0) = 0, then end-1 = -1 causes underflow
22+
// Original behavior: returned true for empty range (no blocks to check)
23+
24+
// This should not panic and should return a sensible value
25+
result := cache.isCached(0, 100)
26+
// With zero-size cache, nothing can be cached
27+
assert.False(t, result, "zero-size cache should report nothing as cached")
28+
}
29+
30+
func TestIsCached_ZeroLength(t *testing.T) {
31+
t.Parallel()
32+
33+
const blockSize = int64(header.PageSize)
34+
const cacheSize = blockSize * 10
35+
36+
cache, err := NewCache(cacheSize, blockSize, t.TempDir()+"/cache", false)
37+
require.NoError(t, err)
38+
defer cache.Close()
39+
40+
// Zero-length query should return true (vacuously true - no blocks to check)
41+
// This matches the original sync.Map behavior where BlocksOffsets(0, blockSize)
42+
// returned an empty slice, so the loop didn't execute and returned true.
43+
result := cache.isCached(0, 0)
44+
assert.True(t, result, "zero-length isCached should return true (vacuously)")
45+
46+
result = cache.isCached(blockSize*5, 0)
47+
assert.True(t, result, "zero-length isCached at any offset should return true")
48+
}
49+
50+
func TestSetIsCached_ZeroLength(t *testing.T) {
51+
t.Parallel()
52+
53+
const blockSize = int64(header.PageSize)
54+
const cacheSize = blockSize * 10
55+
56+
cache, err := NewCache(cacheSize, blockSize, t.TempDir()+"/cache", false)
57+
require.NoError(t, err)
58+
defer cache.Close()
59+
60+
// Zero-length setIsCached should be a no-op
61+
// The bug: off=0, length=0 -> off+length-1 = -1 -> BlockIdx(-1, blockSize) = 0
62+
// This incorrectly marks block 0 as dirty
63+
cache.setIsCached(0, 0)
64+
65+
// Block 0 should NOT be marked as cached after a zero-length set
66+
// We need to check directly since isCached(0, 0) returns true vacuously
67+
result := cache.isCached(0, blockSize)
68+
assert.False(t, result, "zero-length setIsCached should not mark any blocks as dirty")
69+
}
70+
71+
func TestSetIsCached_ZeroLengthAtNonZeroOffset(t *testing.T) {
72+
t.Parallel()
73+
74+
const blockSize = int64(header.PageSize)
75+
const cacheSize = blockSize * 10
76+
77+
cache, err := NewCache(cacheSize, blockSize, t.TempDir()+"/cache", false)
78+
require.NoError(t, err)
79+
defer cache.Close()
80+
81+
// Zero-length at offset in block 5 should not mark anything
82+
cache.setIsCached(blockSize*5, 0)
83+
84+
// No blocks should be marked as cached
85+
for i := int64(0); i < 10; i++ {
86+
result := cache.isCached(i*blockSize, blockSize)
87+
assert.False(t, result, "block %d should not be cached after zero-length setIsCached", i)
88+
}
89+
}
90+
91+
func TestWriteAt_EmptyData(t *testing.T) {
92+
t.Parallel()
93+
94+
const blockSize = int64(header.PageSize)
95+
const cacheSize = blockSize * 10
96+
97+
cache, err := NewCache(cacheSize, blockSize, t.TempDir()+"/cache", false)
98+
require.NoError(t, err)
99+
defer cache.Close()
100+
101+
// Writing empty data should not mark any blocks as dirty
102+
n, err := cache.WriteAt([]byte{}, 0)
103+
require.NoError(t, err)
104+
assert.Equal(t, 0, n)
105+
106+
// Block 0 should NOT be marked as cached
107+
result := cache.isCached(0, blockSize)
108+
assert.False(t, result, "empty WriteAt should not mark block 0 as dirty")
109+
}
110+
111+
func TestWriteAt_PartiallyBeyondCacheSize(t *testing.T) {
112+
t.Parallel()
113+
114+
const blockSize = int64(header.PageSize)
115+
const cacheSize = blockSize * 10
116+
117+
cache, err := NewCache(cacheSize, blockSize, t.TempDir()+"/cache", false)
118+
require.NoError(t, err)
119+
defer cache.Close()
120+
121+
// Writing at the last block with data extending beyond cache size
122+
// should only write up to cache size and mark appropriate blocks
123+
data := make([]byte, blockSize*2) // 2 blocks of data
124+
n, err := cache.WriteAt(data, blockSize*9) // Start at block 9, would extend to block 11
125+
require.NoError(t, err)
126+
assert.Equal(t, int(blockSize), n, "should only write up to cache size")
127+
128+
// Only block 9 should be marked as cached (the portion that fit)
129+
for i := int64(0); i < 10; i++ {
130+
if i == 9 {
131+
assert.True(t, cache.isCached(i*blockSize, blockSize), "block 9 should be cached")
132+
} else {
133+
assert.False(t, cache.isCached(i*blockSize, blockSize), "block %d should not be cached", i)
134+
}
135+
}
136+
}
137+
138+
func TestIsCached_OffsetBeyondSize(t *testing.T) {
139+
t.Parallel()
140+
141+
const blockSize = int64(header.PageSize)
142+
const cacheSize = blockSize * 10
143+
144+
cache, err := NewCache(cacheSize, blockSize, t.TempDir()+"/cache", false)
145+
require.NoError(t, err)
146+
defer cache.Close()
147+
148+
// Query beyond cache size should return false
149+
result := cache.isCached(cacheSize+blockSize, blockSize)
150+
assert.False(t, result, "isCached beyond cache size should return false")
151+
}
152+
153+
func TestDirtyTracking_NormalOperation(t *testing.T) {
154+
t.Parallel()
155+
156+
const blockSize = int64(header.PageSize)
157+
const cacheSize = blockSize * 10
158+
159+
cache, err := NewCache(cacheSize, blockSize, t.TempDir()+"/cache", false)
160+
require.NoError(t, err)
161+
defer cache.Close()
162+
163+
// Initially nothing is cached
164+
for i := int64(0); i < 10; i++ {
165+
assert.False(t, cache.isCached(i*blockSize, blockSize), "block %d should not be initially cached", i)
166+
}
167+
168+
// Write to block 3
169+
data := make([]byte, blockSize)
170+
n, err := cache.WriteAt(data, blockSize*3)
171+
require.NoError(t, err)
172+
assert.Equal(t, int(blockSize), n)
173+
174+
// Only block 3 should be cached
175+
for i := int64(0); i < 10; i++ {
176+
if i == 3 {
177+
assert.True(t, cache.isCached(i*blockSize, blockSize), "block 3 should be cached")
178+
} else {
179+
assert.False(t, cache.isCached(i*blockSize, blockSize), "block %d should not be cached", i)
180+
}
181+
}
182+
183+
// Write spanning blocks 5-7
184+
multiBlockData := make([]byte, blockSize*3)
185+
n, err = cache.WriteAt(multiBlockData, blockSize*5)
186+
require.NoError(t, err)
187+
assert.Equal(t, int(blockSize*3), n)
188+
189+
// Blocks 3, 5, 6, 7 should be cached
190+
expected := map[int64]bool{3: true, 5: true, 6: true, 7: true}
191+
for i := int64(0); i < 10; i++ {
192+
if expected[i] {
193+
assert.True(t, cache.isCached(i*blockSize, blockSize), "block %d should be cached", i)
194+
} else {
195+
assert.False(t, cache.isCached(i*blockSize, blockSize), "block %d should not be cached", i)
196+
}
197+
}
198+
}

0 commit comments

Comments
 (0)