summaryrefslogtreecommitdiff
path: root/cache.go
blob: d43fa7740f624209c62003ff9d7059ad6a22aae0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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
}