Skip to content

Commit fa35436

Browse files
author
k1low
committed
Change the interface to be ready for RFC 9111
1 parent 1ba8521 commit fa35436

File tree

14 files changed

+1474
-474
lines changed

14 files changed

+1474
-474
lines changed

benchmark_test.go

-99
This file was deleted.

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module github.com/k1LoW/rc
22

3-
go 1.21.0
3+
go 1.21.5

rc.go

+79-75
Original file line numberDiff line numberDiff line change
@@ -5,111 +5,115 @@ import (
55
"io"
66
"net/http"
77
"net/http/httptest"
8+
"time"
9+
10+
"github.com/k1LoW/rc/rfc9111"
811
)
912

1013
type Cacher interface {
11-
// Name returns the name of the cacher.
12-
Name() string
13-
// Load loads the response cache.
14+
// Load loads the request/response cache.
1415
// If the cache is not found, it returns ErrCacheNotFound.
1516
// If not caching, it returns ErrNoCache.
1617
// If the cache is expired, it returns ErrCacheExpired.
17-
Load(req *http.Request) (res *http.Response, err error)
18+
Load(req *http.Request) (cachedReq *http.Request, cachedRes *http.Response, err error)
1819
// Store stores the response cache.
1920
Store(req *http.Request, res *http.Response) error
2021
}
2122

23+
type Handler interface {
24+
Handle(req *http.Request, cachedReq *http.Request, cachedRes *http.Response, do func(*http.Request) (*http.Response, error), now time.Time) (cacheUsed bool, res *http.Response, err error)
25+
Storable(req *http.Request, res *http.Response, now time.Time) (ok bool, expires time.Time)
26+
}
27+
28+
var _ Handler = new(rfc9111.Shared)
29+
30+
type cacher struct {
31+
Cacher
32+
Handle func(req *http.Request, cachedReq *http.Request, cachedRes *http.Response, do func(*http.Request) (*http.Response, error), now time.Time) (cacheUsed bool, res *http.Response, err error)
33+
Storable func(req *http.Request, res *http.Response, now time.Time) (ok bool, expires time.Time)
34+
}
35+
36+
func newCacher(c Cacher) *cacher {
37+
cc := &cacher{
38+
Cacher: c,
39+
}
40+
if v, ok := c.(Handler); ok {
41+
cc.Handle = v.Handle
42+
cc.Storable = v.Storable
43+
} else {
44+
s, _ := rfc9111.NewShared()
45+
cc.Handle = s.Handle
46+
cc.Storable = s.Storable
47+
}
48+
return cc
49+
}
50+
2251
type cacheMw struct {
23-
cachers []Cacher
52+
cacher *cacher
2453
}
2554

