Skip to content

Commit a834d90

Browse files
committed
feature: retry() filter
feature: net.Client.Retry() Signed-off-by: Sandor Szücs <[email protected]>
1 parent fd81a7a commit a834d90

File tree

9 files changed

+473
-50
lines changed

9 files changed

+473
-50
lines changed

filters/builtin/builtin.go

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/zalando/skipper/filters/fadein"
1616
"github.com/zalando/skipper/filters/flowid"
1717
logfilter "github.com/zalando/skipper/filters/log"
18+
"github.com/zalando/skipper/filters/retry"
1819
"github.com/zalando/skipper/filters/rfc"
1920
"github.com/zalando/skipper/filters/scheduler"
2021
"github.com/zalando/skipper/filters/sed"
@@ -229,6 +230,7 @@ func Filters() []filters.Spec {
229230
fadein.NewEndpointCreated(),
230231
consistenthash.NewConsistentHashKey(),
231232
consistenthash.NewConsistentHashBalanceFactor(),
233+
retry.NewRetry(),
232234
tls.New(),
233235
}
234236
}

filters/filters.go

+1
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ const (
333333
FifoWithBodyName = "fifoWithBody"
334334
LifoName = "lifo"
335335
LifoGroupName = "lifoGroup"
336+
RetryName = "retry"
336337
RfcPathName = "rfcPath"
337338
RfcHostName = "rfcHost"
338339
BearerInjectorName = "bearerinjector"

filters/retry/retry.go

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package retry
2+
3+
import (
4+
"github.com/zalando/skipper/filters"
5+
)
6+
7+
type retry struct{}
8+
9+
// NewRetry creates a filter specification for the retry() filter
10+
func NewRetry() filters.Spec { return retry{} }
11+
12+
func (retry) Name() string { return filters.RetryName }
13+
func (retry) CreateFilter([]interface{}) (filters.Filter, error) { return retry{}, nil }
14+
func (retry) Response(filters.FilterContext) {}
15+
16+
func (retry) Request(ctx filters.FilterContext) {
17+
ctx.StateBag()[filters.RetryName] = struct{}{}
18+
}

filters/retry/retry_test.go

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package retry
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/AlexanderYastrebov/noleak"
11+
"github.com/zalando/skipper/eskip"
12+
"github.com/zalando/skipper/filters"
13+
"github.com/zalando/skipper/proxy/proxytest"
14+
)
15+
16+
func TestRetry(t *testing.T) {
17+
for _, tt := range []struct {
18+
name string
19+
method string
20+
body string
21+
}{
22+
{
23+
name: "test GET",
24+
method: "GET",
25+
},
26+
{
27+
name: "test POST",
28+
method: "POST",
29+
body: "hello POST",
30+
},
31+
{
32+
name: "test PATCH",
33+
method: "PATCH",
34+
body: "hello PATCH",
35+
},
36+
{
37+
name: "test PUT",
38+
method: "PUT",
39+
body: "hello PUT",
40+
}} {
41+
t.Run(tt.name, func(t *testing.T) {
42+
i := 0
43+
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
44+
if i == 0 {
45+
i++
46+
w.WriteHeader(http.StatusBadGateway)
47+
return
48+
}
49+
50+
got, err := io.ReadAll(r.Body)
51+
if err != nil {
52+
t.Fatalf("got no data")
53+
}
54+
s := string(got)
55+
if tt.body != s {
56+
t.Fatalf("Failed to get the right data want: %q, got: %q", tt.body, s)
57+
}
58+
59+
w.WriteHeader(http.StatusOK)
60+
}))
61+
defer backend.Close()
62+
63+
noleak.Check(t)
64+
65+
fr := make(filters.Registry)
66+
retry := NewRetry()
67+
fr.Register(retry)
68+
r := &eskip.Route{
69+
Filters: []*eskip.Filter{
70+
{Name: retry.Name()},
71+
},
72+
Backend: backend.URL,
73+
}
74+
75+
proxy := proxytest.New(fr, r)
76+
defer proxy.Close()
77+
78+
buf := bytes.NewBufferString(tt.body)
79+
req, err := http.NewRequest(tt.method, proxy.URL, buf)
80+
if err != nil {
81+
t.Fatal(err)
82+
}
83+
84+
rsp, err := http.DefaultClient.Do(req)
85+
if err != nil {
86+
t.Fatalf("Failed to execute retry: %v", err)
87+
}
88+
89+
if rsp.StatusCode != http.StatusOK {
90+
t.Fatalf("unexpected status code: %s", rsp.Status)
91+
}
92+
rsp.Body.Close()
93+
})
94+
}
95+
}

io/copy_stream.go

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package io
2+
3+
import (
4+
"bytes"
5+
"io"
6+
)
7+
8+
type bodyBuffer struct{ *bytes.Buffer }
9+
10+
func (buf *bodyBuffer) Close() error {
11+
return nil
12+
}
13+
14+
type CopyBodyStream struct {
15+
left int
16+
buf *bodyBuffer
17+
input io.ReadCloser
18+
}
19+
20+
func NewCopyBodyStream(left int, buf *bytes.Buffer, rc io.ReadCloser) *CopyBodyStream {
21+
return &CopyBodyStream{
22+
left: left,
23+
buf: &bodyBuffer{Buffer: buf},
24+
input: rc,
25+
}
26+
}
27+
28+
func (cb *CopyBodyStream) Len() int {
29+
return cb.buf.Len()
30+
}
31+
32+
func (cb *CopyBodyStream) Read(p []byte) (n int, err error) {
33+
n, err = cb.input.Read(p)
34+
if cb.left > 0 && n > 0 {
35+
m := min(n, cb.left)
36+
cb.buf.Write(p[:m])
37+
cb.left -= m
38+
}
39+
return n, err
40+
}
41+
42+
func (cb *CopyBodyStream) Close() error {
43+
return cb.input.Close()
44+
}
45+
46+
func (cb *CopyBodyStream) GetBody() io.ReadCloser {
47+
return cb.buf
48+
}

