Check cached sources with upstream after the cache TTL (closes #69)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Cached images were served until eviction, so upstream changes were missed. After 24 hours a request checks the source with upstream, sending its stored ETag and Last-Modified. A 304 renews it with no download or conversion. New bytes replace it; older variants are converted again. A failed check serves the cached image and puts the URL in the negative cache. Images not checked for 30 days are evicted. Variant rows keep the source's ETag and Last-Modified for checking a variant of an evicted source. A variant file's modification time is its source's check time when it was made or renewed. Hits read it without a database call. The ETag includes it, so new bytes get a new ETag. Model: opus-5-5
This commit is contained in:
1 parent
b3b8408d18
commit
445ff628fd
29 files changed
+1709
-269
No files matched your search
@@ -80,8 +80,13 @@ var (
|
||||
|
||||
// Fetcher retrieves content from upstream origins.
|
||||
type Fetcher interface {
|
||||
// Fetch retrieves the content of the source image.
|
||||
Fetch(ctx context.Context, source imagerequest.Source) (*FetchResult, error)
|
||||
// Fetch retrieves the content of the source image. A non-empty
|
||||
// ifNoneMatch or ifModifiedSince is sent as the request header of that
|
||||
// name, and upstream may then answer 304 Not Modified with no content.
|
||||
Fetch(
|
||||
ctx context.Context, source imagerequest.Source,
|
||||
ifNoneMatch, ifModifiedSince string,
|
||||
) (*FetchResult, error)
|
||||
}
|
||||
|
||||
// FetchResult contains the result of fetching from upstream.
|
||||
@@ -223,9 +228,13 @@ func New(config *Config) *HTTPFetcher {
|
||||
// Fetch retrieves the source image with SSRF protection, over https, or over
|
||||
// http when AllowHTTP is set. When MaxConnections fetches are already in
|
||||
// progress, it waits up to ConnectionWaitTimeout for one to finish, then
|
||||
// fails with ErrTooManyConnections.
|
||||
// fails with ErrTooManyConnections. A non-empty ifNoneMatch or
|
||||
// ifModifiedSince is sent as the If-None-Match or If-Modified-Since header.
|
||||
// Upstream's 304 Not Modified to such a request is a result with StatusCode
|
||||
// 304 and empty Content; to any other request it is ErrUpstreamError.
|
||||
func (f *HTTPFetcher) Fetch(
|
||||
ctx context.Context, source imagerequest.Source,
|
||||
ifNoneMatch, ifModifiedSince string,
|
||||
) (*FetchResult, error) {
|
||||
sourceURL := source.URL(f.config.AllowHTTP)
|
||||
|
||||
@@ -252,17 +261,7 @@ func (f *HTTPFetcher) Fetch(
|
||||
req := &http.Request{
|
||||
Method: http.MethodGet,
|
||||
URL: sourceURL,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
req.Header.Set("Accept", strings.Join(allowedContentTypes(), ", "))
|
||||
|
||||
// The ID of the request this fetch serves, so the fetch can be found in
|
||||
// the upstream host's logs
|
||||
requestID := middleware.GetReqID(ctx)
|
||||
if requestID != "" {
|
||||
req.Header.Set(middleware.RequestIDHeader, requestID)
|
||||
Header: requestHeaders(ctx, ifNoneMatch, ifModifiedSince),
|
||||
}
|
||||
|
||||
// Use httptrace to capture connection details
|
||||
@@ -291,6 +290,20 @@ func (f *HTTPFetcher) Fetch(
|
||||
return nil, fmt.Errorf("upstream request failed: %w", err)
|
||||
}
|
||||
|
||||
// The deferred release gives the connection back, as no body follows
|
||||
conditional := ifNoneMatch != "" || ifModifiedSince != ""
|
||||
if conditional && resp.StatusCode == http.StatusNotModified {
|
||||
_ = resp.Body.Close()
|
||||
|
||||
return &FetchResult{
|
||||
Content: http.NoBody,
|
||||
Headers: resp.Header,
|
||||
StatusCode: resp.StatusCode,
|
||||
FetchDurationMs: fetchDuration.Milliseconds(),
|
||||
RemoteAddr: remoteAddr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
result, err := f.buildResult(resp, remoteAddr, fetchDuration, release)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -303,6 +316,33 @@ func (f *HTTPFetcher) Fetch(
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// requestHeaders returns the headers of a fetch: User-Agent, Accept, the
|
||||
// ID of the request the fetch serves, so the fetch can be found in the
|
||||
// upstream host's logs, and a non-empty ifNoneMatch or ifModifiedSince.
|
||||
func requestHeaders(
|
||||
ctx context.Context, ifNoneMatch, ifModifiedSince string,
|
||||
) http.Header {
|
||||
header := make(http.Header)
|
||||
|
||||
header.Set("User-Agent", userAgent)
|
||||
header.Set("Accept", strings.Join(allowedContentTypes(), ", "))
|
||||
|
||||
requestID := middleware.GetReqID(ctx)
|
||||
if requestID != "" {
|
||||
header.Set(middleware.RequestIDHeader, requestID)
|
||||
}
|
||||
|
||||
if ifNoneMatch != "" {
|
||||
header.Set("If-None-Match", ifNoneMatch)
|
||||
}
|
||||
|
||||
if ifModifiedSince != "" {
|
||||
header.Set("If-Modified-Since", ifModifiedSince)
|
||||
}
|
||||
|
||||
return header
|
||||
}
|
||||
|
||||
// acquireConnection takes a slot for host, then one of the slots shared by
|
||||
// all hosts, and returns the func that gives both back. The host's slot
|
||||
// comes first, so fetches queued for one busy host hold no shared slot.
|
||||
|
||||
Reference in new issue
Block a user