summaryrefslogtreecommitdiff
path: root/cache.go
diff options
context:
space:
mode:
Diffstat (limited to 'cache.go')
-rw-r--r--cache.go129
1 files changed, 129 insertions, 0 deletions
diff --git a/cache.go b/cache.go
new file mode 100644
index 0000000..d43fa77
--- /dev/null
+++ b/cache.go
@@ -0,0 +1,129 @@
+package main
+
+import (
+ "context"
+ "log"
+ "path"
+ "sync"
+ "time"
+)
+
+// A PackageCache holds information about Go packages and their upstream
+// repositories.
+//
+// Given a package name, it looks for a similarly named repository in one of
+// the upstream hosts and holds on to the result for quicker subsequent access.
+type PackageCache struct {
+ // ExpireAfter is how long to consider a found repository valid for. As
+ // repositories do not frequently move around, this could be rather long,
+ // though it also determines how long a user might have to wait for a new
+ // repository to become available if they previously got a 404.
+ ExpireAfter time.Duration
+
+ // UpstreamTimeout is how long we should allow HTTP requests to upstream
+ // hosts to carry on before they are canceled.
+ UpstreamTimeout time.Duration
+
+ // Hosts are the RepoHosts which should be checked for a repository matching
+ // the package name. Hosts are checked in order; if two hosts both have a
+ // repo with the desired name, the repo host which is listed first will
+ // "win".
+ Hosts []RepoHost
+
+ // Logger is a logger for diagnostics
+ Logger *log.Logger
+
+ // CanonicalPrefix is the package prefix to use when naming packages. E.g.,
+ // when looking for a package named "conf", we'll treat the package name as
+ // CanonicalPrefix + "/conf", e.g. burwell.io/conf.
+ CanonicalPrefix string
+
+ entries sync.Map // concurrency-safe map[string]entry
+}
+
+type entry struct {
+ t time.Time
+ pkg *Package
+}
+
+// A Package represents a Go package's canonical name and its source git
+// repository.
+type Package struct {
+ Name string
+ Repo string
+}
+
+// Get loads package name from the cache, consulting upstream repositories to
+// find a matching repo if not present. As this method is meant to be called in
+// the critical path of an incoming HTTP request, it can also be cancelled with
+// the supplied context.
+func (c *PackageCache) Get(ctx context.Context, name string) (Package, bool) {
+ val, ok := c.entries.Load(name)
+ if !ok {
+ return c.load(ctx, name)
+ }
+ ent := val.(entry)
+
+ if time.Now().After(ent.t.Add(c.ExpireAfter)) {
+ return c.load(ctx, name)
+ }
+
+ if ent.pkg == nil {
+ return Package{}, false
+ }
+
+ return *ent.pkg, true
+}
+
+// load looks for a package in the upstream repos, and if found, stores it in
+// the cache. If no matching repo is found, a nil entry is stored in the map so
+// we don't immediately re-check the upstreams for subsequent requests.
+func (c *PackageCache) load(ctx context.Context, name string) (Package, bool) {
+ pkg, ok := c.find(ctx, name)
+ if !ok {
+ c.entries.Store(name, entry{t: time.Now()})
+ return Package{}, false
+ }
+ c.entries.Store(name, entry{t: time.Now(), pkg: &pkg})
+ return pkg, true
+}
+
+// requestContext creates a context for making HTTP requests to upstream hosts.
+func (c *PackageCache) requestContext(ctx context.Context) (context.Context, context.CancelFunc) {
+ if c.UpstreamTimeout == 0 {
+ return ctx, func() {}
+ }
+ return context.WithTimeout(ctx, c.UpstreamTimeout)
+}
+
+// logf logs a message using the configured logger, if any.
+func (c *PackageCache) logf(msg string, args ...interface{}) {
+ if c.Logger == nil {
+ return
+ }
+ c.Logger.Printf(msg, args...)
+}
+
+// find consults the configured upstream hosts to try to find a matching repo
+// for package name.
+func (c *PackageCache) find(ctx context.Context, name string) (Package, bool) {
+ c.logf("loading %s", name)
+ for _, h := range c.Hosts {
+ reqCtx, cancel := c.requestContext(ctx)
+ defer cancel()
+ repo, ok, err := h.HostsRepo(reqCtx, name)
+ if err != nil {
+ c.logf("error checking upstream for repo %s: %v", name, err)
+ return Package{}, false
+ }
+ if ok {
+ c.logf("found repo %s: %s", name, repo)
+ return Package{
+ Name: path.Join(c.CanonicalPrefix, name),
+ Repo: repo,
+ }, true
+ }
+ }
+ c.logf("could not find repo %s on any host", name)
+ return Package{}, false
+}