-
Notifications
You must be signed in to change notification settings - Fork 55
/
rtv.go
162 lines (141 loc) · 3.49 KB
/
rtv.go
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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
package rtv
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"net/url"
"sync"
"time"
"github.com/pkg/errors"
)
const (
defaultHTTPTimeout = 1 * time.Minute
defaultPollInterval = 1 * time.Hour
)
// not a const for testing purposes
var rtvHost = "https://cdn.ampproject.org"
// rtvData stores the AMP runtime version number and the CSS for that version
// Note: fields must be exported for json unmarshaling.
type rtvData struct {
RTV string `json:"ampRuntimeVersion"`
CSSURL string `json:"ampCssUrl"`
CanaryPercentage, CSS string
}
type RTVCache struct {
d *rtvData
c http.Client
lk sync.Mutex
stop chan struct{}
}
// New returns a new cache for storing AMP runtime values, or an
// error if there was a problem initializing. To have it auto-refresh,
// call StartCron().
func New() (*RTVCache, error) {
r := &RTVCache{c: http.Client{Timeout: defaultHTTPTimeout}, d: &rtvData{}, stop: make(chan struct{})}
if err := r.poll(); err != nil {
return nil, err
}
return r, nil
}
// StartCron starts a cron job to re-fill the RTVCache hourly.
func (r *RTVCache) StartCron() {
go func() {
ticker := time.NewTicker(defaultPollInterval)
for {
select {
case <-ticker.C:
r.poll() // Ignores error return.
case <-r.stop:
ticker.Stop()
return
}
}
}()
}
// StopCron stops the cron job.
func (r *RTVCache) StopCron() {
r.stop <- struct{}{}
}
// getRTVData returns the cached rtvData.
func (r *RTVCache) getRTVData() *rtvData {
r.lk.Lock()
defer r.lk.Unlock()
return r.d
}
// GetRTV returns the cached value for the runtime version.
func (r *RTVCache) GetRTV() string {
return r.getRTVData().RTV
}
// GetCSS returns the cached value for the inline CSS.
func (r *RTVCache) GetCSS() string {
return r.getRTVData().CSS
}
// poll attempts to re-populate the RTVCache, returning an error if there
// were any problems.
func (r *RTVCache) poll() error {
// Fetch the runtime metadata
d, err := getMetadata(r)
if err != nil {
return err
}
// If the value is unchanged, skip CSS call
if d.RTV == r.GetRTV() {
return nil
}
// Fetch the CSS payload
var b []byte
b, err = getRTVBody(r.c, d.CSSURL)
if err != nil {
return err
}
d.CSS = string(b)
// No errors, update cache.
r.lk.Lock()
defer r.lk.Unlock()
r.d = d
return nil
}
// getMetadata fetches the JSON from the metadata endpoint and returns the
// data parsed as a struct.
func getMetadata(r *RTVCache) (*rtvData, error) {
// Fetch the runtime metadata json
b, err := getRTVBody(r.c, rtvHost+"/rtv/metadata")
if err != nil {
return nil, err
}
var d rtvData
err = json.Unmarshal(b, &d)
if err != nil {
return nil, err
}
// Minimal validation of expected values.
if d.RTV == "" {
return nil, errors.Errorf("Could not unmarshal RTV value from %s", b)
}
if d.CSSURL == "" {
return nil, errors.Errorf("Could not unmarshal CSS URL value from %s", b)
}
if _, err := url.Parse(d.CSSURL); err != nil {
return nil, errors.Wrapf(err, "Error parsing CSS URL %s", d.CSSURL)
}
return &d, nil
}
// getRTVBody returns the body contents of the given url, or an error
// if there was problem.
func getRTVBody(c http.Client, url string) ([]byte, error) {
log.Printf("Fetching URL: %q\n", url)
resp, err := c.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, errors.Errorf("Non-200 response fetching %s, %+v", url, resp)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}