Repository: buchgr/bazel-remote
Pull request #442 (2021-05-19 21:48:46)

[grpc] optimise FindMissingBlobs
FindMissingBlobs calls can contain lots of blob hashes to lookup. Instead of locking and unlocking the index once per hash, we can perform the lookups in batches. And instead of querying proxy backends sequentially, we can use a pool of goroutines to make lots of concurrent queries.

For the first part to work, we need to do this in the disk package instead of the server package (where the rest of the gRPC code is).

Relates to #408.

mostynb: This is still a work-in-progress, barely tested. It doesn't offer full logging yet.
mostynb: @glukasiknuro: do you have a good stress test for this that you could share?

Patch:
diff --git a/cache/disk/BUILD.bazel b/cache/disk/BUILD.bazel
index 32e6465..0b0285d 100644
--- a/cache/disk/BUILD.bazel
+++ b/cache/disk/BUILD.bazel
@@ -4,6 +4,7 @@ go_library(
     name = "go_default_library",
     srcs = [
         "disk.go",
+        "findmissing.go",
         "lru.go",
     ],
     importpath = "github.com/buchgr/bazel-remote/cache/disk",
@@ -17,6 +18,8 @@ go_library(
         "@com_github_golang_protobuf//proto:go_default_library",
         "@com_github_prometheus_client_golang//prometheus:go_default_library",
         "@com_github_prometheus_client_golang//prometheus/promauto:go_default_library",
+        "@org_golang_google_grpc//codes:go_default_library",
+        "@org_golang_google_grpc//status:go_default_library",
     ],
 )
 
