Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 82 additions & 34 deletions fsthttp/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ type CandidateResponse struct {
overrideStaleWhileRevalidate uint32 // seconds
useSWR bool

overrideStaleIfError uint32 // seconds
useSIE bool

extraSurrogateKeys string
overrideSurrogateKeys string
useSurrogate bool
Expand All @@ -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
staleRevalidate uint32 // seconds
surrogate string
length uint64
useLength bool
sensitive bool
staleError uint32

abiOpts fastly.HTTPCacheWriteOptions
}
Expand All @@ -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.staleRevalidate))
opts.abiOpts.SetSurrogateKeys(opts.surrogate)
opts.abiOpts.SetSensitiveData(opts.sensitive)
opts.abiOpts.SetStaleIfErrorNs(u32sTou64ns(opts.staleError))
}

func (opts *cacheWriteOptions) loadFromABI() {
Expand All @@ -81,11 +86,15 @@ func (opts *cacheWriteOptions) loadFromABI() {
opts.age = u64nsTou32s(ns)
}
if ns, ok := opts.abiOpts.StaleWhileRevalidateNs(); ok {
opts.stale = u64nsTou32s(ns)
opts.staleRevalidate = 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.staleError = u64nsTou32s(ns)
}
}

func (opts *cacheWriteOptions) loadFromHandle(c *fastly.HTTPCacheHandle) error {
Expand All @@ -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.staleRevalidate = u64nsTou32s(uint64(ns))
}

opts.surrogate, err = fastly.HTTPCacheGetSurrogateKeys(c)
Expand All @@ -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.staleError = u64nsTou32s(uint64(ns))
}

return nil
}

Expand All @@ -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)
}
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 {
Expand All @@ -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 {
h, err := fastly.HTTPCacheGetHits(c)
if err != nil {
return nil, fmt.Errorf("get hits: %w", err)
}
hits = uint64(h)
}

var opts cacheWriteOptions
Expand All @@ -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
}
Expand Down Expand Up @@ -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,
Expand All @@ -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
}
Expand Down Expand Up @@ -403,7 +418,40 @@ func (candidateResponse *CandidateResponse) StaleWhileRevalidate() (uint32, erro
if err != nil {
return 0, err
}
return opts.stale, nil
return opts.staleRevalidate, nil
}

// SetStaleWhileRevalidate 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 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.staleError, 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 after_send 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&fastly.CacheLookupStateUsableIfError == fastly.CacheLookupStateUsableIfError
}

// SetSensitive sets the caching behavior of this response to enable or disable PCI/HIPAA-compliant
Expand Down Expand Up @@ -582,9 +630,9 @@ func (candidateResponse *CandidateResponse) finalizeOptions() (fastly.HTTPCacheS
opts.age = suggestedCacheWriteOptions.age

if candidateResponse.useSWR {
opts.stale = candidateResponse.overrideStaleWhileRevalidate
opts.staleRevalidate = candidateResponse.overrideStaleWhileRevalidate
} else {
opts.stale = suggestedCacheWriteOptions.stale
opts.staleRevalidate = suggestedCacheWriteOptions.staleRevalidate
}

if candidateResponse.useVary {
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down
38 changes: 32 additions & 6 deletions fsthttp/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand All @@ -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)
if foundErr != nil {
return nil, foundErr
}
resp.maskedError = err
resp.updateFastlyCacheHeaders(req)
return resp, nil
}

return nil, err
}

Expand Down Expand Up @@ -998,6 +1018,12 @@ type CacheOptions struct {
// bypass the cache.
StaleWhileRevalidate uint32

// The maximum duration 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.
Expand Down
21 changes: 20 additions & 1 deletion fsthttp/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ type Response struct {

trailers Header

// If this response was served from the cache *and* the response was stale-if-error,
// this is the error from the revalidation attempt.
maskedError error

cacheResponse cacheResponse

abi struct {
Expand Down Expand Up @@ -113,6 +117,12 @@ func (resp *Response) Trailers() (Header, error) {
return resp.trailers, nil
}

// If this response was served from the cache *and* the response was stale-if-error,
// this is the error from the revalidation attempt.
func (r *Response) MaskedError() error {
return r.maskedError
}

type netaddr struct {
ip net.IP
port uint16
Expand Down Expand Up @@ -236,7 +246,16 @@ func (resp *Response) StaleWhileRevalidate() (uint32, bool) {
return 0, false
}

return resp.cacheResponse.cacheWriteOptions.stale, true
return resp.cacheResponse.cacheWriteOptions.staleRevalidate, true
}

// StaleIfError returns the time in seconds for which a cached item delivered stale if synchronous revalidation produces an error.
func (resp *Response) StaleIfError() (uint32, bool) {
if resp.wasWrittenToCache() {
return 0, false
}

return resp.cacheResponse.cacheWriteOptions.staleError, true
}

// Vary returns the set of request headers for which the response may vary.
Expand Down
10 changes: 9 additions & 1 deletion fsthttp/senderror.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

package fsthttp

import "github.com/fastly/compute-sdk-go/internal/abi/fastly"
import (
"errors"

"github.com/fastly/compute-sdk-go/internal/abi/fastly"
)

// SendError provides detailed information about backend request failures.
//
Expand Down Expand Up @@ -131,3 +135,7 @@ const (
// error.
SendErrorInternalError = fastly.SendErrorDetailTagInternalError
)

var (
ErrRequestCollapse = errors.New("error during request collapse")
)
Loading
Loading