feat(server): Reimplement local manifest cache backed by files

Implements a local manifest cache that uses the temporary directory to
cache manifest builds.

This is necessary due to the size of manifests: Keeping them entirely
in-memory would quickly balloon the memory usage of Nixery, unless
some mechanism for cache eviction is implemented.
This commit is contained in:
Vincent Ambo 2019-10-03 12:49:26 +01:00 committed by Vincent Ambo
parent 313e5d08f1
commit 43a642435b
5 changed files with 71 additions and 52 deletions

View file

@ -34,6 +34,8 @@ import (
"sort" "sort"
"strings" "strings"
"cloud.google.com/go/storage"
"github.com/google/nixery/config"
"github.com/google/nixery/layers" "github.com/google/nixery/layers"
"github.com/google/nixery/manifest" "github.com/google/nixery/manifest"
"golang.org/x/oauth2/google" "golang.org/x/oauth2/google"
@ -50,6 +52,15 @@ const gcsScope = "https://www.googleapis.com/auth/devstorage.read_write"
// HTTP client to use for direct calls to APIs that are not part of the SDK // HTTP client to use for direct calls to APIs that are not part of the SDK
var client = &http.Client{} var client = &http.Client{}
// State holds the runtime state that is carried around in Nixery and
// passed to builder functions.
type State struct {
Bucket *storage.BucketHandle
Cache *LocalCache
Cfg config.Config
Pop layers.Popularity
}
// Image represents the information necessary for building a container image. // Image represents the information necessary for building a container image.
// This can be either a list of package names (corresponding to keys in the // This can be either a list of package names (corresponding to keys in the
// nixpkgs set) or a Nix expression that results in a *list* of derivations. // nixpkgs set) or a Nix expression that results in a *list* of derivations.

View file

@ -20,6 +20,7 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"os"
"sync" "sync"
"github.com/google/nixery/manifest" "github.com/google/nixery/manifest"
@ -30,39 +31,63 @@ import (
type LocalCache struct { type LocalCache struct {
// Manifest cache // Manifest cache
mmtx sync.RWMutex mmtx sync.RWMutex
mcache map[string]string mdir string
// Layer cache // Layer cache
lmtx sync.RWMutex lmtx sync.RWMutex
lcache map[string]manifest.Entry lcache map[string]manifest.Entry
} }
func NewCache() LocalCache { // Creates an in-memory cache and ensures that the local file path for
return LocalCache{ // manifest caching exists.
mcache: make(map[string]string), func NewCache() (LocalCache, error) {
lcache: make(map[string]manifest.Entry), path := os.TempDir() + "/nixery"
err := os.MkdirAll(path, 0755)
if err != nil {
return LocalCache{}, err
} }
return LocalCache{
mdir: path + "/",
lcache: make(map[string]manifest.Entry),
}, nil
} }
// Retrieve a cached manifest if the build is cacheable and it exists. // Retrieve a cached manifest if the build is cacheable and it exists.
func (c *LocalCache) manifestFromLocalCache(key string) (string, bool) { func (c *LocalCache) manifestFromLocalCache(key string) (json.RawMessage, bool) {
c.mmtx.RLock() c.mmtx.RLock()
path, ok := c.mcache[key] defer c.mmtx.RUnlock()
c.mmtx.RUnlock()
if !ok { f, err := os.Open(c.mdir + key)
return "", false if err != nil {
// TODO(tazjin): Once log levels are available, this
// might warrant a debug log.
return nil, false
}
defer f.Close()
m, err := ioutil.ReadAll(f)
if err != nil {
log.Printf("Failed to read manifest '%s' from local cache: %s\n", key, err)
return nil, false
} }
return path, true return json.RawMessage(m), true
} }
// Adds the result of a manifest build to the local cache, if the // Adds the result of a manifest build to the local cache, if the
// manifest is considered cacheable. // manifest is considered cacheable.
func (c *LocalCache) localCacheManifest(key, path string) { //
// Manifests can be quite large and are cached on disk instead of in
// memory.
func (c *LocalCache) localCacheManifest(key string, m json.RawMessage) {
c.mmtx.Lock() c.mmtx.Lock()
c.mcache[key] = path defer c.mmtx.Unlock()
c.mmtx.Unlock()
err := ioutil.WriteFile(c.mdir+key, []byte(m), 0644)
if err != nil {
log.Printf("Failed to locally cache manifest for '%s': %s\n", key, err)
}
} }
// Retrieve a layer build from the local cache. // Retrieve a layer build from the local cache.
@ -84,11 +109,9 @@ func (c *LocalCache) localCacheLayer(key string, e manifest.Entry) {
// Retrieve a manifest from the cache(s). First the local cache is // Retrieve a manifest from the cache(s). First the local cache is
// checked, then the GCS-bucket cache. // checked, then the GCS-bucket cache.
func manifestFromCache(ctx context.Context, s *State, key string) (json.RawMessage, bool) { func manifestFromCache(ctx context.Context, s *State, key string) (json.RawMessage, bool) {
// path, cached := s.Cache.manifestFromLocalCache(key) if m, cached := s.Cache.manifestFromLocalCache(key); cached {
// if cached { return m, true
// return path, true }
// }
// TODO: local cache?
obj := s.Bucket.Object("manifests/" + key) obj := s.Bucket.Object("manifests/" + key)
@ -110,15 +133,15 @@ func manifestFromCache(ctx context.Context, s *State, key string) (json.RawMessa
log.Printf("Failed to read cached manifest for '%s': %s\n", key, err) log.Printf("Failed to read cached manifest for '%s': %s\n", key, err)
} }
// TODO: locally cache manifest, but the cache needs to be changed go s.Cache.localCacheManifest(key, m)
log.Printf("Retrieved manifest for sha1:%s from GCS\n", key) log.Printf("Retrieved manifest for sha1:%s from GCS\n", key)
return json.RawMessage(m), true return json.RawMessage(m), true
} }
// Add a manifest to the bucket & local caches // Add a manifest to the bucket & local caches
func cacheManifest(ctx context.Context, s *State, key string, m json.RawMessage) { func cacheManifest(ctx context.Context, s *State, key string, m json.RawMessage) {
// go s.Cache.localCacheManifest(key, path) go s.Cache.localCacheManifest(key, m)
// TODO local cache
obj := s.Bucket.Object("manifests/" + key) obj := s.Bucket.Object("manifests/" + key)
w := obj.NewWriter(ctx) w := obj.NewWriter(ctx)

View file

@ -1,24 +0,0 @@
package builder
import (
"cloud.google.com/go/storage"
"github.com/google/nixery/config"
"github.com/google/nixery/layers"
)
// State holds the runtime state that is carried around in Nixery and
// passed to builder functions.
type State struct {
Bucket *storage.BucketHandle
Cache LocalCache
Cfg config.Config
Pop layers.Popularity
}
func NewState(bucket *storage.BucketHandle, cfg config.Config) State {
return State{
Bucket: bucket,
Cfg: cfg,
Cache: NewCache(),
}
}

View file

@ -71,13 +71,13 @@ type Config struct {
PopUrl string // URL to the Nix package popularity count PopUrl string // URL to the Nix package popularity count
} }
func FromEnv() (*Config, error) { func FromEnv() (Config, error) {
pkgs, err := pkgSourceFromEnv() pkgs, err := pkgSourceFromEnv()
if err != nil { if err != nil {
return nil, err return Config{}, err
} }
return &Config{ return Config{
Bucket: getConfig("BUCKET", "GCS bucket for layer storage", ""), Bucket: getConfig("BUCKET", "GCS bucket for layer storage", ""),
Port: getConfig("PORT", "HTTP port", ""), Port: getConfig("PORT", "HTTP port", ""),
Pkgs: pkgs, Pkgs: pkgs,

View file

@ -194,8 +194,17 @@ func main() {
} }
ctx := context.Background() ctx := context.Background()
bucket := prepareBucket(ctx, cfg) bucket := prepareBucket(ctx, &cfg)
state := builder.NewState(bucket, *cfg) cache, err := builder.NewCache()
if err != nil {
log.Fatalln("Failed to instantiate build cache", err)
}
state := builder.State{
Bucket: bucket,
Cache: &cache,
Cfg: cfg,
}
log.Printf("Starting Nixery on port %s\n", cfg.Port) log.Printf("Starting Nixery on port %s\n", cfg.Port)