Context timeout implementation on every request us

2019-03-11 12:59发布

问题:

I am trying to handle context timeout for every request. We have following server structures:

Flow overview:

Go Server: Basically, acts as a [Reverse-proxy].2

Auth Server: Check for requests Authentication.

Application Server: Core request processing logic.

Now if Authorization server can't able to process a request in stipulated time, then I want to close the goroutine from memory.

Here is what I tried:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, _ := http.NewRequest("GET", authorizationServer, nil)
req.Header = r.Header
req.WithContext(ctx)
res, error := client.Do(req)
select {
case <-time.After(10 * time.Second):
    fmt.Println("overslept")
case <-ctx.Done():
    fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}

Over here, context returns as "deadline exceeded", if request is not processed in stipulated time. But It continues to process that request and return response in more that specified time. So How can I stop request flow(goroutine), when time exceeded.

Although I've also implemented complete request needs to be processed in 60 seconds with this code:

var netTransport = &http.Transport{
    Dial: (&net.Dialer{
        Timeout: 60 * time.Second,
    }).Dial,
    TLSHandshakeTimeout: 60 * time.Second,
}
client := &http.Client{
    Timeout:   time.Second * 60,
    Transport: netTransport,
    CheckRedirect: func(req *http.Request, via []*http.Request) error {
        return http.ErrUseLastResponse
    },
}

So do I need any seperate context implementations as well? Thanks in advance for help.

Note1: It will be awesome, If we can manage timeout on every requests(goroutine) created by HTTP server, using context.

回答1:

What happens in your code is very correct and behaves as expected.

You create a context with 5 seconds timeout. You pass it to the request and make that request. Let's say that request returns in 2 seconds. You then do a select and either wait 10 seconds or wait for the context to finish. Context will always finish in the initial 5 seconds from when it was created and will also give that error every time it reaches the end.

The context is independent of the request and it will reach it's deadline unless, cancelled previously. You cancel the request when the function finishes using defer.

In your code the request takes your timeout in consideration. But the ctx.Err() will return deadline exceeded everytime it reaches the timeout. Since that's what happens inside the context. calling ctx.Err() multiple times will return the same error.

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

go func () {
    select {
    case <-time.After(10 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err()) // prints "context deadline exceeded"
    }
}()
req, _ := http.NewRequest("GET", authorizationServer, nil)
req.Header = r.Header
req = req.WithContext(ctx)
res, error := client.Do(req)

From the context documentation:

// Err returns a non-nil error value after Done is closed. Err returns
// Canceled if the context was canceled or DeadlineExceeded if the
// context's deadline passed. No other values for Err are defined.
// After Done is closed, successive calls to Err return the same value.

In your code, the timeout will always be reached and not cancelled, that is why you receive DeadlineExceeeded. Your code is correct except the select part which will block until either 10 seconds pass or context timeout is reached. In your case always the context timeout is reached.

You should check the error returned by the client.Do call and not worry about the context error in here. You are the one controlling the context. If the request timeouts, a case you should test of course, then a proper error would be returned for you to verify.