Reputation: 1063
I made a server with client hitting throught http. I set retry mechanism in the client inside its Transport's RoundTripper method. Here's the example of working code for each server and client:
server main.go
package main
import (
"fmt"
"net/http"
"time"
)
func test(w http.ResponseWriter, req *http.Request) {
time.Sleep(2 * time.Second)
fmt.Fprintf(w, "hello\n")
}
func main() {
http.HandleFunc("/test", test)
http.ListenAndServe(":8090", nil)
}
client main.go
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
)
type Retry struct {
nums int
transport http.RoundTripper
}
// to retry
func (r *Retry) RoundTrip(req *http.Request) (resp *http.Response, err error) {
for i := 0; i < r.nums; i++ {
log.Println("Attempt: ", i+1)
resp, err = r.transport.RoundTrip(req)
if resp != nil && err == nil {
return
}
log.Println("Retrying...")
}
return
}
func main() {
r := &Retry{
nums: 5,
transport: http.DefaultTransport,
}
c := &http.Client{Transport: r}
// each request will be timeout in 1 second
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8090/test", nil)
if err != nil {
panic(err)
}
resp, err := c.Do(req)
if err != nil {
panic(err)
}
fmt.Println(resp.StatusCode)
}
What's happening is the retry seems only work for first iteration. For subsequent iteration it doesn't wait for one second each, instead the debugging message printed for as much as the retry nums.
I expect the retry attempt to be waiting 1 second each as I put the timeout for 1 second in the context. But it seems only wait for 1 second for whole retries. What do I miss?
Beside, how to stop server from processing timeout request?, I saw CloseNotifier
already deprecated.
Upvotes: 2
Views: 4946
Reputation: 2287
The problem is with the context
. Once the context is done, you cannot reuse the same context anymore. You have to re-create the context at every attempt. You can get the timeout from parent context, and use it to create new context with it.
func (r *retry) RoundTrip(req *http.Request) (resp *http.Response, err error) {
var (
duration time.Duration
ctx context.Context
cancel func()
)
if deadline, ok := req.Context().Deadline(); ok {
duration = time.Until(deadline)
}
for i := 0; i < r.nums; i++ {
if duration > 0 {
ctx, cancel = context.WithTimeout(context.Background(), duration)
req = req.WithContext(ctx)
}
resp, err = r.rt.RoundTrip(req)
...
// the rest of code
...
}
return
}
This code will create new fresh context at every attempt by using the timeout from its parent.
Upvotes: 6
Reputation: 547
For the server you can use the Request.Context()
to check if a request is cancelled or not.
In the client the request times out when the context times out after 1 second. So the context does not trigger the roundtrip period. If you want the request to retry before the context is done you should change the behaviour of the transport you are using. You are now using the http.DefaultTransport
which is defined as follows:
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
Transport
has more time out variables these set here so depending on when you want to retry you should set the appropriate time out. For example in your case you could set Transport.ResponseHeaderTimeout
to 1 second. When the server does not reply with a response header within 1 second the client will retry. You then make your context time out after 5 (or better 6) seconds you should see the client retrying the amount you specified (5 times).
Upvotes: 0