Skip to content

Commit fa1a6d6

Browse files
committed
update
1 parent a6033f8 commit fa1a6d6

9 files changed

Lines changed: 299 additions & 121 deletions

File tree

TODO.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
TODO
22
====
33

4-
- [ ] Review options.. ie.
5-
- [ ] WithCacheKeyRequestBody
6-
- [ ] what else..?
4+
- [x] Review options.. ie.
5+
- [x] WithCacheKeyRequestBody
6+
- [x] what else..?
77

8-
- [ ] lets add another workspace where we do e2e tests with chi, cachestore-mem, etc.
8+
- [x] lets add another workspace where we do e2e tests with chi, cachestore-mem, etc.
99
this way just go-chi/stampede package can be very lightweight for its go.mod
1010

11-
- [ ] vary header support, which will adjust cache key too, and singleflight key.. etc.
11+
- [x] vary header support, which will adjust cache key too, and singleflight key.. etc.
12+
13+
- [ ] what if there is a panic in the caller fn or http handelr..?
14+
15+
- [ ] developer experience on per-handler setup..?

go.mod

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ go 1.23.0
55
toolchain go1.24.2
66

77
require (
8-
github.com/goware/cachestore-mem v0.2.1
98
github.com/goware/cachestore2 v0.12.2
109
github.com/goware/singleflight v0.3.0
1110
github.com/stretchr/testify v1.10.0
@@ -14,7 +13,6 @@ require (
1413

1514
require (
1615
github.com/davecgh/go-spew v1.1.1 // indirect
17-
github.com/elastic/go-freelru v0.16.0 // indirect
1816
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
1917
github.com/pmezard/go-difflib v1.0.0 // indirect
2018
golang.org/x/sys v0.32.0 // indirect

go.sum

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
22
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3-
github.com/elastic/go-freelru v0.16.0 h1:gG2HJ1WXN2tNl5/p40JS/l59HjvjRhjyAa+oFTRArYs=
4-
github.com/elastic/go-freelru v0.16.0/go.mod h1:bSdWT4M0lW79K8QbX6XY2heQYSCqD7THoYf82pT/H3I=
5-
github.com/goware/cachestore-mem v0.2.1 h1:8ZIFtzpoFlwnPUKuGeazhuV2qzR4Bk7UslEGyXRZp9E=
6-
github.com/goware/cachestore-mem v0.2.1/go.mod h1:0WU95kEa8kmuYSsqOC/fXg/cGVqj5rsTzjUpQgaJHmw=
73
github.com/goware/cachestore2 v0.12.2 h1:04YGXkMwbH1xe82siCO7iaPhetntRABN5fWhBKEzduY=
84
github.com/goware/cachestore2 v0.12.2/go.mod h1:PR+lXK8UXa/wjKB7mpIj6HtRhC7vbcRXx4b5F1Av/ik=
95
github.com/goware/singleflight v0.3.0 h1:b+OM844fuHzanOlE84WeI+G8YMksUY636v0bdcAfnHE=
@@ -18,8 +14,6 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
1814
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
1915
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
2016
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
21-
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
22-
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
2317
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
2418
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
2519
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

http.go

Lines changed: 80 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -12,70 +12,102 @@ import (
1212
cachestore "github.com/goware/cachestore2"
1313
)
1414

15-
// TODO: we want TTL for OK and TTL for error ..
16-
// but really, it could be any TTL depending on the request ..
17-
18-
// kinda need like ... for status set the TTL ..?
19-
20-
func Handler2(logger *slog.Logger, cacheBackend cachestore.Backend, ttl time.Duration, options ...Option) func(next http.Handler) http.Handler {
21-
opts := &Options{
22-
TTL: ttl,
23-
}
24-
for _, o := range options {
25-
o(opts)
26-
}
27-
28-
var cacheKeyFunc func(r *http.Request) (uint64, error)
29-
30-
if !opts.HTTPCacheKeyRequestBody {
31-
cacheKeyFunc = func(r *http.Request) (uint64, error) {
32-
return StringToHash(strings.ToLower(r.URL.Path)), nil
15+
func HandlerWithKey(logger *slog.Logger, cacheBackend cachestore.Backend, ttl time.Duration, cacheKeyFunc CacheKeyFunc, options ...Option) func(next http.Handler) http.Handler {
16+
opts := getOptions(ttl, options...)
17+
18+
// Combine various cache key functions into a single cache key value.
19+
cacheKeyWithRequestHeaders := cacheKeyWithRequestHeaders(opts.HTTPCacheKeyRequestHeaders)
20+
21+
comboCacheKeyFunc := func(r *http.Request) (uint64, error) {
22+
var cacheKey1, cacheKey2, cacheKey3, cacheKey4 uint64
23+
var err error
24+
cacheKey1, err = cacheKeyWithRequestURL(r)
25+
if err != nil {
26+
return 0, err
3327
}
34-
} else {
35-
cacheKeyFunc = func(r *http.Request) (uint64, error) {
36-
// Read the request payload, and then setup buffer for future reader
37-
var err error
38-
var buf []byte
39-
if r.Body != nil {
40-
buf, err = io.ReadAll(r.Body)
41-
if err != nil {
42-
return 0, err
43-
}
44-
r.Body = io.NopCloser(bytes.NewBuffer(buf))
28+
if opts.HTTPCacheKeyRequestBody {
29+
cacheKey2, err = cacheKeyWithRequestBody(r)
30+
if err != nil {
31+
return 0, err
4532
}
46-
47-
// Prepare cache key based on request URL path and the request data payload.
48-
key := BytesToHash([]byte(strings.ToLower(r.URL.Path)), buf)
49-
return key, nil
5033
}
34+
if len(opts.HTTPCacheKeyRequestHeaders) > 0 {
35+
cacheKey3, err = cacheKeyWithRequestHeaders(r)
36+
if err != nil {
37+
return 0, err
38+
}
39+
}
40+
if cacheKeyFunc != nil {
41+
cacheKey4, err = cacheKeyFunc(r)
42+
if err != nil {
43+
return 0, err
44+
}
45+
}
46+
return cacheKey1 + cacheKey2 + cacheKey3 + cacheKey4, nil
5147
}
5248

53-
var cache cachestore.Store[responseValue2]
49+
var cache cachestore.Store[responseValue]
5450
if cacheBackend != nil {
55-
cache = cachestore.OpenStore[responseValue2](cacheBackend)
51+
cache = cachestore.OpenStore[responseValue](cacheBackend)
5652
}
57-
h := stampedeHandler2(logger, cache, cacheKeyFunc, opts)
53+
h := stampedeHandler(logger, cache, comboCacheKeyFunc, opts)
5854

5955
return func(next http.Handler) http.Handler {
60-
61-
// TODO: the "wee" function(request) doesn't make sense, because
62-
// we actually need the response ..
63-
// and also, might want to "vary" on the response headers ....?
64-
6556
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
6657
h(next).ServeHTTP(w, r)
6758
})
6859
}
6960
}
7061

62+
func Handler(logger *slog.Logger, cacheBackend cachestore.Backend, ttl time.Duration, options ...Option) func(next http.Handler) http.Handler {
63+
return HandlerWithKey(logger, cacheBackend, ttl, nil, options...)
64+
}
65+
66+
func cacheKeyWithRequestURL(r *http.Request) (uint64, error) {
67+
return StringToHash(strings.ToLower(r.URL.Path)), nil
68+
}
69+
70+
func cacheKeyWithRequestBody(r *http.Request) (uint64, error) {
71+
// Read the request payload, and then setup buffer for future reader
72+
var err error
73+
var buf []byte
74+
if r.Body != nil {
75+
buf, err = io.ReadAll(r.Body)
76+
if err != nil {
77+
return 0, err
78+
}
79+
r.Body = io.NopCloser(bytes.NewBuffer(buf))
80+
}
81+
82+
// Prepare cache key based on the request data payload.
83+
return BytesToHash(buf), nil
84+
}
85+
86+
func cacheKeyWithRequestHeaders(headers []string) func(r *http.Request) (uint64, error) {
87+
return func(r *http.Request) (uint64, error) {
88+
if len(headers) == 0 {
89+
return 0, nil
90+
}
91+
var keys []string
92+
for _, header := range headers {
93+
v := r.Header.Get(header)
94+
if v == "" {
95+
continue
96+
}
97+
keys = append(keys, fmt.Sprintf("%s:%s", strings.ToLower(header), v))
98+
}
99+
return StringToHash(keys...), nil
100+
}
101+
}
102+
71103
type CacheKeyFunc func(r *http.Request) (uint64, error)
72104

73-
func stampedeHandler2(logger *slog.Logger, cache cachestore.Store[responseValue2], cacheKeyFunc CacheKeyFunc, options *Options) func(next http.Handler) http.Handler {
105+
func stampedeHandler(logger *slog.Logger, cache cachestore.Store[responseValue], cacheKeyFunc CacheKeyFunc, options *Options) func(next http.Handler) http.Handler {
74106
stampede := NewStampede(cache)
107+
stampede.SetOptions(options)
75108

76109
return func(next http.Handler) http.Handler {
77110
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
78-
79111
cacheKey, err := cacheKeyFunc(r)
80112
if err != nil {
81113
logger.Warn("stampede: fail to compute cache key", "err", err)
@@ -85,23 +117,14 @@ func stampedeHandler2(logger *slog.Logger, cache cachestore.Store[responseValue2
85117

86118
firstRequest := false
87119

88-
k := fmt.Sprintf("%d", cacheKey) // TODO ...
89-
90-
ttl := options.TTL
91-
_ = ttl
92-
93-
// TODO: pass down the greater TTL to the .Do()..
94-
95-
// TODO: .. THEN .. we can check cachedVal.TS and we'll
96-
97-
cachedVal, err := stampede.Do(k, func() (responseValue2, error) {
120+
cachedVal, err := stampede.Do(fmt.Sprintf("%d", cacheKey), func() (responseValue, error) {
98121
firstRequest = true
99122
buf := bytes.NewBuffer(nil)
100123
ww := &responseWriter{ResponseWriter: w, tee: buf}
101124

102125
next.ServeHTTP(ww, r)
103126

104-
val := responseValue2{
127+
val := responseValue{
105128
Headers: ww.Header(),
106129
Status: ww.Status(),
107130
Body: buf.Bytes(),
@@ -111,9 +134,7 @@ func stampedeHandler2(logger *slog.Logger, cache cachestore.Store[responseValue2
111134
Skip: !ww.IsValid(),
112135
}
113136
return val, nil
114-
}) //, options) // TODO .. we need this..
115-
_ = cacheKey
116-
_ = firstRequest
137+
})
117138

118139
if firstRequest {
119140
fmt.Println("first request")
@@ -149,15 +170,15 @@ func stampedeHandler2(logger *slog.Logger, cache cachestore.Store[responseValue2
149170
}
150171
respHeader[k] = v
151172
}
152-
respHeader.Set("x-cache", "hit") // TODO: confirm works..
173+
respHeader.Set("x-cache", "hit") // TODO: confirm works....
153174

154175
w.WriteHeader(cachedVal.Status)
155176
w.Write(cachedVal.Body)
156177
})
157178
}
158179
}
159180

160-
type responseValue2 struct {
181+
type responseValue struct {
161182
Headers http.Header `json:"headers"`
162183
Status int `json:"status"`
163184
Body []byte `json:"body"`

http_test.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,10 @@ import (
1010
"time"
1111

1212
"github.com/go-chi/stampede"
13-
memcache "github.com/goware/cachestore-mem"
14-
cachestore "github.com/goware/cachestore2"
1513
"github.com/stretchr/testify/require"
1614
)
1715

18-
func Test2SingleflightHTTPHandler(t *testing.T) {
16+
func TestSingleflightHTTPHandler(t *testing.T) {
1917
// Create a counter to track how many times handlers are called
2018
var callCount int
2119
var mu sync.Mutex
@@ -85,7 +83,7 @@ func Test2SingleflightHTTPHandler(t *testing.T) {
8583
require.Equal(t, 1, callCount)
8684
}
8785

88-
func Test2HTTPCachingHandler(t *testing.T) {
86+
func TestHTTPCachingHandler(t *testing.T) {
8987
// Create a counter to track how many times handlers are called
9088
var callCount int
9189
var mu sync.Mutex
@@ -107,10 +105,10 @@ func Test2HTTPCachingHandler(t *testing.T) {
107105
})
108106

109107
// Apply Handler2 to the slow handler only
110-
cache, err := memcache.NewBackend(1000, cachestore.WithDefaultKeyExpiry(10*time.Second))
111-
require.NoError(t, err)
108+
// cache, _ := memcache.NewBackend(1000, cachestore.WithDefaultKeyExpiry(10*time.Second))
109+
cache := newMockCacheBackend()
112110

113-
wrappedSlowHandler := stampede.Handler2(slog.Default(), cache, 5*time.Second,
111+
wrappedSlowHandler := stampede.Handler(slog.Default(), cache, 5*time.Second,
114112
stampede.WithHTTPStatusTTL(func(status int) time.Duration {
115113
switch {
116114
case status >= 200 && status < 300:

0 commit comments

Comments
 (0)