2018-10-01 06:08:12 +00:00
|
|
|
package diskcache
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"io"
|
|
|
|
|
"os"
|
2022-06-03 04:58:15 +00:00
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
2018-10-01 06:08:12 +00:00
|
|
|
"testing"
|
2021-12-14 14:55:26 +00:00
|
|
|
|
2024-08-08 13:18:34 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
|
2021-12-14 14:55:26 +00:00
|
|
|
"github.com/sourcegraph/sourcegraph/internal/observation"
|
2024-08-08 13:18:34 +00:00
|
|
|
"github.com/sourcegraph/sourcegraph/internal/tenant"
|
2018-10-01 06:08:12 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestOpen(t *testing.T) {
|
2022-04-27 16:08:29 +00:00
|
|
|
dir := t.TempDir()
|
2018-10-01 06:08:12 +00:00
|
|
|
|
2021-12-14 14:55:26 +00:00
|
|
|
store := &store{
|
|
|
|
|
dir: dir,
|
|
|
|
|
component: "test",
|
2024-04-10 12:07:39 +00:00
|
|
|
observe: newOperations(observation.TestContextTB(t), "test"),
|
2018-10-01 06:08:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
do := func() (*File, bool) {
|
|
|
|
|
want := "foobar"
|
|
|
|
|
calledFetcher := false
|
2021-11-19 03:12:44 +00:00
|
|
|
f, err := store.Open(context.Background(), []string{"key"}, func(ctx context.Context) (io.ReadCloser, error) {
|
2018-10-01 06:08:12 +00:00
|
|
|
calledFetcher = true
|
2021-06-07 12:33:02 +00:00
|
|
|
return io.NopCloser(bytes.NewReader([]byte(want))), nil
|
2018-10-01 06:08:12 +00:00
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2021-06-07 12:33:02 +00:00
|
|
|
got, err := io.ReadAll(f.File)
|
2018-10-01 06:08:12 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
f.Close()
|
|
|
|
|
if string(got) != want {
|
|
|
|
|
t.Fatalf("did not return fetcher output. got %q, want %q", string(got), want)
|
|
|
|
|
}
|
|
|
|
|
return f, !calledFetcher
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cache should be empty
|
|
|
|
|
_, usedCache := do()
|
|
|
|
|
if usedCache {
|
|
|
|
|
t.Fatal("Expected fetcher to be called on empty cache")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Redo, now we should use the cache
|
|
|
|
|
f, usedCache := do()
|
|
|
|
|
if !usedCache {
|
|
|
|
|
t.Fatal("Expected fetcher to not be called when cached")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Evict, then we should not use the cache
|
|
|
|
|
os.Remove(f.Path)
|
|
|
|
|
_, usedCache = do()
|
|
|
|
|
if usedCache {
|
|
|
|
|
t.Fatal("Item was not properly evicted")
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-02-04 00:13:38 +00:00
|
|
|
|
|
|
|
|
func TestMultiKeyEviction(t *testing.T) {
|
2022-04-27 16:08:29 +00:00
|
|
|
dir := t.TempDir()
|
2022-02-04 00:13:38 +00:00
|
|
|
|
|
|
|
|
store := &store{
|
|
|
|
|
dir: dir,
|
|
|
|
|
component: "test",
|
2024-04-10 12:07:39 +00:00
|
|
|
observe: newOperations(observation.TestContextTB(t), "test"),
|
2022-02-04 00:13:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f, err := store.Open(context.Background(), []string{"key1", "key2"}, func(ctx context.Context) (io.ReadCloser, error) {
|
|
|
|
|
return io.NopCloser(bytes.NewReader([]byte("blah"))), nil
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
f.Close()
|
|
|
|
|
|
|
|
|
|
stats, err := store.Evict(0)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if stats.Evicted != 1 {
|
|
|
|
|
t.Fatal("Expected to evict 1 item, evicted", stats.Evicted)
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-06-03 04:58:15 +00:00
|
|
|
|
|
|
|
|
func TestEvict(t *testing.T) {
|
2024-08-08 13:18:34 +00:00
|
|
|
// no tenant
|
|
|
|
|
doTestEvict(t, func() context.Context { return context.Background() })
|
|
|
|
|
// multi-tenant
|
2024-08-09 08:03:41 +00:00
|
|
|
tenant.MockEnforceTenant(t)
|
2024-08-08 13:18:34 +00:00
|
|
|
doTestEvict(t, func() context.Context { return tenant.NewTestContext() })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func doTestEvict(t *testing.T, ctxFunc func() context.Context) {
|
2022-06-03 04:58:15 +00:00
|
|
|
dir := t.TempDir()
|
|
|
|
|
|
|
|
|
|
store := &store{
|
|
|
|
|
dir: dir,
|
|
|
|
|
component: "test",
|
2024-04-10 12:07:39 +00:00
|
|
|
observe: newOperations(observation.TestContextTB(t), "test"),
|
2022-06-03 04:58:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, name := range []string{
|
|
|
|
|
"key-first",
|
|
|
|
|
"key-second",
|
|
|
|
|
"not-managed.txt",
|
|
|
|
|
"key-third",
|
|
|
|
|
"key-fourth",
|
|
|
|
|
} {
|
|
|
|
|
if strings.HasPrefix(name, "key-") {
|
2024-08-08 13:18:34 +00:00
|
|
|
f, err := store.Open(ctxFunc(), []string{name}, func(ctx context.Context) (io.ReadCloser, error) {
|
2022-06-03 04:58:15 +00:00
|
|
|
return io.NopCloser(bytes.NewReader([]byte("x"))), nil
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
f.Close()
|
|
|
|
|
} else {
|
2022-12-05 21:52:30 +00:00
|
|
|
if err := os.WriteFile(filepath.Join(dir, name), []byte("x"), 0o600); err != nil {
|
2022-06-03 04:58:15 +00:00
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
evict := func(maxCacheSizeBytes int64) EvictStats {
|
|
|
|
|
t.Helper()
|
|
|
|
|
stats, err := store.Evict(maxCacheSizeBytes)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
return stats
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expect := func(maxCacheSizeBytes int64, cacheSize int64, evicted int) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
before := evict(10000) // just get cache size before
|
|
|
|
|
stats := evict(maxCacheSizeBytes)
|
|
|
|
|
after := evict(10000)
|
|
|
|
|
|
|
|
|
|
if before.CacheSize != stats.CacheSize {
|
|
|
|
|
t.Fatalf("expected evict to return cache size before evictions: got=%d want=%d", stats.CacheSize, before.CacheSize)
|
|
|
|
|
}
|
|
|
|
|
if after.CacheSize != cacheSize {
|
|
|
|
|
t.Fatalf("unexpected cache size: got=%d want=%d", stats.CacheSize, cacheSize)
|
|
|
|
|
}
|
|
|
|
|
if stats.Evicted != evicted {
|
|
|
|
|
t.Fatalf("unexpected evicted: got=%d want=%d", stats.Evicted, evicted)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// we have 5 files with size 1 each.
|
|
|
|
|
expect(10000, 5, 0)
|
|
|
|
|
|
|
|
|
|
// our cachesize is 5, so making it 4 will evict one.
|
|
|
|
|
expect(4, 4, 1)
|
|
|
|
|
|
|
|
|
|
// we have 4 files left, but 1 can't be evicted since it isn't managed by
|
2024-08-08 13:18:34 +00:00
|
|
|
// diskcache.
|
2022-06-03 04:58:15 +00:00
|
|
|
expect(0, 1, 3)
|
|
|
|
|
}
|
2024-08-08 13:18:34 +00:00
|
|
|
|
|
|
|
|
func TestTenantRequired(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
tenant.MockEnforceTenant(t)
|
|
|
|
|
|
|
|
|
|
store := &store{
|
|
|
|
|
dir: dir,
|
|
|
|
|
component: "test",
|
|
|
|
|
observe: newOperations(observation.TestContextTB(t), "test"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err := store.Open(context.Background(), []string{"key"}, func(ctx context.Context) (io.ReadCloser, error) {
|
|
|
|
|
return nil, nil
|
|
|
|
|
})
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("Expected error when no tenant is provided")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTenantsHaveSeparateDirs(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
tenant.MockEnforceTenant(t)
|
|
|
|
|
|
|
|
|
|
ctx1 := tenant.NewTestContext()
|
|
|
|
|
ctx2 := tenant.NewTestContext()
|
|
|
|
|
|
|
|
|
|
key1 := "key1"
|
|
|
|
|
key2 := "key2"
|
|
|
|
|
|
|
|
|
|
store := &store{
|
|
|
|
|
dir: dir,
|
|
|
|
|
component: "test",
|
|
|
|
|
observe: newOperations(observation.TestContextTB(t), "test"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f1, err := store.Open(ctx1, []string{key1}, func(ctx context.Context) (io.ReadCloser, error) {
|
|
|
|
|
return io.NopCloser(bytes.NewReader([]byte("x"))), nil
|
|
|
|
|
})
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
f1.Close()
|
|
|
|
|
|
|
|
|
|
f2, err := store.Open(ctx2, []string{key2}, func(ctx context.Context) (io.ReadCloser, error) {
|
|
|
|
|
return io.NopCloser(bytes.NewReader([]byte("y"))), nil
|
|
|
|
|
})
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
f2.Close()
|
|
|
|
|
|
|
|
|
|
require.NotEqual(t, filepath.Dir(f1.Path), filepath.Dir(f2.Path))
|
|
|
|
|
|
|
|
|
|
// Ensure that the cache is not shared between tenants
|
|
|
|
|
for _, c := range []struct {
|
|
|
|
|
ctx context.Context
|
|
|
|
|
key string
|
|
|
|
|
cacheMiss bool
|
|
|
|
|
}{
|
|
|
|
|
{ctx1, key1, false},
|
|
|
|
|
{ctx1, key2, true},
|
|
|
|
|
{ctx2, key1, true},
|
|
|
|
|
{ctx2, key2, false},
|
|
|
|
|
} {
|
|
|
|
|
cacheMiss := false
|
|
|
|
|
_, _ = store.Open(c.ctx, []string{c.key}, func(ctx context.Context) (io.ReadCloser, error) {
|
|
|
|
|
cacheMiss = true
|
|
|
|
|
return io.NopCloser(bytes.NewReader([]byte("z"))), nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
require.Equal(t, c.cacheMiss, cacheMiss)
|
|
|
|
|
}
|
|
|
|
|
}
|