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
}
|