From d4eebc634e4edaa9a8789fd7e3877b4d56423293 Mon Sep 17 00:00:00 2001 From: Ben Burwell Date: Fri, 13 Sep 2019 14:49:08 -0400 Subject: initial commit --- cache.go | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 cache.go (limited to 'cache.go') 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 +} -- cgit v1.2.3