@@ -6,14 +6,13 @@ import (
6
6
"encoding/json"
7
7
"errors"
8
8
"fmt"
9
- "io"
10
9
"net/http"
11
10
"reflect"
12
11
"strings"
13
- "sync"
14
12
"time"
15
13
16
14
"github.com/go-playground/validator/v10"
15
+ "golang.org/x/xerrors"
17
16
18
17
"github.com/coder/coder/coderd/tracing"
19
18
"github.com/coder/coder/codersdk"
@@ -174,8 +173,7 @@ func WebsocketCloseSprintf(format string, vars ...any) string {
174
173
return msg
175
174
}
176
175
177
- func ServerSentEventSender (rw http.ResponseWriter , r * http.Request ) (func (ctx context.Context , sse codersdk.ServerSentEvent ) error , error ) {
178
- var mu sync.Mutex
176
+ func ServerSentEventSender (rw http.ResponseWriter , r * http.Request ) (sendEvent func (ctx context.Context , sse codersdk.ServerSentEvent ) error , closed chan struct {}, err error ) {
179
177
h := rw .Header ()
180
178
h .Set ("Content-Type" , "text/event-stream" )
181
179
h .Set ("Cache-Control" , "no-cache" )
@@ -187,37 +185,50 @@ func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) (func(ctx co
187
185
panic ("http.ResponseWriter is not http.Flusher" )
188
186
}
189
187
190
- // Send a heartbeat every 15 seconds to avoid the connection being killed.
188
+ closed = make (chan struct {})
189
+ type sseEvent struct {
190
+ payload []byte
191
+ errC chan error
192
+ }
193
+ eventC := make (chan sseEvent )
194
+
195
+ // Synchronized handling of events (no guarantee of order).
191
196
go func () {
197
+ defer close (closed )
198
+
199
+ // Send a heartbeat every 15 seconds to avoid the connection being killed.
192
200
ticker := time .NewTicker (time .Second * 15 )
193
201
defer ticker .Stop ()
194
202
195
203
for {
204
+ var event sseEvent
205
+
196
206
select {
197
207
case <- r .Context ().Done ():
198
208
return
209
+ case event = <- eventC :
199
210
case <- ticker .C :
200
- mu .Lock ()
201
- _ , err := io .WriteString (rw , fmt .Sprintf ("event: %s\n \n " , codersdk .ServerSentEventTypePing ))
202
- if err != nil {
203
- mu .Unlock ()
204
- return
211
+ event = sseEvent {
212
+ payload : []byte (fmt .Sprintf ("event: %s\n \n " , codersdk .ServerSentEventTypePing )),
205
213
}
206
- f .Flush ()
207
- mu .Unlock ()
208
214
}
209
- }
210
- }()
211
215
212
- sendEvent := func (ctx context.Context , sse codersdk.ServerSentEvent ) error {
213
- if ctx .Err () != nil {
214
- return ctx .Err ()
216
+ _ , err := rw .Write (event .payload )
217
+ if event .errC != nil {
218
+ event .errC <- err
219
+ }
220
+ if err != nil {
221
+ return
222
+ }
223
+ f .Flush ()
215
224
}
225
+ }()
216
226
227
+ sendEvent = func (ctx context.Context , sse codersdk.ServerSentEvent ) error {
217
228
buf := & bytes.Buffer {}
218
229
enc := json .NewEncoder (buf )
219
230
220
- _ , err := buf .Write ([] byte ( fmt .Sprintf ("event: %s\n data: " , sse .Type ) ))
231
+ _ , err := buf .WriteString ( fmt .Sprintf ("event: %s\n data: " , sse .Type ))
221
232
if err != nil {
222
233
return err
223
234
}
@@ -232,16 +243,32 @@ func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) (func(ctx co
232
243
return err
233
244
}
234
245
235
- mu .Lock ()
236
- defer mu .Unlock ()
237
- _ , err = rw .Write (buf .Bytes ())
238
- if err != nil {
239
- return err
246
+ event := sseEvent {
247
+ payload : buf .Bytes (),
248
+ errC : make (chan error , 1 ), // Buffered to prevent deadlock.
240
249
}
241
- f .Flush ()
242
250
243
- return nil
251
+ select {
252
+ case <- r .Context ().Done ():
253
+ return r .Context ().Err ()
254
+ case <- ctx .Done ():
255
+ return ctx .Err ()
256
+ case <- closed :
257
+ return xerrors .New ("server sent event sender closed" )
258
+ case eventC <- event :
259
+ // Re-check closure signals after sending the event to allow
260
+ // for early exit. We don't check closed here because it
261
+ // can't happen while processing the event.
262
+ select {
263
+ case <- r .Context ().Done ():
264
+ return r .Context ().Err ()
265
+ case <- ctx .Done ():
266
+ return ctx .Err ()
267
+ case err := <- event .errC :
268
+ return err
269
+ }
270
+ }
244
271
}
245
272
246
- return sendEvent , nil
273
+ return sendEvent , closed , nil
247
274
}
0 commit comments