forked from zalando/skipper
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Adds `backendTimeout` filter to configure route backend timeout * Proxy sets up request context with configured timeout and responds with 504 status on timeout (note: if response streaming has already started it will be terminated, client will receive backend status and truncated response body). See zalando#1041 Signed-off-by: Alexander Yastrebov <[email protected]>
- Loading branch information
1 parent
8f32650
commit d3af5cd
Showing
8 changed files
with
332 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package builtin | ||
|
||
import ( | ||
"time" | ||
|
||
"github.com/zalando/skipper/filters" | ||
) | ||
|
||
type timeout struct { | ||
timeout time.Duration | ||
} | ||
|
||
func NewBackendTimeout() filters.Spec { | ||
return &timeout{} | ||
} | ||
|
||
func (*timeout) Name() string { return BackendTimeoutName } | ||
|
||
func (*timeout) CreateFilter(args []interface{}) (filters.Filter, error) { | ||
if len(args) != 1 { | ||
return nil, filters.ErrInvalidFilterParameters | ||
} | ||
|
||
var tf timeout | ||
switch v := args[0].(type) { | ||
case string: | ||
d, err := time.ParseDuration(v) | ||
if err != nil { | ||
return nil, err | ||
} | ||
tf.timeout = d | ||
case time.Duration: | ||
tf.timeout = v | ||
default: | ||
return nil, filters.ErrInvalidFilterParameters | ||
} | ||
return &tf, nil | ||
} | ||
|
||
func (t *timeout) Request(ctx filters.FilterContext) { | ||
sb := ctx.StateBag() | ||
if _, ok := sb[filters.BackendTimeout]; !ok { // once | ||
sb[filters.BackendTimeout] = t.timeout | ||
} | ||
} | ||
|
||
func (t *timeout) Response(filters.FilterContext) {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package builtin | ||
|
||
import ( | ||
"net/http" | ||
"testing" | ||
"time" | ||
|
||
"github.com/zalando/skipper/filters" | ||
"github.com/zalando/skipper/filters/filtertest" | ||
) | ||
|
||
func TestBackendTimeout(t *testing.T) { | ||
bt := NewBackendTimeout() | ||
if bt.Name() != BackendTimeoutName { | ||
t.Error("wrong name") | ||
} | ||
|
||
f, err := bt.CreateFilter([]interface{}{"2s"}) | ||
if err != nil { | ||
t.Error("wrong id") | ||
} | ||
|
||
c := &filtertest.Context{FRequest: &http.Request{}, FStateBag: make(map[string]interface{})} | ||
f.Request(c) | ||
|
||
if c.FStateBag[filters.BackendTimeout] != 2*time.Second { | ||
t.Error("wrong timeout") | ||
} | ||
|
||
// second filter | ||
f, _ = bt.CreateFilter([]interface{}{"5s"}) | ||
f.Request(c) | ||
|
||
if c.FStateBag[filters.BackendTimeout] != 2*time.Second { | ||
t.Error("no change expected") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
package proxy | ||
|
||
import ( | ||
"fmt" | ||
"io/ioutil" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
"time" | ||
) | ||
|
||
func TestSlowService(t *testing.T) { | ||
service := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
time.Sleep(2 * time.Millisecond) | ||
})) | ||
defer service.Close() | ||
|
||
doc := fmt.Sprintf(`* -> backendTimeout("1ms") -> "%s"`, service.URL) | ||
tp, err := newTestProxy(doc, FlagsNone) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
defer tp.close() | ||
|
||
ps := httptest.NewServer(tp.proxy) | ||
defer ps.Close() | ||
|
||
rsp, err := http.Get(ps.URL) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
if rsp.StatusCode != http.StatusGatewayTimeout { | ||
t.Errorf("expected 504, got: %v", rsp) | ||
} | ||
} | ||
|
||
func TestFastService(t *testing.T) { | ||
service := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
time.Sleep(2 * time.Millisecond) | ||
})) | ||
defer service.Close() | ||
|
||
doc := fmt.Sprintf(`* -> backendTimeout("10ms") -> "%s"`, service.URL) | ||
tp, err := newTestProxy(doc, FlagsNone) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
defer tp.close() | ||
|
||
ps := httptest.NewServer(tp.proxy) | ||
defer ps.Close() | ||
|
||
rsp, err := http.Get(ps.URL) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
if rsp.StatusCode != http.StatusOK { | ||
t.Errorf("expected 200, got: %v", rsp) | ||
} | ||
} | ||
|
||
func TestBackendTimeoutInTheMiddleOfServiceResponse(t *testing.T) { | ||
service := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.WriteHeader(200) | ||
w.Write([]byte("Wish You")) | ||
|
||
f := w.(http.Flusher) | ||
f.Flush() | ||
|
||
time.Sleep(20 * time.Millisecond) | ||
|
||
w.Write([]byte(" Were Here")) | ||
})) | ||
defer service.Close() | ||
|
||
doc := fmt.Sprintf(`* -> backendTimeout("10ms") -> "%s"`, service.URL) | ||
tp, err := newTestProxy(doc, FlagsNone) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
defer tp.close() | ||
|
||
ps := httptest.NewServer(tp.proxy) | ||
defer ps.Close() | ||
|
||
rsp, err := http.Get(ps.URL) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
if rsp.StatusCode != http.StatusOK { | ||
t.Errorf("expected 200, got: %v", rsp) | ||
} | ||
|
||
body, err := ioutil.ReadAll(rsp.Body) | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
|
||
content := string(body) | ||
if content != "Wish You" { | ||
t.Errorf("expected partial content, got %s", content) | ||
} | ||
|
||
const msg = "error while copying the response stream: context deadline exceeded" | ||
if err = tp.log.WaitFor(msg, 10*time.Millisecond); err != nil { | ||
t.Errorf("expected '%s' in logs", msg) | ||
} | ||
} | ||
|
||
type unstableRoundTripper struct { | ||
inner http.RoundTripper | ||
timeout time.Duration | ||
attempt int | ||
} | ||
|
||
// Simulates dial timeout on every odd request | ||
func (r *unstableRoundTripper) RoundTrip(req *http.Request) (rsp *http.Response, err error) { | ||
if r.attempt%2 == 0 { | ||
time.Sleep(r.timeout) | ||
rsp, err = nil, &proxyError{ | ||
code: -1, // omit 0 handling in proxy.Error() | ||
dialingFailed: true, // indicate error happened before http | ||
} | ||
} else { | ||
rsp, err = r.inner.RoundTrip(req) | ||
} | ||
r.attempt = r.attempt + 1 | ||
return | ||
} | ||
|
||
func newUnstable(timeout time.Duration) func(r http.RoundTripper) http.RoundTripper { | ||
return func(r http.RoundTripper) http.RoundTripper { | ||
return &unstableRoundTripper{inner: r, timeout: timeout} | ||
} | ||
} | ||
|
||
// Retryable request, dial timeout on first attempt, load balanced backend | ||
// dial timeout (5ms) + service latency (5ms) > backendTimeout("9ms") => Gateway Timeout | ||
func TestRetryAndSlowService(t *testing.T) { | ||
service := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
time.Sleep(5 * time.Millisecond) | ||
})) | ||
defer service.Close() | ||
|
||
doc := fmt.Sprintf(`* -> backendTimeout("9ms") -> <"%s", "%s">`, service.URL, service.URL) | ||
tp, err := newTestProxyWithParams(doc, Params{ | ||
CustomHttpRoundTripperWrap: newUnstable(5 * time.Millisecond), | ||
}) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
defer tp.close() | ||
tp.log.Unmute() | ||
|
||
ps := httptest.NewServer(tp.proxy) | ||
defer ps.Close() | ||
|
||
rsp, err := http.Get(ps.URL) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
if rsp.StatusCode != http.StatusGatewayTimeout { | ||
t.Errorf("expected 504, got: %v", rsp) | ||
} | ||
} | ||
|
||
// Retryable request, dial timeout on first attempt, load balanced backend | ||
// dial timeout (5ms) + service latency (5ms) < backendTimeout("15ms") => OK | ||
func TestRetryAndFastService(t *testing.T) { | ||
service := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
time.Sleep(5 * time.Millisecond) | ||
})) | ||
defer service.Close() | ||
|
||
doc := fmt.Sprintf(`* -> backendTimeout("15ms") -> <"%s", "%s">`, service.URL, service.URL) | ||
tp, err := newTestProxyWithParams(doc, Params{ | ||
CustomHttpRoundTripperWrap: newUnstable(5 * time.Millisecond), | ||
}) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
defer tp.close() | ||
tp.log.Unmute() | ||
|
||
ps := httptest.NewServer(tp.proxy) | ||
defer ps.Close() | ||
|
||
rsp, err := http.Get(ps.URL) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
if rsp.StatusCode != http.StatusOK { | ||
t.Errorf("expected 200, got: %v", rsp) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.