-
Notifications
You must be signed in to change notification settings - Fork 886
fix: Avoid panic in ServerSentEventSender
by keeping handler alive
#4821
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,14 +6,13 @@ import ( | |
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"reflect" | ||
"strings" | ||
"sync" | ||
"time" | ||
|
||
"github.com/go-playground/validator/v10" | ||
"golang.org/x/xerrors" | ||
|
||
"github.com/coder/coder/coderd/tracing" | ||
"github.com/coder/coder/codersdk" | ||
|
@@ -174,8 +173,7 @@ func WebsocketCloseSprintf(format string, vars ...any) string { | |
return msg | ||
} | ||
|
||
func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) (func(ctx context.Context, sse codersdk.ServerSentEvent) error, error) { | ||
var mu sync.Mutex | ||
func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) (sendEvent func(ctx context.Context, sse codersdk.ServerSentEvent) error, closed chan struct{}, err error) { | ||
h := rw.Header() | ||
h.Set("Content-Type", "text/event-stream") | ||
h.Set("Cache-Control", "no-cache") | ||
|
@@ -187,37 +185,50 @@ func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) (func(ctx co | |
panic("http.ResponseWriter is not http.Flusher") | ||
} | ||
|
||
// Send a heartbeat every 15 seconds to avoid the connection being killed. | ||
closed = make(chan struct{}) | ||
type sseEvent struct { | ||
payload []byte | ||
errC chan error | ||
} | ||
eventC := make(chan sseEvent) | ||
|
||
// Synchronized handling of events (no guarantee of order). | ||
go func() { | ||
defer close(closed) | ||
|
||
// Send a heartbeat every 15 seconds to avoid the connection being killed. | ||
ticker := time.NewTicker(time.Second * 15) | ||
defer ticker.Stop() | ||
|
||
for { | ||
var event sseEvent | ||
|
||
select { | ||
case <-r.Context().Done(): | ||
return | ||
Comment on lines
207
to
208
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why doesn't this prevent the write/flush after finish? We're not hijacking the connection so the context should still be cancelled on finish right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because this code had the following race condition:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Awesome explanation thank you |
||
case event = <-eventC: | ||
case <-ticker.C: | ||
mu.Lock() | ||
_, err := io.WriteString(rw, fmt.Sprintf("event: %s\n\n", codersdk.ServerSentEventTypePing)) | ||
if err != nil { | ||
mu.Unlock() | ||
return | ||
event = sseEvent{ | ||
payload: []byte(fmt.Sprintf("event: %s\n\n", codersdk.ServerSentEventTypePing)), | ||
} | ||
f.Flush() | ||
mu.Unlock() | ||
} | ||
} | ||
}() | ||
|
||
sendEvent := func(ctx context.Context, sse codersdk.ServerSentEvent) error { | ||
if ctx.Err() != nil { | ||
return ctx.Err() | ||
_, err := rw.Write(event.payload) | ||
if event.errC != nil { | ||
event.errC <- err | ||
} | ||
if err != nil { | ||
return | ||
} | ||
f.Flush() | ||
} | ||
}() | ||
|
||
sendEvent = func(ctx context.Context, sse codersdk.ServerSentEvent) error { | ||
buf := &bytes.Buffer{} | ||
enc := json.NewEncoder(buf) | ||
|
||
_, err := buf.Write([]byte(fmt.Sprintf("event: %s\ndata: ", sse.Type))) | ||
_, err := buf.WriteString(fmt.Sprintf("event: %s\ndata: ", sse.Type)) | ||
if err != nil { | ||
return err | ||
} | ||
|
@@ -232,16 +243,32 @@ func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) (func(ctx co | |
return err | ||
} | ||
|
||
mu.Lock() | ||
defer mu.Unlock() | ||
_, err = rw.Write(buf.Bytes()) | ||
if err != nil { | ||
return err | ||
event := sseEvent{ | ||
payload: buf.Bytes(), | ||
errC: make(chan error, 1), // Buffered to prevent deadlock. | ||
} | ||
f.Flush() | ||
|
||
return nil | ||
select { | ||
case <-r.Context().Done(): | ||
return r.Context().Err() | ||
case <-ctx.Done(): | ||
return ctx.Err() | ||
case <-closed: | ||
return xerrors.New("server sent event sender closed") | ||
deansheather marked this conversation as resolved.
Show resolved
Hide resolved
|
||
case eventC <- event: | ||
// Re-check closure signals after sending the event to allow | ||
// for early exit. We don't check closed here because it | ||
// can't happen while processing the event. | ||
select { | ||
case <-r.Context().Done(): | ||
return r.Context().Err() | ||
case <-ctx.Done(): | ||
return ctx.Err() | ||
case err := <-event.errC: | ||
return err | ||
} | ||
} | ||
} | ||
|
||
return sendEvent, nil | ||
return sendEvent, closed, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like we can eliminate the error return from this as it's never set to a non-nil value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really relevant for this PR, but that's a good point. I'd like to see it kept and remove the panic in favor of an error, though.