-
Notifications
You must be signed in to change notification settings - Fork 13
Add stale-if-error logic to http caching layer #236
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
64a116c
6937241
45fd824
69108c8
aa717c3
cae31ed
0f0cf6f
2815678
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,9 @@ type CandidateResponse struct { | |
| overrideStaleWhileRevalidate uint32 // seconds | ||
| useSWR bool | ||
|
|
||
| overrideStaleIfError uint32 // seconds | ||
| useSIE bool | ||
|
|
||
| extraSurrogateKeys string | ||
| overrideSurrogateKeys string | ||
| useSurrogate bool | ||
|
|
@@ -47,15 +50,16 @@ type cacheResponse struct { | |
| } | ||
|
|
||
| type cacheWriteOptions struct { | ||
| maxAge uint32 // seconds | ||
| vary string | ||
| useVary bool | ||
| age uint32 // seconds | ||
| stale uint32 // seconds | ||
| surrogate string | ||
| length uint64 | ||
| useLength bool | ||
| sensitive bool | ||
| maxAge uint32 // seconds | ||
| vary string | ||
| useVary bool | ||
| age uint32 // seconds | ||
| staleWhileRevalidate uint32 // seconds | ||
| surrogate string | ||
| length uint64 | ||
| useLength bool | ||
| sensitive bool | ||
| staleIfError uint32 // seconds | ||
|
|
||
| abiOpts fastly.HTTPCacheWriteOptions | ||
| } | ||
|
|
@@ -69,9 +73,10 @@ func (opts *cacheWriteOptions) flushToABI() { | |
| opts.abiOpts.SetMaxAgeNs(u32sTou64ns(opts.maxAge)) | ||
| opts.abiOpts.SetVaryRule(opts.vary) | ||
| opts.abiOpts.SetInitialAgeNs(u32sTou64ns(opts.age)) | ||
| opts.abiOpts.SetStaleWhileRevalidateNs(u32sTou64ns(opts.stale)) | ||
| opts.abiOpts.SetStaleWhileRevalidateNs(u32sTou64ns(opts.staleWhileRevalidate)) | ||
| opts.abiOpts.SetSurrogateKeys(opts.surrogate) | ||
| opts.abiOpts.SetSensitiveData(opts.sensitive) | ||
| opts.abiOpts.SetStaleIfErrorNs(u32sTou64ns(opts.staleIfError)) | ||
| } | ||
|
|
||
| func (opts *cacheWriteOptions) loadFromABI() { | ||
|
|
@@ -81,11 +86,15 @@ func (opts *cacheWriteOptions) loadFromABI() { | |
| opts.age = u64nsTou32s(ns) | ||
| } | ||
| if ns, ok := opts.abiOpts.StaleWhileRevalidateNs(); ok { | ||
| opts.stale = u64nsTou32s(ns) | ||
| opts.staleWhileRevalidate = u64nsTou32s(ns) | ||
| } | ||
| opts.surrogate, _ = opts.abiOpts.SurrogateKeys() | ||
| opts.length, opts.useLength = opts.abiOpts.Length() | ||
| opts.sensitive = opts.abiOpts.SensitiveData() | ||
|
|
||
| if ns, ok := opts.abiOpts.StaleIfErrorNs(); ok { | ||
| opts.staleIfError = u64nsTou32s(ns) | ||
| } | ||
| } | ||
|
|
||
| func (opts *cacheWriteOptions) loadFromHandle(c *fastly.HTTPCacheHandle) error { | ||
|
|
@@ -111,7 +120,7 @@ func (opts *cacheWriteOptions) loadFromHandle(c *fastly.HTTPCacheHandle) error { | |
| if ns, err := fastly.HTTPCacheGetStaleWhileRevalidateNs(c); err != nil { | ||
| return fmt.Errorf("get stale while revalidate: %w", err) | ||
| } else { | ||
| opts.stale = u64nsTou32s(uint64(ns)) | ||
| opts.staleWhileRevalidate = u64nsTou32s(uint64(ns)) | ||
| } | ||
|
|
||
| opts.surrogate, err = fastly.HTTPCacheGetSurrogateKeys(c) | ||
|
|
@@ -136,6 +145,12 @@ func (opts *cacheWriteOptions) loadFromHandle(c *fastly.HTTPCacheHandle) error { | |
| return fmt.Errorf("get sensitive data: %w", err) | ||
| } | ||
|
|
||
| if ns, err := fastly.HTTPCacheGetStaleIfErrorNs(c); err != nil { | ||
| return fmt.Errorf("get stale if error: %w", err) | ||
| } else { | ||
| opts.staleIfError = u64nsTou32s(uint64(ns)) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
|
|
@@ -144,24 +159,15 @@ const ( | |
| cacheStorageActionInvalid = 0xffff | ||
| ) | ||
|
|
||
| func httpCacheWait(c *fastly.HTTPCacheHandle) error { | ||
| _, err := fastly.HTTPCacheGetState(c) | ||
| if err != nil { | ||
| return fmt.Errorf("get state: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func httpCacheMustInsertOrUpdate(c *fastly.HTTPCacheHandle) (bool, error) { | ||
| func httpCacheWait(c *fastly.HTTPCacheHandle) (fastly.CacheLookupState, error) { | ||
| state, err := fastly.HTTPCacheGetState(c) | ||
| if err != nil { | ||
| return false, fmt.Errorf("get state: %w", err) | ||
|
|
||
| return 0, fmt.Errorf("get state: %w", err) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Would be a little clearer to define a |
||
| } | ||
| return state&fastly.CacheLookupStateMustInsertOrUpdate == fastly.CacheLookupStateMustInsertOrUpdate, nil | ||
| return state, nil | ||
| } | ||
|
|
||
| func httpCacheGetFoundResponse(c *fastly.HTTPCacheHandle, req *Request, backend string, transformForClient bool) (*Response, error) { | ||
| func httpCacheGetFoundResponse(c *fastly.HTTPCacheHandle, req *Request, backend string, transformForClient bool, wasHit bool) (*Response, error) { | ||
| abiResp, abiBody, err := fastly.HTTPCacheGetFoundResponse(c, transformForClient) | ||
| if err != nil { | ||
| if status, ok := fastly.IsFastlyError(err); ok && status == fastly.FastlyStatusNone { | ||
|
|
@@ -170,9 +176,13 @@ func httpCacheGetFoundResponse(c *fastly.HTTPCacheHandle, req *Request, backend | |
| return nil, fmt.Errorf("get found response: %w", err) | ||
| } | ||
|
|
||
| hits, err := fastly.HTTPCacheGetHits(c) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("get hits: %w", err) | ||
| var hits uint64 | ||
| if wasHit { | ||
|
dgryski marked this conversation as resolved.
|
||
| h, err := fastly.HTTPCacheGetHits(c) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("get hits: %w", err) | ||
| } | ||
| hits = uint64(h) | ||
| } | ||
|
|
||
| var opts cacheWriteOptions | ||
|
|
@@ -189,7 +199,7 @@ func httpCacheGetFoundResponse(c *fastly.HTTPCacheHandle, req *Request, backend | |
| resp.cacheResponse = cacheResponse{ | ||
| cacheWriteOptions: opts, | ||
| storageAction: cacheStorageActionInvalid, | ||
| hits: uint64(hits), | ||
| hits: hits, | ||
| } | ||
| return resp, nil | ||
| } | ||
|
|
@@ -239,6 +249,7 @@ func newCandidate(c *fastly.HTTPCacheHandle, opts *CacheOptions, abiResp *fastly | |
| overrideStorageAction: 0, | ||
| overridePCI: opts.PCI, | ||
| overrideStaleWhileRevalidate: opts.StaleWhileRevalidate, | ||
| overrideStaleIfError: opts.StaleIfError, | ||
| extraSurrogateKeys: opts.SurrogateKey, | ||
| overrideSurrogateKeys: "", | ||
| overrideTTL: opts.TTL, | ||
|
|
@@ -253,6 +264,10 @@ func newCandidate(c *fastly.HTTPCacheHandle, opts *CacheOptions, abiResp *fastly | |
| candidate.useSWR = true | ||
| } | ||
|
|
||
| if candidate.overrideStaleIfError != 0 { | ||
| candidate.useSIE = true | ||
| } | ||
|
|
||
| if candidate.overridePCI { | ||
| candidate.usePCI = true | ||
| } | ||
|
|
@@ -351,7 +366,7 @@ func (candidateResponse *CandidateResponse) IsStale() (bool, error) { | |
| if err != nil { | ||
| return false, fmt.Errorf("get state: %w", err) | ||
| } | ||
| return state&fastly.CacheLookupStateStale == fastly.CacheLookupStateStale, nil | ||
| return state.Has(fastly.CacheLookupStateStale), nil | ||
| } | ||
|
|
||
| // Age returns current age in seconds of the cached item, relative to the originating backend. | ||
|
|
@@ -403,7 +418,40 @@ func (candidateResponse *CandidateResponse) StaleWhileRevalidate() (uint32, erro | |
| if err != nil { | ||
| return 0, err | ||
| } | ||
| return opts.stale, nil | ||
| return opts.staleWhileRevalidate, nil | ||
| } | ||
|
|
||
| // SetStaleIfError sets the time in seconds for which a cached item can be delivered stale if synchronous revalidation produces an error. | ||
| func (candidateResponse *CandidateResponse) SetStaleIfError(sie uint32) { | ||
| candidateResponse.overrideStaleIfError = sie | ||
| candidateResponse.useSIE = true | ||
| } | ||
|
|
||
| // StaleIfError returns the time in seconds for which a cached item will be delivered stale if synchronous revalidation produces an error. | ||
| func (candidateResponse *CandidateResponse) StaleIfError() (uint32, error) { | ||
| if candidateResponse.useSIE { | ||
| return candidateResponse.overrideStaleIfError, nil | ||
| } | ||
| opts, err := candidateResponse.getSuggestedCacheWriteOptions() | ||
| if err != nil { | ||
| return 0, err | ||
| } | ||
| return opts.staleIfError, nil | ||
| } | ||
|
|
||
| // Returns whether there is a stale-if-error response available from the cache. | ||
| // | ||
| // A CandidateResponse represents an HTTP response returned from a Backend. However, it may be | ||
| // preferable to return a cached response rather than the Backend's response -- for instance, | ||
| // if the Backend's response is a 5xx error. | ||
| // | ||
| // This method returns true if there is a cached response that is within the stale-if-error | ||
| // period. If a stale-if-error response is available, and the AfterSend hook returns an | ||
| // error, the response from the Backend will not be cached, and the [Request.Send] call will | ||
| // return the stale-if-error response. | ||
| func (candidateResponse *CandidateResponse) StaleIfErrorAvailable() bool { | ||
| state, _ := fastly.HTTPCacheGetState(candidateResponse.cacheHandle) | ||
| return state.Has(fastly.CacheLookupStateUsableIfError) | ||
| } | ||
|
|
||
| // SetSensitive sets the caching behavior of this response to enable or disable PCI/HIPAA-compliant | ||
|
|
@@ -582,9 +630,9 @@ func (candidateResponse *CandidateResponse) finalizeOptions() (fastly.HTTPCacheS | |
| opts.age = suggestedCacheWriteOptions.age | ||
|
|
||
| if candidateResponse.useSWR { | ||
| opts.stale = candidateResponse.overrideStaleWhileRevalidate | ||
| opts.staleWhileRevalidate = candidateResponse.overrideStaleWhileRevalidate | ||
| } else { | ||
| opts.stale = suggestedCacheWriteOptions.stale | ||
| opts.staleWhileRevalidate = suggestedCacheWriteOptions.staleWhileRevalidate | ||
| } | ||
|
|
||
| if candidateResponse.useVary { | ||
|
|
@@ -657,7 +705,7 @@ func (candidateResponse *CandidateResponse) applyAndStreamBack(req *Request) (*R | |
| } | ||
| body.Close() | ||
|
|
||
| resp, err = httpCacheGetFoundResponse(readback, req, "", false) | ||
| resp, err = httpCacheGetFoundResponse(readback, req, "", false, true) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("cache get found response: %w", err) | ||
| } | ||
|
|
@@ -669,7 +717,7 @@ func (candidateResponse *CandidateResponse) applyAndStreamBack(req *Request) (*R | |
| } | ||
| defer fastly.HTTPCacheTransactionClose(newch) | ||
|
|
||
| resp, err = httpCacheGetFoundResponse(newch, req, "", true) | ||
| resp, err = httpCacheGetFoundResponse(newch, req, "", true, true) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("cache get found response: %w", err) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -635,12 +635,13 @@ func (req *Request) sendWithGuestCache(ctx context.Context, backend string) (*Re | |
| fastly.HTTPCacheTransactionClose(cacheHandle) | ||
| } | ||
| }() | ||
| if err := httpCacheWait(cacheHandle); err != nil { | ||
| state, err := httpCacheWait(cacheHandle) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| // is there a "usable" cached response (i.e. fresh or within SWR period) | ||
| resp, err := httpCacheGetFoundResponse(cacheHandle, req, backend, true) | ||
| resp, err := httpCacheGetFoundResponse(cacheHandle, req, backend, true, true) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
@@ -650,7 +651,7 @@ func (req *Request) sendWithGuestCache(ctx context.Context, backend string) (*Re | |
|
|
||
| // if this is during SWR, we may be the "lucky winner" who is | ||
| // tasked with performing a background revalidation | ||
| if ok, _ := httpCacheMustInsertOrUpdate(cacheHandle); ok { | ||
| if state.Has(fastly.CacheLookupStateMustInsertOrUpdate) { | ||
| pending, err := req.sendAsyncForCaching(ctx, cacheHandle, backend) | ||
| if err != nil { | ||
| return nil, err | ||
|
|
@@ -672,23 +673,42 @@ func (req *Request) sendWithGuestCache(ctx context.Context, backend string) (*Re | |
| cacheHandle = nil | ||
| } | ||
|
|
||
| // Meanwhile, whether fresh or in SWR, we can immediately return | ||
| if state.Has(fastly.CacheLookupStateUsableIfError) { | ||
| // This is a stale-if-error response that is also USABLE, implying the request | ||
| // collapse has already happened. | ||
| // Mark the response's masked error as "error in request collapse leader". | ||
| resp.maskedError = ErrRequestCollapse | ||
| } | ||
|
|
||
| // Meanwhile, whether fresh or in SWR/SIE, we can immediately return | ||
| // the cached response: | ||
| resp.updateFastlyCacheHeaders(req) | ||
| return resp, nil | ||
| } | ||
|
|
||
| // no cached response | ||
|
|
||
| if ok, _ := httpCacheMustInsertOrUpdate(cacheHandle); ok { | ||
|
|
||
| if state.Has(fastly.CacheLookupStateMustInsertOrUpdate) { | ||
| pending, err := req.sendAsyncForCaching(ctx, cacheHandle, backend) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| candidateResp, err := newCandidateFromPendingBackendCaching(pending) | ||
| if err != nil { | ||
| if state.Has(fastly.CacheLookupStateUsableIfError) { | ||
| // Substitute stale-if-error response; let anyone else in the collapse know as | ||
| // well. | ||
| fastly.HTTPCacheTransactionChooseStale(cacheHandle) | ||
| resp, foundErr := httpCacheGetFoundResponse(cacheHandle, req, backend, true, false) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A question for my understanding: this is not a hit, since it is serving a stale response?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess that makes sense? I was just translating the Rust code here :/ |
||
| if foundErr != nil { | ||
| return nil, foundErr | ||
| } | ||
| resp.maskedError = err | ||
| resp.updateFastlyCacheHeaders(req) | ||
| return resp, nil | ||
| } | ||
|
|
||
| return nil, err | ||
| } | ||
|
|
||
|
|
@@ -998,6 +1018,12 @@ type CacheOptions struct { | |
| // bypass the cache. | ||
| StaleWhileRevalidate uint32 | ||
|
|
||
| // The maximum duration in seconds after `max_age` during which the response | ||
| // may be delivered stale if synchronous revalidation produces an error. | ||
| // | ||
| // If this field is not set, the default value is zero. | ||
| StaleIfError uint32 | ||
|
|
||
| // SurrogateKey represents an explicit surrogate key for the request, which | ||
| // will be added to any `Surrogate-Key` response headers received from the | ||
| // backend. If nonempty, the request will not be forced to bypass the cache. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.