Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Http Server Read-Write timeouts and Server Side Events

Tags:

go

I'm writing a test app with SSE, but my problem is that ReadTimeout and WriteTimeout are closing the clients connection every 10 Seconds and because of this the main page are losing data.

How can I manage this Issue, serving SSE and webpages together without goroutines leak risk and SSE working done?

Server:

server := &http.Server{Addr: addr,
    ReadTimeout:  10 * time.Second,
    WriteTimeout: 10 * time.Second,
    Handler:      s.mainHandler(),
}

Handler:

func sseHandler(w http.ResponseWriter, r *http.Requests) {
    f, ok := w.(http.Flusher)
    if !ok {
        http.Error(w, "Streaming not supported!", http.StatusInternalServerError)
        log.Println("Streaming not supported")
        return
    }
    messageChannel := make(chan string)
    hub.addClient <- messageChannel
    notify := w.(http.CloseNotifier).CloseNotify()

    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")
    for i := 0; i < 1440; {
        select {
        case msg := <-messageChannel:
            jsonData, _ := json.Marshal(msg)
            str := string(jsonData)
            fmt.Fprintf(w, "data: {\"str\": %s, \"time\": \"%v\"}\n\n", str, time.Now())
            f.Flush()

        case <-time.After(time.Second * 60):
            fmt.Fprintf(w, "data: {\"str\": \"No Data\"}\n\n")

            f.Flush()
            i++

        case <-notify:
            f.Flush()
            i = 1440
            hub.removeClient <- messageChannel
        }
    }
}
like image 919
Mmeyer Avatar asked Nov 24 '14 02:11

Mmeyer


1 Answers

Both ReadTimeout and WriteTimeout define the duration within which the whole request must be read from or written back to the client. These timeouts are applied to the underlying connection (http://golang.org/src/pkg/net/http/server.go?s=15704:15902) and this is before any headers are received, so you cannot set different limits for separate handlers – all connections within the server will share the same timeout limits.

Saying this, if you need customised timeouts per request, you will need to implement them in your handler. In your code you're already using timeouts for your job, so this would be a matter of creating a time.After at the beginning of the handler, keep checking (in the handler itself or even pass it around) and stop the request when necessary. This actually gives you more precise control over the timeout, as WriteTimeout would only trigger when trying to write the response (e.g. if the timeout is set to 10 seconds and it takes a minute to prepare the response before any write, you won't get any error until the job is done so you'll waste resources for 50 seconds). From this perspective, I think WriteTimeout itself is good only when your response is ready quite quickly, but you want to drop the connection when network become very slow (or the client deliberately stops receiving data).

There is a little helper function in the library, http.TimeoutHandler, which behaves similarly to WriteTimeout, but sends back 503 error if the response takes longer than predefined time. You could use it to set up behaviour similar to WriteTimeout per handler, for example:

package main

import (
    "log"   
    "net/http"
    "time"
)

type Handler struct {
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    time.Sleep(3*time.Second)
    // This will return http.ErrHandlerTimeout
    log.Println(w.Write([]byte("body")))
}

func main() {
    h := &Handler{}
    http.Handle("/t1", http.TimeoutHandler(h, 1*time.Second, ""))
    http.Handle("/t2", http.TimeoutHandler(h, 2*time.Second, ""))   
    http.ListenAndServe(":8080", nil)
}

This looks very handy, but I found one disadvantage that would affect your code: http.ResponseWriter passed from http.TimeoutHandler doesn't implement http.CloseNotifier. If it's required, you could dive into the implementation and see how they solved the timeout problem in order to come up with a similar, but enhanced solution.

like image 179
tomasz Avatar answered Oct 29 '22 18:10

tomasz