@@ -24,6 +27,7 @@ go_test(
     name = "go_default_test",
     srcs = [
         "disk_test.go",
+        "findmissing_test.go",
         "lru_test.go",
     ],
     embed = [":go_default_library"],
diff --git a/cache/disk/disk.go b/cache/disk/disk.go
index ea3d045..7f8d41b 100644
--- a/cache/disk/disk.go
+++ b/cache/disk/disk.go
@@ -66,10 +66,12 @@ type lruItem struct {
 // Cache is a filesystem-based LRU cache, with an optional backend proxy.
 // It is safe for concurrent use.
 type Cache struct {
-	dir         string
-	proxy       cache.Proxy
-	storageMode casblob.CompressionType
-	maxBlobSize int64
+	dir           string
+	proxy         cache.Proxy
+	storageMode   casblob.CompressionType
+	maxBlobSize   int64
+	accessLogger  *log.Logger
+	containsQueue chan proxyCheck
 
 	mu  sync.Mutex
 	lru SizedLRU
@@ -100,7 +102,7 @@ func badReqErr(format string, a ...interface{}) *cache.Error {
 // New returns a new instance of a filesystem-based cache rooted at `dir`,
 // with a maximum size of `maxSizeBytes` bytes, maximum logical blob size
 // `maxBlobSize` and an optional backend `proxy`.
-func New(dir string, maxSizeBytes int64, maxBlobSize int64, storageMode string, proxy cache.Proxy) (*Cache, error) {
+func New(dir string, maxSizeBytes int64, maxBlobSize int64, storageMode string, proxy cache.Proxy, accessLogger *log.Logger) (*Cache, error) {
 	// Create the directory structure.
 	hexLetters := []byte("0123456789abcdef")
 	for _, c1 := range hexLetters {
@@ -132,10 +134,15 @@ func New(dir string, maxSizeBytes int64, maxBlobSize int64, storageMode string,
 	}
 
 	c := &Cache{
-		dir:         resolved,
-		storageMode: compressionType,
-		proxy:       proxy,
-		maxBlobSize: maxBlobSize,
+		dir:          resolved,
+		storageMode:  compressionType,
+		proxy:        proxy,
+		maxBlobSize:  maxBlobSize,
+		accessLogger: accessLogger,
+	}
+
+	if proxy != nil {
+		c.spawnContainsQueueWorkers()
 	}
 
 	// The eviction callback deletes the file from disk.
diff --git a/cache/disk/findmissing.go b/cache/disk/findmissing.go
new file mode 100644
index 0000000..8645710
--- /dev/null
+++ b/cache/disk/findmissing.go
@@ -0,0 +1,141 @@
+package disk
+
+import (
+	"context"
+	"sync"
+
+	"github.com/buchgr/bazel-remote/cache"
+
+	pb "github.com/buchgr/bazel-remote/genproto/build/bazel/remote/execution/v2"
+
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+)
+
+type proxyCheck struct {
+	wg     *sync.WaitGroup
+	digest **pb.Digest
+}
+
+// Optimised implementation of FindMissingBlobs, which batches local index
+// lookups and performs concurrent proxy lookups for local cache misses.
+// Returns a slice with the blobs that are missing from the cache.
+//
+// Note that this modifies the input slice and returns a subset of it.
+func (c *Cache) FindMissingCasBlobs(ctx context.Context, blobs []*pb.Digest) ([]*pb.Digest, error) {
+	const batchSize = 20
+
+	var wg sync.WaitGroup
+
+	var chunk []*pb.Digest
+	remaining := blobs
+
+	for len(remaining) > 0 {
+		select {
+		case <-ctx.Done():
+			return nil, status.Error(codes.Canceled, "Request was cancelled")
+		default:
+		}
+
+		if len(remaining) <= batchSize {
+			chunk = remaining
+			remaining = nil
+		} else {
+			chunk = remaining[:batchSize]
+			remaining = remaining[batchSize:]
+		}
+
+		numMissing := c.findMissingLocalCAS(chunk)
+		if numMissing > 0 && c.proxy != nil {
+			wg.Add(numMissing)
+			for i := range chunk {
+				if chunk[i] != nil {
+					c.containsQueue <- proxyCheck{
+						wg:     &wg,
+						digest: &chunk[i],
+					}
+				}
+			}
+		}
+	}
+
+	if c.proxy != nil {
+		wg.Wait()
+	}
+
+	missingBlobs := filterNonNil(blobs)
+
+	return missingBlobs, nil
+}
+
+// Move all the non-nil items in the input slice to the
+// start, and return the non-nil sub-slice.
+func filterNonNil(blobs []*pb.Digest) []*pb.Digest {
+	count := 0
+	for i := 0; i < len(blobs); i++ {
+		if blobs[i] != nil {
+			blobs[count] = blobs[i]
+			count++
+		}
+	}
+
+	return blobs[:count]
+}
+
+// Set blobs that exist in the disk cache to nil, and return the number
+// of missing blobs.
+func (c *Cache) findMissingLocalCAS(blobs []*pb.Digest) int {
+	var exists bool
+	var key string
+	missing := 0
+
+	c.mu.Lock()
+
+	for i := range blobs {
+		if blobs[i].SizeBytes == 0 {
+			c.accessLogger.Printf("GRPC CAS HEAD %s OK", blobs[i].Hash)
+			blobs[i] = nil
+			continue
+		}
+
+		key = cache.LookupKey(cache.CAS, blobs[i].Hash)
+		_, exists = c.lru.Get(key)
+		if exists {
+			c.accessLogger.Printf("GRPC CAS HEAD %s OK", blobs[i].Hash)
+			blobs[i] = nil
+		} else {
+			missing++
+		}
+	}
+
+	c.mu.Unlock()
+
+	return missing
+}
+
+func (c *Cache) containsWorker() {
+	var ok bool
+	for req := range c.containsQueue {
+		ok, _ = c.proxy.Contains(cache.CAS, (*req.digest).Hash)
+		if ok {
+			c.accessLogger.Printf("GRPC CAS HEAD %s OK", (*req.digest).Hash)
+			// The blob exists on the proxy, remove it from the
+			// list of missing blobs.
+			*(req.digest) = nil
+		} else {
+			c.accessLogger.Printf("GRPC CAS HEAD %s NOT FOUND", (*req.digest).Hash)
+		}
+		req.wg.Done()
+	}
+}
+
+func (c *Cache) spawnContainsQueueWorkers() {
+	// TODO: make these configurable?
+	const queueSize = 2048
+	const numWorkers = 512
+
+	c.containsQueue = make(chan proxyCheck, queueSize)
+	for i := 0; i < numWorkers; i++ {
+		go c.containsWorker()
+	}
+}
diff --git a/main.go b/main.go
index 0a5e202..17a8bde 100644
--- a/main.go
+++ b/main.go
@@ -81,7 +81,7 @@ func run(ctx *cli.Context) error {
 
 	rlimit.Raise()
 
-	diskCache, err := disk.New(c.Dir, int64(c.MaxSize)*1024*1024*1024, c.MaxBlobSize, c.StorageMode, c.ProxyBackend)
+	diskCache, err := disk.New(c.Dir, int64(c.MaxSize)*1024*1024*1024, c.MaxBlobSize, c.StorageMode, c.ProxyBackend, c.AccessLogger)
 	if err != nil {
 		log.Fatal(err)
 	}
diff --git a/server/grpc_cas.go b/server/grpc_cas.go
index c4d7965..ec13974 100644
--- a/server/grpc_cas.go
+++ b/server/grpc_cas.go
@@ -28,8 +28,6 @@ var (
 func (s *grpcServer) FindMissingBlobs(ctx context.Context,
 	req *pb.FindMissingBlobsRequest) (*pb.FindMissingBlobsResponse, error) {
 
-	resp := pb.FindMissingBlobsResponse{}
-
 	errorPrefix := "GRPC CAS HEAD"
 	for _, digest := range req.BlobDigests {
 		hash := digest.GetHash()
@@ -37,17 +35,14 @@ func (s *grpcServer) FindMissingBlobs(ctx context.Context,
 		if err != nil {
 			return nil, err
 		}
+	}
 
-		found, _ := s.cache.Contains(cache.CAS, hash, digest.GetSizeBytes())
-		if !found {
-			s.accessLogger.Printf("GRPC CAS HEAD %s NOT FOUND", hash)
-			resp.MissingBlobDigests = append(resp.MissingBlobDigests, digest)
-		} else {
-			s.accessLogger.Printf("GRPC CAS HEAD %s OK", hash)
-		}
+	missingBlobs, err := s.cache.FindMissingCasBlobs(ctx, req.BlobDigests)
+	if err != nil {
+		return nil, err
 	}
 
-	return &resp, nil
+	return &pb.FindMissingBlobsResponse{MissingBlobDigests: missingBlobs}, nil
 }
 
 func (s *grpcServer) BatchUpdateBlobs(ctx context.Context,


Test patch:
diff --git a/cache/disk/disk_test.go b/cache/disk/disk_test.go
index 177c9a6..821712e 100644
--- a/cache/disk/disk_test.go
+++ b/cache/disk/disk_test.go
@@ -49,7 +49,7 @@ func TestCacheBasics(t *testing.T) {
 	// Add some overhead for likely CAS blob storage expansion.
 	cacheSize := int64(itemSize*2 + BlockSize)
 
-	testCache, err := New(cacheDir, cacheSize, math.MaxInt64, "zstd", nil)
+	testCache, err := New(cacheDir, cacheSize, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -92,7 +92,7 @@ func TestCacheBasics(t *testing.T) {
 func TestCachePutWrongSize(t *testing.T) {
 	cacheDir := tempDir(t)
 	defer os.RemoveAll(cacheDir)
-	testCache, err := New(cacheDir, BlockSize, math.MaxInt64, "zstd", nil)
+	testCache, err := New(cacheDir, BlockSize, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -126,7 +126,7 @@ func TestCacheGetContainsWrongSize(t *testing.T) {
 
 	cacheDir := tempDir(t)
 	defer os.RemoveAll(cacheDir)
-	testCache, err := New(cacheDir, BlockSize, math.MaxInt64, "zstd", nil)
+	testCache, err := New(cacheDir, BlockSize, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -164,7 +164,7 @@ func TestCacheGetContainsWrongSizeWithProxy(t *testing.T) {
 
 	cacheDir := tempDir(t)
 	defer os.RemoveAll(cacheDir)
-	testCache, err := New(cacheDir, BlockSize, math.MaxInt64, "zstd", new(proxyStub))
+	testCache, err := New(cacheDir, BlockSize, math.MaxInt64, "zstd", new(proxyStub), testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -304,7 +304,7 @@ func TestOverwrite(t *testing.T) {
 	cacheDir := tempDir(t)
 	defer os.RemoveAll(cacheDir)
 
-	testCache, err := New(cacheDir, BlockSize, math.MaxInt64, "zstd", nil)
+	testCache, err := New(cacheDir, BlockSize, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -401,7 +401,7 @@ func TestCacheExistingFiles(t *testing.T) {
 	// Add some overhead for likely CAS blob storage expansion.
 	const cacheSize = BlockSize * 5
 
-	testCache, err := New(cacheDir, cacheSize, math.MaxInt64, "zstd", nil)
+	testCache, err := New(cacheDir, cacheSize, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -456,7 +456,7 @@ func TestCacheExistingFiles(t *testing.T) {
 func TestCacheBlobTooLarge(t *testing.T) {
 	cacheDir := tempDir(t)
 	defer os.RemoveAll(cacheDir)
-	testCache, err := New(cacheDir, BlockSize, math.MaxInt64, "zstd", nil)
+	testCache, err := New(cacheDir, BlockSize, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -482,7 +482,7 @@ func TestCacheBlobTooLarge(t *testing.T) {
 func TestCacheCorruptedCASBlob(t *testing.T) {
 	cacheDir := tempDir(t)
 	defer os.RemoveAll(cacheDir)
-	testCache, err := New(cacheDir, BlockSize, math.MaxInt64, "zstd", nil)
+	testCache, err := New(cacheDir, BlockSize, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -566,7 +566,7 @@ func TestMigrateFromOldDirectoryStructure(t *testing.T) {
 	// Add some overhead for likely CAS blob storage expansion.
 	const cacheSize = 2560*2 + BlockSize*2
 
-	testCache, err := New(cacheDir, cacheSize, math.MaxInt64, "zstd", nil)
+	testCache, err := New(cacheDir, cacheSize, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -661,7 +661,7 @@ func TestLoadExistingEntries(t *testing.T) {
 	// Add some overhead for likely CAS blob storage expansion.
 	cacheSize := int64((blobSize + BlockSize) * numBlobs * 2)
 
-	testCache, err := New(cacheDir, cacheSize, math.MaxInt64, "zstd", nil)
+	testCache, err := New(cacheDir, cacheSize, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -704,7 +704,7 @@ func TestDistinctKeyspaces(t *testing.T) {
 	// Add some overhead for likely CAS blob storage expansion.
 	cacheSize := int64((blobSize+BlockSize)*3) * 2
 
-	testCache, err := New(cacheDir, cacheSize, math.MaxInt64, "zstd", nil)
+	testCache, err := New(cacheDir, cacheSize, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -824,7 +824,7 @@ func TestHttpProxyBackend(t *testing.T) {
 	// Add some overhead for likely CAS blob storage expansion.
 	cacheSize := int64(1024*10) * 2
 
-	testCache, err := New(cacheDir, cacheSize, math.MaxInt64, "zstd", proxy)
+	testCache, err := New(cacheDir, cacheSize, math.MaxInt64, "zstd", proxy, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -862,7 +862,7 @@ func TestHttpProxyBackend(t *testing.T) {
 	// Create a new (empty) testCache, without a proxy backend.
 	cacheDir = testutils.TempDir(t)
 	defer os.RemoveAll(cacheDir)
-	testCache, err = New(cacheDir, cacheSize, math.MaxInt64, "zstd", nil)
+	testCache, err = New(cacheDir, cacheSize, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -924,7 +924,7 @@ func TestGetValidatedActionResult(t *testing.T) {
 	cacheDir := testutils.TempDir(t)
 	defer os.RemoveAll(cacheDir)
 
-	testCache, err := New(cacheDir, 1024*32, math.MaxInt64, "zstd", nil)
+	testCache, err := New(cacheDir, 1024*32, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -1096,7 +1096,7 @@ func TestGetWithOffset(t *testing.T) {
 
 	const blobSize = 2048 + 256
 
-	testCache, err := New(cacheDir, blobSize*2, math.MaxInt64, "zstd", nil)
+	testCache, err := New(cacheDir, blobSize*2, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/cache/disk/findmissing_test.go b/cache/disk/findmissing_test.go
new file mode 100644
index 0000000..f92aa57
--- /dev/null
+++ b/cache/disk/findmissing_test.go
@@ -0,0 +1,148 @@
+package disk
+
+import (
+	"io"
+	"sync"
+	"testing"
+
+	"github.com/buchgr/bazel-remote/cache"
+	testutils "github.com/buchgr/bazel-remote/utils"
+
+	pb "github.com/buchgr/bazel-remote/genproto/build/bazel/remote/execution/v2"
+)
+
+func TestFilterNonNIl(t *testing.T) {
+	t.Parallel()
+
+	blob1 := pb.Digest{
+		Hash:      "7f715e87ab77cfa3084ce8f7bb8f51e4059d02147b2139635673b7751004a170",
+		SizeBytes: 152,
+	}
+	blob2 := pb.Digest{
+		Hash:      "3db63cc7c4972b451c075f1ee198f4c02d8e5ec065f04b5d7b6cb2ba3aeb8ca6",
+		SizeBytes: 136,
+	}
+	blob3 := pb.Digest{
+		Hash:      "9205adc12a2c8b65e7cd77918ff8e6e20f39bdd0b7fc4b984abfd690c79d80c1",
+		SizeBytes: 217,
+	}
+
+	tcs := []struct {
+		input    []*pb.Digest
+		expected map[*pb.Digest]struct{}
+	}{
+		{
+			[]*pb.Digest{},
+			map[*pb.Digest]struct{}{},
+		},
+		{
+			[]*pb.Digest{nil},
+			map[*pb.Digest]struct{}{},
+		},
+		{
+			[]*pb.Digest{nil, nil, nil},
+			map[*pb.Digest]struct{}{},
+		},
+		{
+			[]*pb.Digest{nil, &blob1, &blob2, nil, &blob3},
+			map[*pb.Digest]struct{}{
+				&blob1: {},
+				&blob2: {},
+				&blob3: {},
+			},
+		},
+	}
+
+	for _, tc := range tcs {
+		output := filterNonNil(tc.input)
+
+		if len(output) != len(tc.expected) {
+			t.Errorf("Expected %d items, found %d",
+				len(tc.expected), len(output))
+		}
+
+		for _, ptr := range output {
+			if ptr == nil {
+				t.Errorf("Found nil pointer in output")
+			}
+		}
+
+		for _, ptr := range tc.input {
+			if ptr == nil {
+				continue
+			}
+
+			_, exists := tc.expected[ptr]
+			if !exists {
+				t.Errorf("Expected to find %q in output", *ptr)
+			}
+		}
+	}
+}
+
+type testCWProxy struct {
+	blob string
+}
+
+func (p *testCWProxy) Put(kind cache.EntryKind, hash string, size int64, rc io.ReadCloser) {}
+func (p *testCWProxy) Get(kind cache.EntryKind, hash string) (io.ReadCloser, int64, error) {
+	return nil, -1, nil
+}
+func (p *testCWProxy) Contains(kind cache.EntryKind, hash string) (bool, int64) {
+	if kind == cache.CAS && hash == p.blob {
+		return true, 42
+	}
+	return false, -1
+}
+
+func TestContainsWorker(t *testing.T) {
+	t.Parallel()
+
+	tp := testCWProxy{blob: "9205adc12a2c8b65e7cd77918ff8e6e20f39bdd0b7fc4b984abfd690c79d80c1"}
+
+	c := Cache{
+		accessLogger:  testutils.NewSilentLogger(),
+		proxy:         &tp,
+		containsQueue: make(chan proxyCheck, 2),
+	}
+
+	// Spawn a single worker.
+	go c.containsWorker()
+
+	digests := []*pb.Digest{
+		// Expect this to be found in the proxy, and replaced with nil.
+		{Hash: tp.blob, SizeBytes: 42},
+
+		// Expect this not to be found in the proxy, and left unchanged.
+		{Hash: "423789fae66b9539c5622134c580700a154a15e355af4e3311a4e12ee0c9d243", SizeBytes: 43},
+	}
+
+	if cap(c.containsQueue) != len(digests) {
+		t.Fatalf("Broken test setup, expected containsQueue capacity %d to match number of digests %d",
+			cap(c.containsQueue), len(digests))
+	}
+
+	var wg sync.WaitGroup
+
+	for i := range digests {
+		wg.Add(1)
+		c.containsQueue <- proxyCheck{
+			wg:     &wg,
+			digest: &digests[i],
+		}
+	}
+
+	// Wait for the worker to process each request.
+	wg.Wait()
+
+	// Allow the worker goroutine to finish.
+	close(c.containsQueue)
+
+	if digests[0] != nil {
+		t.Error("Expected digests[0] to be found in the proxy and replaced by nil")
+	}
+
+	if digests[1] == nil {
+		t.Error("Expected digests[1] to not be found in the proxy and left as-is")
+	}
+}
diff --git a/cache/httpproxy/httpproxy_test.go b/cache/httpproxy/httpproxy_test.go
index 52e47b6..98c045b 100644
--- a/cache/httpproxy/httpproxy_test.go
+++ b/cache/httpproxy/httpproxy_test.go
@@ -115,7 +115,7 @@ func TestEverything(t *testing.T) {
 	}
 
 	diskCacheSize := int64(len(casData) + disk.BlockSize)
-	diskCache, err := disk.New(cacheDir, diskCacheSize, math.MaxInt64, "zstd", proxyCache)
+	diskCache, err := disk.New(cacheDir, diskCacheSize, math.MaxInt64, "zstd", proxyCache, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -273,7 +273,7 @@ func TestEverything(t *testing.T) {
 	cacheDir2 := testutils.TempDir(t)
 	defer os.RemoveAll(cacheDir2)
 
-	diskCache, err = disk.New(cacheDir2, diskCacheSize, math.MaxInt64, "zstd", proxyCache)
+	diskCache, err = disk.New(cacheDir2, diskCacheSize, math.MaxInt64, "zstd", proxyCache, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/server/grpc_test.go b/server/grpc_test.go
index 5b188a9..fa84e66 100644
--- a/server/grpc_test.go
+++ b/server/grpc_test.go
@@ -76,7 +76,7 @@ func TestMain(m *testing.M) {
 	// Add some overhead for likely CAS blob storage expansion.
 	cacheSize := int64(10 * maxChunkSize * 2)
 
-	diskCache, err = disk.New(dir, cacheSize, math.MaxInt64, "zstd", nil)
+	diskCache, err = disk.New(dir, cacheSize, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		fmt.Println("Test setup failed")
 		os.Exit(1)
diff --git a/server/http_test.go b/server/http_test.go
index 5740d7e..3f632a2 100644
--- a/server/http_test.go
+++ b/server/http_test.go
@@ -35,7 +35,7 @@ func TestDownloadFile(t *testing.T) {
 	// Add some overhead for likely CAS blob storage expansion.
 	cacheSize := blobSize*2 + disk.BlockSize
 
-	c, err := disk.New(cacheDir, cacheSize, math.MaxInt64, "zstd", nil)
+	c, err := disk.New(cacheDir, cacheSize, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -100,7 +100,7 @@ func TestUploadFilesConcurrently(t *testing.T) {
 	// Add some overhead for likely CAS blob storage expansion.
 	cacheSize := int64(NumUploads * blobSize * 2)
 
-	c, err := disk.New(cacheDir, cacheSize, math.MaxInt64, "zstd", nil)
+	c, err := disk.New(cacheDir, cacheSize, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -164,7 +164,7 @@ func TestUploadSameFileConcurrently(t *testing.T) {
 	// Add some overhead for likely CAS blob storage expansion.
 	cacheSize := int64(len(data) * numWorkers * 2)
 
-	c, err := disk.New(cacheDir, cacheSize, math.MaxInt64, "zstd", nil)
+	c, err := disk.New(cacheDir, cacheSize, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -205,7 +205,7 @@ func TestUploadCorruptedFile(t *testing.T) {
 
 	r := httptest.NewRequest("PUT", "/cas/"+hash, bytes.NewReader(corruptedData))
 
-	c, err := disk.New(cacheDir, 2048, math.MaxInt64, "zstd", nil)
+	c, err := disk.New(cacheDir, 2048, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -245,7 +245,7 @@ func TestUploadEmptyActionResult(t *testing.T) {
 
 	r := httptest.NewRequest("PUT", "/ac/"+hash, bytes.NewReader(data))
 
-	c, err := disk.New(cacheDir, disk.BlockSize, math.MaxInt64, "zstd", nil)
+	c, err := disk.New(cacheDir, disk.BlockSize, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -306,7 +306,7 @@ func testEmptyBlobAvailable(t *testing.T, method string) {
 	data, hash := testutils.RandomDataAndHash(0)
 	r := httptest.NewRequest(method, "/cas/"+hash, bytes.NewReader(data))
 
-	c, err := disk.New(cacheDir, 2048, math.MaxInt64, "zstd", nil)
+	c, err := disk.New(cacheDir, 2048, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -332,7 +332,7 @@ func TestStatusPage(t *testing.T) {
 
 	r := httptest.NewRequest("GET", "/status", nil)
 
-	c, err := disk.New(cacheDir, 2048, math.MaxInt64, "zstd", nil)
+	c, err := disk.New(cacheDir, 2048, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -475,7 +475,7 @@ func TestRemoteReturnsNotFound(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer os.RemoveAll(cacheDir)
-	emptyCache, err := disk.New(cacheDir, 1024, math.MaxInt64, "zstd", nil)
+	emptyCache, err := disk.New(cacheDir, 1024, math.MaxInt64, "zstd", nil, testutils.NewSilentLogger())
 	if err != nil {
 		t.Fatal(err)
 	}