cmd/frontend: add a cache for vuln data

The vulndb.Client already supports caching; we just have to supply an
implementation.

The only implementation in golang.org/x/vulndb uses the filesystem, so
we can't use it on App Engine. Provide an in-memory implementation
instead.

For golang/go#48223

Change-Id: I0431921dcabfb5546350dff095ae6aa5668ad892
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/347969
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go
index 04bf2b5..2bd0679 100644
--- a/cmd/frontend/main.go
+++ b/cmd/frontend/main.go
@@ -105,7 +105,9 @@
 	}
 
 	rc := cmdconfig.ReportingClient(ctx, cfg)
-	vc, err := vulndbc.NewClient([]string{cfg.VulnDB}, vulndbc.Options{})
+	vc, err := vulndbc.NewClient([]string{cfg.VulnDB}, vulndbc.Options{
+		HTTPCache: newVulndbCache(),
+	})
 	if err != nil {
 		log.Fatalf(ctx, "vulndbc.NewClient: %v", err)
 	}
diff --git a/cmd/frontend/vulndb_cache.go b/cmd/frontend/vulndb_cache.go
new file mode 100644
index 0000000..eec5d38
--- /dev/null
+++ b/cmd/frontend/vulndb_cache.go
@@ -0,0 +1,93 @@
+// Copyright 2021 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.
+
+package main
+
+import (
+	"fmt"
+	"sync"
+	"time"
+
+	lru "github.com/hashicorp/golang-lru"
+	"golang.org/x/vulndb/osv"
+)
+
+// vulndbCache implements the golang.org/x/vulndb/client.Cache interface. It
+// stores in memory the index and a limited number of path entries for one DB.
+type vulndbCache struct {
+	mu         sync.Mutex
+	dbName     string // support only one DB
+	index      osv.DBIndex
+	retrieved  time.Time
+	entryCache *lru.Cache
+}
+
+func newVulndbCache() *vulndbCache {
+	const size = 100
+	ec, err := lru.New(size)
+	if err != nil {
+		// Can only happen if size is bad, and we control it.
+		panic(err)
+	}
+	return &vulndbCache{entryCache: ec}
+}
+
+// ReadIndex returns the index for dbName from the cache, or returns zero values
+// if it is not present.
+func (c *vulndbCache) ReadIndex(dbName string) (osv.DBIndex, time.Time, error) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	if err := c.checkDB(dbName); err != nil {
+		return nil, time.Time{}, err
+	}
+	return c.index, c.retrieved, nil
+}
+
+// WriteIndex puts the index and retrieved time into the cache.
+func (c *vulndbCache) WriteIndex(dbName string, index osv.DBIndex, retrieved time.Time) error {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	if err := c.checkDB(dbName); err != nil {
+		return err
+	}
+	c.index = index
+	c.retrieved = retrieved
+	return nil
+}
+
+// ReadEntries returns the vulndb entries for path from the cache, or
+// nil if not prsent.
+func (c *vulndbCache) ReadEntries(dbName, path string) ([]*osv.Entry, error) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	if err := c.checkDB(dbName); err != nil {
+		return nil, err
+	}
+	if entries, ok := c.entryCache.Get(path); ok {
+		return entries.([]*osv.Entry), nil
+	}
+	return nil, nil
+}
+
+// WriteEntries puts the entries for path into the cache.
+func (c *vulndbCache) WriteEntries(dbName, path string, entries []*osv.Entry) error {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	if err := c.checkDB(dbName); err != nil {
+		return err
+	}
+	c.entryCache.Add(path, entries)
+	return nil
+}
+
+func (c *vulndbCache) checkDB(name string) error {
+	if c.dbName == "" {
+		c.dbName = name
+		return nil
+	}
+	if c.dbName != name {
+		return fmt.Errorf("vulndbCache: called with db name %q, expected %q", name, c.dbName)
+	}
+	return nil
+}