26-
func newCacheMw(cachers []Cacher) *cacheMw {
55+
func newCacheMw(c Cacher) *cacheMw {
56+
cc := newCacher(c)
2757
return &cacheMw{
28-
cachers: cachers,
58+
cacher: cc,
2959
}
3060
}
3161

3262
func (cw *cacheMw) Handler(next http.Handler) http.Handler {
33-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
34-
var (
35-
res *http.Response
36-
body []byte
37-
err error
38-
// When use cache, hit is the name of the cacher.
39-
hit string
40-
)
63+
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
64+
now := time.Now()
65+
cachedReq, cachedRes, _ := cw.cacher.Load(req)
66+
cacheUsed, res, _ := cw.cacher.Handle(req, cachedReq, cachedRes, HandlerToClientDo(next), now)
4167

42-
// Use cache
43-
for _, c := range cw.cachers {
44-
res, err = c.Load(r)
45-
if err != nil {
46-
// TODO: log error
47-
continue
48-
}
49-
// Response cache
50-
for k, v := range res.Header {
51-
set := false
52-
for _, vv := range v {
53-
if !set {
54-
w.Header().Set(k, vv)
55-
set = true
56-
continue
57-
}
58-
w.Header().Add(k, vv)
68+
// Response
69+
for k, v := range res.Header {
70+
set := false
71+
for _, vv := range v {
72+
if !set {
73+
w.Header().Set(k, vv)
74+
set = true
75+
continue
5976
}
77+
w.Header().Add(k, vv)
6078
}
61-
w.WriteHeader(res.StatusCode)
62-
body, err = io.ReadAll(res.Body)
63-
if err == nil {
64-
_ = res.Body.Close()
65-
_, _ = w.Write(body)
66-
}
79+
}
80+
w.WriteHeader(res.StatusCode)
81+
body, err := io.ReadAll(res.Body)
82+
if err == nil {
6783
_ = res.Body.Close()
68-
hit = c.Name()
69-
break
84+
_, _ = w.Write(body)
7085
}
86+
_ = res.Body.Close()
7187

72-
if hit == "" {
73-
// Record response of next handler ( upstream )
74-
rec := httptest.NewRecorder()
75-
next.ServeHTTP(rec, r)
76-
for k, v := range rec.Header() {
77-
set := false
78-
for _, vv := range v {
79-
if !set {
80-
w.Header().Set(k, vv)
81-
set = true
82-
continue
83-
}
84-
w.Header().Add(k, vv)
85-
}
86-
}
87-
w.WriteHeader(rec.Code)
88-
_, _ = w.Write(rec.Body.Bytes())
89-
body = rec.Body.Bytes()
90-
res = &http.Response{
91-
StatusCode: rec.Code,
92-
Header: rec.Header(),
93-
}
88+
if cacheUsed {
89+
return
9490
}
95-
96-
// Store cache
97-
// If a cache is used, store the response in the cachers before that cacher.
98-
for _, c := range cw.cachers {
99-
if c.Name() == hit {
100-
break
101-
}
102-
// Restore body
103-
res.Body = io.NopCloser(bytes.NewReader(body))
104-
// TODO: log error
105-
_ = c.Store(r, res)
91+
ok, _ := cw.cacher.Storable(req, res, now)
92+
if !ok {
93+
return
10694
}
95+
// Restore response body
96+
res.Body = io.NopCloser(bytes.NewReader(body))
97+
98+
// Store response as cache
99+
_ = cw.cacher.Store(req, res)
107100
})
108101
}
109102

110103
// New returns a new response cache middleware.
111104
// The order of the cachers is arranged in order of high-speed cache, such as CPU L1 cache and L2 cache.
112-
func New(cachers ...Cacher) func(next http.Handler) http.Handler {
113-
rl := newCacheMw(cachers)
105+
func New(cacher Cacher) func(next http.Handler) http.Handler {
106+
rl := newCacheMw(cacher)
114107
return rl.Handler
115108
}
109+
110+
// HandlerToClientDo converts http.Handler to func(*http.Request) (*http.Response, error).
111+
func HandlerToClientDo(h http.Handler) func(*http.Request) (*http.Response, error) {
112+
return func(req *http.Request) (*http.Response, error) {
113+
rec := httptest.NewRecorder()
114+
h.ServeHTTP(rec, req)
115+
res := rec.Result()
116+
res.Header = rec.Header()
117+
return res, nil
118+
}
119+
}

rc_test.go

+24-36
Original file line numberDiff line numberDiff line change
@@ -15,58 +15,47 @@ import (
1515
func TestRC(t *testing.T) {
1616
tests := []struct {
1717
name string
18-
cachers []rc.Cacher
18+
cacher rc.Cacher
1919
requests []*http.Request
2020
wantResponses []*http.Response
21-
wantHits []int
21+
wantHit int
2222
}{
23-
{"all cache", []rc.Cacher{testutil.NewAllCache(t)}, []*http.Request{
23+
{"all cache", testutil.NewAllCache(t), []*http.Request{
2424
{Method: http.MethodGet, URL: testutil.MustParseURL("http://example.com/1")},
2525
{Method: http.MethodGet, URL: testutil.MustParseURL("http://example.com/1")},
2626
{Method: http.MethodGet, URL: testutil.MustParseURL("http://example.com/1")},
2727
{Method: http.MethodGet, URL: testutil.MustParseURL("http://example.com/2")},
2828
}, []*http.Response{
29-
{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":1}`)},
30-
{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}, "X-Cache": []string{"HIT"}}, Body: testutil.NewBody(`{"count":1}`)},
31-
{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}, "X-Cache": []string{"HIT"}}, Body: testutil.NewBody(`{"count":1}`)},
32-
{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":2}`)},
33-
}, []int{2}},
34-
{"all cache 2", []rc.Cacher{testutil.NewAllCache(t)}, []*http.Request{
29+
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":1}`)},
30+
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}, "X-Cache": []string{"HIT"}}, Body: testutil.NewBody(`{"count":1}`)},
31+
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}, "X-Cache": []string{"HIT"}}, Body: testutil.NewBody(`{"count":1}`)},
32+
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":2}`)},
33+
}, 2},
34+
{"all cache 2", testutil.NewAllCache(t), []*http.Request{
3535
{Method: http.MethodGet, URL: testutil.MustParseURL("http://example.com/1")},
3636
{Method: http.MethodGet, URL: testutil.MustParseURL("http://example.com/2")},
3737
{Method: http.MethodGet, URL: testutil.MustParseURL("http://example.com/1")},
3838
}, []*http.Response{
39-
{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":1}`)},
40-
{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":2}`)},
41-
{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}, "X-Cache": []string{"HIT"}}, Body: testutil.NewBody(`{"count":1}`)},
42-
}, []int{1}},
43-
{"get only", []rc.Cacher{testutil.NewGetOnlyCache(t)}, []*http.Request{
39+
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":1}`)},
40+
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":2}`)},
41+
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}, "X-Cache": []string{"HIT"}}, Body: testutil.NewBody(`{"count":1}`)},
42+
}, 1},
43+
{"get only", testutil.NewGetOnlyCache(t), []*http.Request{
4444
{Method: http.MethodPost, URL: testutil.MustParseURL("http://example.com/1")},
4545
{Method: http.MethodGet, URL: testutil.MustParseURL("http://example.com/1")},
4646
{Method: http.MethodDelete, URL: testutil.MustParseURL("http://example.com/1")},
4747
{Method: http.MethodGet, URL: testutil.MustParseURL("http://example.com/1")},
4848
}, []*http.Response{
49-
{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":1}`)},
50-
{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":2}`)},
51-
{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":3}`)},
52-
{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}, "X-Cache": []string{"HIT"}}, Body: testutil.NewBody(`{"count":2}`)},
53-
}, []int{1}},
54-
{"multi cache", []rc.Cacher{testutil.NewGetOnlyCache(t), testutil.NewAllCache(t)}, []*http.Request{
55-
{Method: http.MethodPost, URL: testutil.MustParseURL("http://example.com/1")},
56-
{Method: http.MethodGet, URL: testutil.MustParseURL("http://example.com/1")},
57-
{Method: http.MethodPost, URL: testutil.MustParseURL("http://example.com/1")},
58-
{Method: http.MethodGet, URL: testutil.MustParseURL("http://example.com/1")},
59-
}, []*http.Response{
60-
{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":1}`)},
61-
{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":2}`)},
62-
{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}, "X-Cache": []string{"HIT"}}, Body: testutil.NewBody(`{"count":1}`)},
63-
{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}, "X-Cache": []string{"HIT"}}, Body: testutil.NewBody(`{"count":2}`)},
64-
}, []int{1, 1}},
49+
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":1}`)},
50+
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":2}`)},
51+
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":3}`)},
52+
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}, "X-Cache": []string{"HIT"}}, Body: testutil.NewBody(`{"count":2}`)},
53+
}, 1},
6554
}
6655
for _, tt := range tests {
6756
t.Run(tt.name, func(t *testing.T) {
6857
tr := testutil.NewHTTPRouter(t)
69-
m := rc.New(tt.cachers...)
58+
m := rc.New(tt.cacher)
7059
ts := httptest.NewServer(m(tr))
7160
tu := testutil.MustParseURL(ts.URL)
7261
t.Cleanup(ts.Close)
@@ -77,6 +66,7 @@ func TestRC(t *testing.T) {
7766
got, err := tc.Do(req)
7867
if err != nil {
7968
t.Fatal(err)
69+
return
8070
}
8171
opts := []cmp.Option{
8272
cmpopts.IgnoreFields(http.Response{}, "Status", "Proto", "ProtoMajor", "ProtoMinor", "ContentLength", "TransferEncoding", "Uncompressed", "Trailer", "Request", "Close", "Body"),
@@ -110,11 +100,9 @@ func TestRC(t *testing.T) {
110100
}
111101
}
112102

113-
for i, want := range tt.wantHits {
114-
got := tt.cachers[i].(testutil.Cacher).Hit()
115-
if got != want {
116-
t.Errorf("got %v want %v", got, want)
117-
}
103+
got := tt.cacher.(testutil.Cacher).Hit()
104+
if got != tt.wantHit {
105+
t.Errorf("got %v want %v", got, tt.wantHit)
118106
}
119107
})
120108
}

0 commit comments

Comments
 (0)