Skip to content

Commit 603a90b

Browse files
committed
fsthttp: add stale-if-error logic
1 parent 0b8cce4 commit 603a90b

File tree

5 files changed

+67
-27
lines changed

5 files changed

+67
-27
lines changed

fsthttp/cache.go

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -159,24 +159,15 @@ const (
159159
cacheStorageActionInvalid = 0xffff
160160
)
161161

162-
func httpCacheWait(c *fastly.HTTPCacheHandle) error {
163-
_, err := fastly.HTTPCacheGetState(c)
164-
if err != nil {
165-
return fmt.Errorf("get state: %w", err)
166-
}
167-
return nil
168-
}
169-
170-
func httpCacheMustInsertOrUpdate(c *fastly.HTTPCacheHandle) (bool, error) {
162+
func httpCacheWait(c *fastly.HTTPCacheHandle) (fastly.CacheLookupState, error) {
171163
state, err := fastly.HTTPCacheGetState(c)
172164
if err != nil {
173-
return false, fmt.Errorf("get state: %w", err)
174-
165+
return 0, fmt.Errorf("get state: %w", err)
175166
}
176-
return state&fastly.CacheLookupStateMustInsertOrUpdate == fastly.CacheLookupStateMustInsertOrUpdate, nil
167+
return state, nil
177168
}
178169

179-
func httpCacheGetFoundResponse(c *fastly.HTTPCacheHandle, req *Request, backend string, transformForClient bool) (*Response, error) {
170+
func httpCacheGetFoundResponse(c *fastly.HTTPCacheHandle, req *Request, backend string, transformForClient bool, wasHit bool) (*Response, error) {
180171
abiResp, abiBody, err := fastly.HTTPCacheGetFoundResponse(c, transformForClient)
181172
if err != nil {
182173
if status, ok := fastly.IsFastlyError(err); ok && status == fastly.FastlyStatusNone {
@@ -185,9 +176,13 @@ func httpCacheGetFoundResponse(c *fastly.HTTPCacheHandle, req *Request, backend
185176
return nil, fmt.Errorf("get found response: %w", err)
186177
}
187178

188-
hits, err := fastly.HTTPCacheGetHits(c)
189-
if err != nil {
190-
return nil, fmt.Errorf("get hits: %w", err)
179+
var hits uint64
180+
if wasHit {
181+
h, err := fastly.HTTPCacheGetHits(c)
182+
if err != nil {
183+
return nil, fmt.Errorf("get hits: %w", err)
184+
}
185+
hits = uint64(h)
191186
}
192187

193188
var opts cacheWriteOptions
@@ -204,7 +199,7 @@ func httpCacheGetFoundResponse(c *fastly.HTTPCacheHandle, req *Request, backend
204199
resp.cacheResponse = cacheResponse{
205200
cacheWriteOptions: opts,
206201
storageAction: cacheStorageActionInvalid,
207-
hits: uint64(hits),
202+
hits: hits,
208203
}
209204
return resp, nil
210205
}
@@ -710,7 +705,7 @@ func (candidateResponse *CandidateResponse) applyAndStreamBack(req *Request) (*R
710705
}
711706
body.Close()
712707

713-
resp, err = httpCacheGetFoundResponse(readback, req, "", false)
708+
resp, err = httpCacheGetFoundResponse(readback, req, "", false, true)
714709
if err != nil {
715710
return nil, fmt.Errorf("cache get found response: %w", err)
716711
}
@@ -722,7 +717,7 @@ func (candidateResponse *CandidateResponse) applyAndStreamBack(req *Request) (*R
722717
}
723718
defer fastly.HTTPCacheTransactionClose(newch)
724719

725-
resp, err = httpCacheGetFoundResponse(newch, req, "", true)
720+
resp, err = httpCacheGetFoundResponse(newch, req, "", true, true)
726721
if err != nil {
727722
return nil, fmt.Errorf("cache get found response: %w", err)
728723
}

fsthttp/request.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -629,12 +629,13 @@ func (req *Request) sendWithGuestCache(ctx context.Context, backend string) (*Re
629629
fastly.HTTPCacheTransactionClose(cacheHandle)
630630
}
631631
}()
632-
if err := httpCacheWait(cacheHandle); err != nil {
632+
state, err := httpCacheWait(cacheHandle)
633+
if err != nil {
633634
return nil, err
634635
}
635636

636637
// is there a "usable" cached response (i.e. fresh or within SWR period)
637-
resp, err := httpCacheGetFoundResponse(cacheHandle, req, backend, true)
638+
resp, err := httpCacheGetFoundResponse(cacheHandle, req, backend, true, true)
638639
if err != nil {
639640
return nil, err
640641
}
@@ -644,7 +645,7 @@ func (req *Request) sendWithGuestCache(ctx context.Context, backend string) (*Re
644645

645646
// if this is during SWR, we may be the "lucky winner" who is
646647
// tasked with performing a background revalidation
647-
if ok, _ := httpCacheMustInsertOrUpdate(cacheHandle); ok {
648+
if state.Has(fastly.CacheLookupStateMustInsertOrUpdate) {
648649
pending, err := req.sendAsyncForCaching(ctx, cacheHandle, backend)
649650
if err != nil {
650651
return nil, err
@@ -666,23 +667,42 @@ func (req *Request) sendWithGuestCache(ctx context.Context, backend string) (*Re
666667
cacheHandle = nil
667668
}
668669

669-
// Meanwhile, whether fresh or in SWR, we can immediately return
670+
if state.Has(fastly.CacheLookupStateUsableIfError) {
671+
// This is a stale-if-error response that is also USABLE, implying the request
672+
// collapse has already happened.
673+
// Mark the response's masked error as "error in request collapse leader".
674+
resp.maskedError = ErrRequestCollapse
675+
}
676+
677+
// Meanwhile, whether fresh or in SWR/SIE, we can immediately return
670678
// the cached response:
671679
resp.updateFastlyCacheHeaders(req)
672680
return resp, nil
673681
}
674682

675683
// no cached response
676684

677-
if ok, _ := httpCacheMustInsertOrUpdate(cacheHandle); ok {
678-
685+
if state.Has(fastly.CacheLookupStateMustInsertOrUpdate) {
679686
pending, err := req.sendAsyncForCaching(ctx, cacheHandle, backend)
680687
if err != nil {
681688
return nil, err
682689
}
683690

684691
candidateResp, err := newCandidateFromPendingBackendCaching(pending)
685692
if err != nil {
693+
if state.Has(fastly.CacheLookupStateUsableIfError) {
694+
// Substitute stale-if-error response; let anyone else in the collapse know as
695+
// well.
696+
fastly.HTTPCacheTransactionChooseStale(cacheHandle)
697+
resp, foundErr := httpCacheGetFoundResponse(cacheHandle, req, backend, true, false)
698+
if foundErr != nil {
699+
return nil, foundErr
700+
}
701+
resp.maskedError = err
702+
resp.updateFastlyCacheHeaders(req)
703+
return resp, nil
704+
}
705+
686706
return nil, err
687707
}
688708

fsthttp/response.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ type Response struct {
3333
// Body of the response.
3434
Body io.ReadCloser
3535

36+
// If this response was served from the cache *and* the response was stale-if-error,
37+
// this is the error from the revalidation attempt.
38+
maskedError error
39+
3640
cacheResponse cacheResponse
3741

3842
abi struct {
@@ -71,6 +75,12 @@ func (resp *Response) RemoteAddr() (net.Addr, error) {
7175
return &addr, nil
7276
}
7377

78+
// If this response was served from the cache *and* the response was stale-if-error,
79+
// this is the error from the revalidation attempt.
80+
func (r *Response) MaskedError() error {
81+
return r.maskedError
82+
}
83+
7484
type netaddr struct {
7585
ip net.IP
7686
port uint16

fsthttp/senderror.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
package fsthttp
44

5-
import "github.com/fastly/compute-sdk-go/internal/abi/fastly"
5+
import (
6+
"errors"
7+
8+
"github.com/fastly/compute-sdk-go/internal/abi/fastly"
9+
)
610

711
// SendError provides detailed information about backend request failures.
812
//
@@ -131,3 +135,7 @@ const (
131135
// error.
132136
SendErrorInternalError = fastly.SendErrorDetailTagInternalError
133137
)
138+
139+
var (
140+
ErrRequestCollapse = errors.New("error during request collapse")
141+
)

internal/abi/fastly/types.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,10 @@ const (
996996
CacheLookupStateCollapseError CacheLookupState = 0b0010_0000 // $collapse_error
997997
)
998998

999+
func (c CacheLookupState) Has(m CacheLookupState) bool {
1000+
return (c & m) == m
1001+
}
1002+
9991003
// witx:
10001004
//
10011005
// (typename $purge_options_mask
@@ -1383,6 +1387,7 @@ const (
13831387
SendErrorDetailTagInternalError SendErrorDetailTag = 22
13841388
SendErrorDetailTagTLSAlertReceived SendErrorDetailTag = 23
13851389
SendErrorDetailTagTLSProtocolError SendErrorDetailTag = 24
1390+
SendErrorDetailTagH2Error SendErrorDetailTag = 25
13861391
)
13871392

13881393
// witx:
@@ -1406,6 +1411,7 @@ const (
14061411
sendErrorDetailMaskDNSErrorRCode = 1 << 1 // $dns_error_rcode
14071412
sendErrorDetailMaskDNSErrorInfo = 1 << 2 // $dns_error_info_code
14081413
sendErrorDetailMaskTLSAlertID = 1 << 3 // $tls_alert_id
1414+
sendErrorDetailMaskH2Error = 1 << 4 // $h2_error
14091415
)
14101416

14111417
// witx:
@@ -1544,7 +1550,8 @@ func (d SendErrorDetail) String() string {
15441550
return fmt.Sprintf("TLS alert received (%s)", tlsAlertString(d.tlsAlertID))
15451551
case SendErrorDetailTagTLSProtocolError:
15461552
return "TLS protocol error"
1547-
1553+
case SendErrorDetailTagH2Error:
1554+
return "HTTP/2 error"
15481555
case SendErrorDetailTagUninitialized:
15491556
panic("should not be reached: SendErrorDetailTagUninitialized")
15501557
case SendErrorDetailTagOK:

0 commit comments

Comments
 (0)