2

I wondered for the best method to set a timeout for the specific route on the Echo library.

I implemented timeout with context.WithTimeout, but used several functions to encapsulate the context, and I think that was wrong.

ctx, cancel := context.WithTimeout(ctx, 30*time.Second)

Is there any middleware or a better method to achieve that?

oguz ismail
  • 1
  • 16
  • 47
  • 69
Reza Mousavi
  • 4,420
  • 5
  • 31
  • 48
  • 1
    A goroutine cannot be killed like processes, it needs to listen to a signal and then exit by itself. Not entirely sure how to do it in echo, but typically when using a timeout, your code has to actively check the context if it's [Done](https://pkg.go.dev/context#Context.Done) (check out the example with the `for` loop there). Not the best example, but after each single step [this code](https://github.com/xarantolus/upduck/blob/df73411b0c10f7f82ab561ac2b22c552576155c0/archive.go#L21-L25) checks if the context has finished (timed out in your case) and stops processing if that's the case. – xarantolus Jan 31 '21 at 19:52
  • @xarantolus is there any way to terminate a process regardless of which are things running inside it? – Reza Mousavi Feb 01 '21 at 20:47

1 Answers1

4

Adding a timeout to Server context.Context

This is a bit tricky to answer without knowing exactly what you are trying to do. I'll answer first on how to handle context wrapping using WithTimeout with a middleware.

A middleware can add/modify a request context like so:

func TimeoutMiddleware(timeout time.Duration, next func(w http.ResponseWriter, req *http.Request)) func(w http.ResponseWriter, req *http.Request) {
    return func(w http.ResponseWriter, req *http.Request) {
        // Wrap the existing context from the request
        ctx, cancel := context.WithTimeout(req.Context(), timeout)
        // Always do this to clean up contexts, otherwise they'll hang out
        // and gather since they are blocked go rountines
        defer cancel()

        // Put the new context into the request
        req = req.WithContext(ctx)

        // Pass the modified request forward
        next(w, req)

        // Handle any ctx timeout issues in the general middleware
        if err := ctx.Err(); err != nil {
            if errors.Is(err, context.DeadlineExceeded) {
                log.Println("HTTP Request timed out")
                w.Write([]byte("Timed out"))
            }
        }
    }
}

The issue is next(w, req) requires your http handler to handle a context timeout. If the http handler ignores the context, it will not timeout. Such as this:

func endless(w http.ResponseWriter, req *http.Request) {
    ctx := req.Context()
    // Just a busy loop....
    for {
        select {
        case <-ctx.Done():
            // Exit the handler if the context is Done(), even if our function is not.
            return
        default:
            fmt.Println("wait")
            time.Sleep(1 * time.Second)
        }
    }
}

If you are making a database call or something that takes time in a handler, often times the database api accepts a context.Context to abort early if the context is canceled.

So this solution adds a timeout to the request context going into a handler that you still need to manage.


Client Timeout

You can always add the timeouts to your request:

    client := http.Client{
        Timeout: time.Second,
    }
    client.Do(...)
Steven Masley
  • 634
  • 2
  • 9
  • is there any way to terminate a process regardless of which are things running inside it? like a real multi thread application – Reza Mousavi Feb 03 '21 at 13:41
  • 1
    No there is not. You have to implement your own termination logic for a goroutine, usually using contexts. It should always be possible to terminate a go routine using a context though, I have never hit a case where I could not. All libraries and packages with long running functions support context.Context. – Steven Masley Feb 03 '21 at 16:11