Jake Muller
Jake Muller

Reputation: 1063

Retry http request RoundTrip

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

Answers (2)

mfathirirhas
mfathirirhas

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

Eelco
Eelco

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

Related Questions