gopls/internal/govulncheck: add in-memory cache for vulndb

We used govulncheck.DefaultCache that is a cache in file system.
This helps clients from hitting the network excessively.
Mod diagnostic is computed very frequently, however, even file
read & unmarshal contents for every module for every diagnostic
computation can become slow. This CL adds an additional layer of
caching that holds the information in memory. To avoid network
access across gopls restarts, this cache is backed by the file
system cache where the cached data is persisted on disk.

Change-Id: Ib4e33a51b88453c09f424e944be2f6fc578eb1ea
Reviewed-on: https://go-review.googlesource.com/c/tools/+/453155
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
Reviewed-by: Robert Findley <rfindley@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
diff --git a/gopls/internal/govulncheck/vulncache.go b/gopls/internal/govulncheck/vulncache.go
new file mode 100644
index 0000000..a259f02
--- /dev/null
+++ b/gopls/internal/govulncheck/vulncache.go
@@ -0,0 +1,105 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build go1.18
+// +build go1.18
+
+package govulncheck
+
+import (
+	"sync"
+	"time"
+
+	vulnc "golang.org/x/vuln/client"
+	"golang.org/x/vuln/osv"
+)
+
+// inMemoryCache is an implementation of the [client.Cache] interface
+// that "decorates" another instance of that interface to provide
+// an additional layer of (memory-based) caching.
+type inMemoryCache struct {
+	mu         sync.Mutex
+	underlying vulnc.Cache
+	db         map[string]*db
+}
+
+var _ vulnc.Cache = &inMemoryCache{}
+
+type db struct {
+	retrieved time.Time
+	index     vulnc.DBIndex
+	entry     map[string][]*osv.Entry
+}
+
+// NewInMemoryCache returns a new memory-based cache that decorates
+// the provided cache (file-based, perhaps).
+func NewInMemoryCache(underlying vulnc.Cache) *inMemoryCache {
+	return &inMemoryCache{
+		underlying: underlying,
+		db:         make(map[string]*db),
+	}
+}
+
+func (c *inMemoryCache) lookupDBLocked(dbName string) *db {
+	cached := c.db[dbName]
+	if cached == nil {
+		cached = &db{entry: make(map[string][]*osv.Entry)}
+		c.db[dbName] = cached
+	}
+	return cached
+}
+
+// ReadIndex returns the index for dbName from the cache, or returns zero values
+// if it is not present.
+func (c *inMemoryCache) ReadIndex(dbName string) (vulnc.DBIndex, time.Time, error) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	cached := c.lookupDBLocked(dbName)
+
+	if cached.retrieved.IsZero() {
+		// First time ReadIndex is called.
+		index, retrieved, err := c.underlying.ReadIndex(dbName)
+		if err != nil {
+			return index, retrieved, err
+		}
+		cached.index, cached.retrieved = index, retrieved
+	}
+	return cached.index, cached.retrieved, nil
+}
+
+// WriteIndex puts the index and retrieved time into the cache.
+func (c *inMemoryCache) WriteIndex(dbName string, index vulnc.DBIndex, retrieved time.Time) error {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	cached := c.lookupDBLocked(dbName)
+	cached.index, cached.retrieved = index, retrieved
+	// TODO(hyangah): shouldn't we invalidate all cached entries?
+	return c.underlying.WriteIndex(dbName, index, retrieved)
+}
+
+// ReadEntries returns the vulndb entries for path from the cache.
+func (c *inMemoryCache) ReadEntries(dbName, path string) ([]*osv.Entry, error) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	cached := c.lookupDBLocked(dbName)
+	entries, ok := cached.entry[path]
+	if !ok {
+		// cache miss
+		entries, err := c.underlying.ReadEntries(dbName, path)
+		if err != nil {
+			return entries, err
+		}
+		cached.entry[path] = entries
+	}
+	return entries, nil
+}
+
+// WriteEntries puts the entries for path into the cache.
+func (c *inMemoryCache) WriteEntries(dbName, path string, entries []*osv.Entry) error {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	cached := c.lookupDBLocked(dbName)
+	cached.entry[path] = entries
+	return c.underlying.WriteEntries(dbName, path, entries)
+}
diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go
index 004541f..86db0a3 100644
--- a/gopls/internal/vulncheck/command.go
+++ b/gopls/internal/vulncheck/command.go
@@ -385,7 +385,7 @@
 	// Request vuln entries from remote service.
 	cli, err := client.NewClient(
 		findGOVULNDB(snapshot.View().Options().EnvSlice()),
-		client.Options{HTTPCache: govulncheck.DefaultCache()})
+		client.Options{HTTPCache: govulncheck.NewInMemoryCache(govulncheck.DefaultCache())})
 	if err != nil {
 		return nil, err
 	}