2017-04-18 09:22:06 +03:00
package middlewares
import (
"net/http"
"net/http/httptest"
"testing"
2018-06-19 14:56:04 +03:00
"github.com/containous/traefik/testhelpers"
"github.com/vulcand/oxy/forward"
"github.com/vulcand/oxy/roundrobin"
2017-04-18 09:22:06 +03:00
)
func TestRetry ( t * testing . T ) {
testCases := [ ] struct {
2018-06-19 14:56:04 +03:00
desc string
maxRequestAttempts int
wantRetryAttempts int
wantResponseStatus int
amountFaultyEndpoints int
isWebsocketHandshakeRequest bool
2017-04-18 09:22:06 +03:00
} {
{
2018-06-19 14:56:04 +03:00
desc : "no retry on success" ,
maxRequestAttempts : 1 ,
wantRetryAttempts : 0 ,
wantResponseStatus : http . StatusOK ,
amountFaultyEndpoints : 0 ,
} ,
{
desc : "no retry when max request attempts is one" ,
maxRequestAttempts : 1 ,
wantRetryAttempts : 0 ,
wantResponseStatus : http . StatusInternalServerError ,
amountFaultyEndpoints : 1 ,
} ,
{
desc : "one retry when one server is faulty" ,
maxRequestAttempts : 2 ,
wantRetryAttempts : 1 ,
wantResponseStatus : http . StatusOK ,
amountFaultyEndpoints : 1 ,
} ,
{
desc : "two retries when two servers are faulty" ,
maxRequestAttempts : 3 ,
wantRetryAttempts : 2 ,
wantResponseStatus : http . StatusOK ,
amountFaultyEndpoints : 2 ,
} ,
{
desc : "max attempts exhausted delivers the 5xx response" ,
maxRequestAttempts : 3 ,
wantRetryAttempts : 2 ,
wantResponseStatus : http . StatusInternalServerError ,
amountFaultyEndpoints : 3 ,
2017-04-18 09:22:06 +03:00
} ,
{
2018-06-19 14:56:04 +03:00
desc : "websocket request should not be retried" ,
maxRequestAttempts : 3 ,
wantRetryAttempts : 0 ,
wantResponseStatus : http . StatusBadGateway ,
amountFaultyEndpoints : 1 ,
isWebsocketHandshakeRequest : true ,
2017-04-18 09:22:06 +03:00
} ,
}
2018-06-19 14:56:04 +03:00
backendServer := httptest . NewServer ( http . HandlerFunc ( func ( rw http . ResponseWriter , req * http . Request ) {
rw . WriteHeader ( http . StatusOK )
rw . Write ( [ ] byte ( "OK" ) )
} ) )
forwarder , err := forward . New ( )
if err != nil {
t . Fatalf ( "Error creating forwarder: %s" , err )
}
2017-04-18 09:22:06 +03:00
for _ , tc := range testCases {
tc := tc
2018-06-19 14:56:04 +03:00
t . Run ( tc . desc , func ( t * testing . T ) {
2017-04-18 09:22:06 +03:00
t . Parallel ( )
2018-06-19 14:56:04 +03:00
loadBalancer , err := roundrobin . New ( forwarder )
if err != nil {
t . Fatalf ( "Error creating load balancer: %s" , err )
}
basePort := 33444
for i := 0 ; i < tc . amountFaultyEndpoints ; i ++ {
// 192.0.2.0 is a non-routable IP for testing purposes.
// See: https://stackoverflow.com/questions/528538/non-routable-ip-address/18436928#18436928
// We only use the port specification here because the URL is used as identifier
// in the load balancer and using the exact same URL would not add a new server.
loadBalancer . UpsertServer ( testhelpers . MustParseURL ( "http://192.0.2.0:" + string ( basePort + i ) ) )
}
// add the functioning server to the end of the load balancer list
loadBalancer . UpsertServer ( testhelpers . MustParseURL ( backendServer . URL ) )
retryListener := & countingRetryListener { }
retry := NewRetry ( tc . maxRequestAttempts , loadBalancer , retryListener )
2017-04-18 09:22:06 +03:00
recorder := httptest . NewRecorder ( )
2018-06-19 14:56:04 +03:00
req := httptest . NewRequest ( http . MethodGet , "http://localhost:3000/ok" , nil )
if tc . isWebsocketHandshakeRequest {
req . Header . Add ( "Connection" , "Upgrade" )
req . Header . Add ( "Upgrade" , "websocket" )
2017-04-18 09:22:06 +03:00
}
2018-06-19 14:56:04 +03:00
retry . ServeHTTP ( recorder , req )
2017-04-18 09:22:06 +03:00
2018-06-19 14:56:04 +03:00
if tc . wantResponseStatus != recorder . Code {
t . Errorf ( "got status code %d, want %d" , recorder . Code , tc . wantResponseStatus )
2017-04-18 09:22:06 +03:00
}
2018-06-19 14:56:04 +03:00
if tc . wantRetryAttempts != retryListener . timesCalled {
t . Errorf ( "retry listener called %d time(s), want %d time(s)" , retryListener . timesCalled , tc . wantRetryAttempts )
2017-04-18 09:22:06 +03:00
}
} )
}
}
2018-06-19 14:56:04 +03:00
func TestRetryEmptyServerList ( t * testing . T ) {
forwarder , err := forward . New ( )
if err != nil {
t . Fatalf ( "Error creating forwarder: %s" , err )
2017-05-03 11:20:33 +03:00
}
2018-06-19 14:56:04 +03:00
loadBalancer , err := roundrobin . New ( forwarder )
if err != nil {
t . Fatalf ( "Error creating load balancer: %s" , err )
2017-05-03 11:20:33 +03:00
}
2018-06-19 14:56:04 +03:00
// The EmptyBackendHandler middleware ensures that there is a 503
// response status set when there is no backend server in the pool.
next := NewEmptyBackendHandler ( loadBalancer )
retryListener := & countingRetryListener { }
retry := NewRetry ( 3 , next , retryListener )
recorder := httptest . NewRecorder ( )
req := httptest . NewRequest ( http . MethodGet , "http://localhost:3000/ok" , nil )
retry . ServeHTTP ( recorder , req )
const wantResponseStatus = http . StatusServiceUnavailable
if wantResponseStatus != recorder . Code {
t . Errorf ( "got status code %d, want %d" , recorder . Code , wantResponseStatus )
}
const wantRetryAttempts = 0
if wantRetryAttempts != retryListener . timesCalled {
t . Errorf ( "retry listener called %d time(s), want %d time(s)" , retryListener . timesCalled , wantRetryAttempts )
2017-05-03 11:20:33 +03:00
}
}
2017-08-28 13:50:02 +03:00
func TestRetryListeners ( t * testing . T ) {
req := httptest . NewRequest ( http . MethodGet , "/" , nil )
retryListeners := RetryListeners { & countingRetryListener { } , & countingRetryListener { } }
retryListeners . Retried ( req , 1 )
retryListeners . Retried ( req , 1 )
for _ , retryListener := range retryListeners {
listener := retryListener . ( * countingRetryListener )
if listener . timesCalled != 2 {
2018-06-19 14:56:04 +03:00
t . Errorf ( "retry listener was called %d time(s), want %d time(s)" , listener . timesCalled , 2 )
2017-08-28 13:50:02 +03:00
}
}
}
2017-04-18 09:22:06 +03:00
// countingRetryListener is a RetryListener implementation to count the times the Retried fn is called.
type countingRetryListener struct {
timesCalled int
}
2017-08-28 13:50:02 +03:00
func ( l * countingRetryListener ) Retried ( req * http . Request , attempt int ) {
2017-04-18 09:22:06 +03:00
l . timesCalled ++
}
2018-01-02 18:02:03 +03:00
func TestRetryWithFlush ( t * testing . T ) {
next := http . HandlerFunc ( func ( rw http . ResponseWriter , req * http . Request ) {
rw . WriteHeader ( 200 )
rw . Write ( [ ] byte ( "FULL " ) )
rw . ( http . Flusher ) . Flush ( )
rw . Write ( [ ] byte ( "DATA" ) )
} )
retry := NewRetry ( 1 , next , & countingRetryListener { } )
responseRecorder := httptest . NewRecorder ( )
retry . ServeHTTP ( responseRecorder , & http . Request { } )
if responseRecorder . Body . String ( ) != "FULL DATA" {
t . Errorf ( "Wrong body %q want %q" , responseRecorder . Body . String ( ) , "FULL DATA" )
}
}