io/copy_stream_test.go

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"testing"
7+
)
8+
9+
type tbuf struct{ *bytes.Buffer }
10+
11+
func (tb *tbuf) Read(p []byte) (int, error) {
12+
return tb.Buffer.Read(p)
13+
}
14+
func (tb *tbuf) Close() error {
15+
return nil
16+
}
17+
18+
func TestCopyBodyStream(t *testing.T) {
19+
s := "content"
20+
bbuf := &tbuf{bytes.NewBufferString(s)}
21+
cbs := NewCopyBodyStream(bbuf.Len(), &bytes.Buffer{}, bbuf)
22+
23+
buf := make([]byte, len(s))
24+
cbs.Read(buf)
25+
26+
if cbs.Len() != len(buf) {
27+
t.Fatalf("Failed to have the same buf buffer size want: %d, got: %d", cbs.Len(), len(buf))
28+
}
29+
30+
got, err := io.ReadAll(cbs.GetBody())
31+
if err != nil {
32+
t.Fatalf("Failed to read: %v", err)
33+
}
34+
if gotStr := string(got); s != gotStr {
35+
t.Fatalf("Failed to get the right content: %s != %s", s, gotStr)
36+
}
37+
38+
if err = cbs.Close(); err != nil {
39+
t.Fatal(err)
40+
}
41+
}

net/httpclient.go

+14-48
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package net
33
import (
44
"bytes"
55
"crypto/tls"
6+
"errors"
67
"fmt"
78
"io"
89
"net/http"
@@ -15,6 +16,7 @@ import (
1516

1617
"github.com/opentracing/opentracing-go"
1718
"github.com/opentracing/opentracing-go/ext"
19+
skpio "github.com/zalando/skipper/io"
1820
"github.com/zalando/skipper/logging"
1921
"github.com/zalando/skipper/secrets"
2022
)
@@ -24,43 +26,7 @@ const (
2426
defaultRefreshInterval = 5 * time.Minute
2527
)
2628

27-
type mybuf struct{ *bytes.Buffer }
28-
29-
func (buf *mybuf) Close() error {
30-
return nil
31-
}
32-
33-
type copyBodyStream struct {
34-
left int
35-
buf *mybuf
36-
input io.ReadCloser
37-
}
38-
39-
func newCopyBodyStream(left int, buf *bytes.Buffer, rc io.ReadCloser) *copyBodyStream {
40-
return &copyBodyStream{
41-
left: left,
42-
buf: &mybuf{Buffer: buf},
43-
input: rc,
44-
}
45-
}
46-
47-
func (cb *copyBodyStream) Read(p []byte) (n int, err error) {
48-
n, err = cb.input.Read(p)
49-
if cb.left > 0 && n > 0 {
50-
m := min(n, cb.left)
51-
cb.buf.Write(p[:m])
52-
cb.left -= m
53-
}
54-
return n, err
55-
}
56-
57-
func (cb *copyBodyStream) Close() error {
58-
return cb.input.Close()
59-
}
60-
61-
func (cb *copyBodyStream) GetBody() io.ReadCloser {
62-
return cb.buf
63-
}
29+
var errRequestNotFound = errors.New("request not found")
6430

6531
// Client adds additional features like Bearer token injection, and
6632
// opentracing to the wrapped http.Client with the same interface as
@@ -166,8 +132,8 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
166132
req.Header.Set("Authorization", "Bearer "+string(b))
167133
}
168134
}
169-
if req.Body != nil && req.Body != http.NoBody {
170-
retryBuffer := newCopyBodyStream(int(req.ContentLength), &bytes.Buffer{}, req.Body)
135+
if req.Body != nil && req.Body != http.NoBody && req.ContentLength > 0 {
136+
retryBuffer := skpio.NewCopyBodyStream(int(req.ContentLength), &bytes.Buffer{}, req.Body)
171137
c.retryBuffers.Store(req, retryBuffer)
172138
req.Body = retryBuffer
173139
}
@@ -179,20 +145,20 @@ func (c *Client) Retry(req *http.Request) (*http.Response, error) {
179145
return c.Do(req)
180146
}
181147

182-
if rc, err := req.GetBody(); err == nil {
183-
println("req.GetBody() case")
184-
c.retryBuffers.Delete(req)
185-
req.Body = rc
186-
return c.Do(req)
187-
}
148+
// Next line panics on TestClientRetryBodyHalfReader
149+
// if rc, err := req.GetBody(); err == nil {
150+
// c.retryBuffers.Delete(req)
151+
// req.Body = rc
152+
// return c.Do(req)
153+
// }
154+
// return nil, fmt.Errorf("failed to retry")
188155

189-
println("our own retry buffer impl")
190156
buf, ok := c.retryBuffers.Load(req)
191157
if !ok {
192-
return nil, fmt.Errorf("no retry possible, request not found: %s %s", req.Method, req.URL)
158+
return nil, fmt.Errorf("no retry possible, %w: %s %s", errRequestNotFound, req.Method, req.URL)
193159
}
194160

195-
retryBuffer, ok := buf.(*copyBodyStream)
161+
retryBuffer, ok := buf.(*skpio.CopyBodyStream)
196162
if !ok {
197163
return nil, fmt.Errorf("no retry possible, no retry buffer for request: %s %s", req.Method, req.URL)
198164
}

0 commit comments

Comments
 (0)