Commit 90ffcd3e9d
Verified · cmc ci/build: success ci/lint: success ci/test: success
Layout: unified · split
app/httpclient.go +33 −4
| @@ -81,11 +81,40 @@ func tunedTransport() *http.Transport { | |||
| 81 | return t | 81 | return t |
| 82 | } | 82 | } |
| 83 | 83 | ||
| 84 | // InstallDAThrottle wraps http.DefaultTransport with the rate/concurrency limits and | 84 | // daCache is the API response cache shared by every transport, or nil when |
| 85 | // timeouts above. Call once at startup, before any DeviantArt request is made. | 85 | // api-cache.enabled is false. |
| 86 | var daCache *apiCache | ||
| 87 | |||
| 88 | // chain wraps base with the throttle and, when enabled, the cache in front | ||
| 89 | // of it, so a hit never spends a throttle slot. | ||
| 90 | func chain(base http.RoundTripper) http.RoundTripper { | ||
| 91 | rt := throttled(base) | ||
| 92 | if daCache != nil { | ||
| 93 | return daCache.transport(rt) | ||
| 94 | } | ||
| 95 | return rt | ||
| 96 | } | ||
| 97 | |||
| 98 | // logCacheStatsForever prints one line an hour so an operator can see the | ||
| 99 | // cache working without an endpoint. Run it in its own goroutine. | ||
| 100 | func logCacheStatsForever(c *apiCache) { | ||
| 101 | for { | ||
| 102 | time.Sleep(time.Hour) | ||
| 103 | hits, misses, entries, held := c.stats() | ||
| 104 | println("api cache:", hits, "hits,", misses, "misses,", entries, "entries,", held>>20, "MB held") | ||
| 105 | } | ||
| 106 | } | ||
| 107 | |||
| 108 | // InstallDAThrottle wraps http.DefaultTransport with the rate/concurrency limits | ||
| 109 | // and timeouts above, and with the API response cache when it is enabled. Call | ||
| 110 | // once at startup, after ExecuteConfig and before any DeviantArt request. | ||
| 86 | func InstallDAThrottle() { | 111 | func InstallDAThrottle() { |
| 87 | baseTransport = tunedTransport() | 112 | baseTransport = tunedTransport() |
| 88 | http.DefaultTransport = throttled(baseTransport) | 113 | if CFG.APICache.Enabled { |
| 114 | daCache = newAPICache(CFG.APICache.MaxSize<<20, apiCacheTTL) | ||
| 115 | go logCacheStatsForever(daCache) | ||
| 116 | } | ||
| 117 | http.DefaultTransport = chain(baseTransport) | ||
| 89 | } | 118 | } |
| 90 | 119 | ||
| 91 | // throttled wraps base with the DeviantArt rate and concurrency limits. | 120 | // throttled wraps base with the DeviantArt rate and concurrency limits. |
| @@ -104,5 +133,5 @@ func ProxiedTransport(proxy *url.URL) http.RoundTripper { | |||
| 104 | base = tunedTransport() | 133 | base = tunedTransport() |
| 105 | } | 134 | } |
| 106 | base.Proxy = http.ProxyURL(proxy) | 135 | base.Proxy = http.ProxyURL(proxy) |
| 107 | return throttled(base) | 136 | return chain(base) |
| 108 | } | 137 | } |
app/httpclient_test.go +14 −4
| @@ -116,14 +116,24 @@ func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { retu | |||
| 116 | // InstallDAThrottle must preserve proxy-from-environment so HTTPS_PROXY (VPN | 116 | // InstallDAThrottle must preserve proxy-from-environment so HTTPS_PROXY (VPN |
| 117 | // egress) keeps working, and must not panic on a repeat call. | 117 | // egress) keeps working, and must not panic on a repeat call. |
| 118 | func TestInstallDAThrottlePreservesProxy(t *testing.T) { | 118 | func TestInstallDAThrottlePreservesProxy(t *testing.T) { |
| 119 | orig := http.DefaultTransport | 119 | orig, origCache := http.DefaultTransport, daCache |
| 120 | defer func() { http.DefaultTransport = orig }() | 120 | defer func() { http.DefaultTransport, daCache = orig, origCache }() |
| 121 | 121 | ||
| 122 | InstallDAThrottle() | 122 | InstallDAThrottle() |
| 123 | 123 | ||
| 124 | th, ok := http.DefaultTransport.(*daThrottle) | 124 | // With api-cache on (the default) the cache is outermost and the throttle |
| 125 | // sits inside it; with it off the throttle is outermost. | ||
| 126 | rt := http.DefaultTransport | ||
| 127 | if CFG.APICache.Enabled { | ||
| 128 | ct, ok := rt.(*cachedTransport) | ||
| 129 | if !ok { | ||
| 130 | t.Fatalf("DefaultTransport is not the cache, got %T", rt) | ||
| 131 | } | ||
| 132 | rt = ct.base | ||
| 133 | } | ||
| 134 | th, ok := rt.(*daThrottle) | ||
| 125 | if !ok { | 135 | if !ok { |
| 126 | t.Fatalf("DefaultTransport was not wrapped, got %T", http.DefaultTransport) | 136 | t.Fatalf("DefaultTransport was not wrapped by the throttle, got %T", rt) |
| 127 | } | 137 | } |
| 128 | base, ok := th.base.(*http.Transport) | 138 | base, ok := th.base.(*http.Transport) |
| 129 | if !ok { | 139 | if !ok { |