Skip to content

Commit 65010e5

Browse files
authored
sse: add closed SelectCase to handle request closing signal (#25)
Signed-off-by: E99p1ant <i@github.red>
1 parent daaa4c3 commit 65010e5

2 files changed

Lines changed: 74 additions & 3 deletions

File tree

sse.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,24 @@ func Bind(obj interface{}, opts ...Options) flamego.Handler {
4545
}
4646
c.Set(reflect.ChanOf(reflect.SendDir, sse.sender.Type().Elem()), sse.sender)
4747

48-
go sse.handle(log, c.ResponseWriter())
48+
// stopCh is closed when the next handler returns, signaling handle()
49+
// to stop writing to the ResponseWriter.
50+
stopCh := make(chan struct{})
51+
// doneCh is closed when handle() exits, allowing the handler to wait
52+
// for the goroutine to fully stop before returning.
53+
doneCh := make(chan struct{})
54+
55+
go func() {
56+
defer close(doneCh)
57+
sse.handle(log, c, stopCh)
58+
}()
59+
60+
// Call the next handler(s) in the chain. When they return, signal
61+
// the handle goroutine to stop, then wait for it to finish before
62+
// returning control to the HTTP server.
63+
c.Next()
64+
close(stopCh)
65+
<-doneCh
4966
}
5067
}
5168

@@ -59,7 +76,8 @@ func newOptions(opts []Options) Options {
5976
return opts[0]
6077
}
6178

62-
func (c *connection) handle(log *log.Logger, w flamego.ResponseWriter) {
79+
func (c *connection) handle(log *log.Logger, ctx flamego.Context, stopCh <-chan struct{}) {
80+
w := ctx.ResponseWriter()
6381
ticker := time.NewTicker(c.PingInterval)
6482
defer func() { ticker.Stop() }()
6583

@@ -78,11 +96,15 @@ func (c *connection) handle(log *log.Logger, w flamego.ResponseWriter) {
7896
senderSend = iota
7997
tickerTick
8098
timeout
99+
closed
100+
stopped
81101
)
82-
cases := make([]reflect.SelectCase, 3)
102+
cases := make([]reflect.SelectCase, 5)
83103
cases[senderSend] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: c.sender, Send: reflect.ValueOf(nil)}
84104
cases[tickerTick] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ticker.C), Send: reflect.ValueOf(nil)}
85105
cases[timeout] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(time.After(time.Hour)), Send: reflect.ValueOf(nil)}
106+
cases[closed] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ctx.Request().Context().Done()), Send: reflect.ValueOf(nil)}
107+
cases[stopped] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(stopCh), Send: reflect.ValueOf(nil)}
86108

87109
loop:
88110
for {
@@ -112,6 +134,12 @@ loop:
112134
write("events: stream timeout\n\n")
113135
w.Flush()
114136
break loop
137+
138+
case closed:
139+
return
140+
141+
case stopped:
142+
return
115143
}
116144
}
117145

sse_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package sse
66

77
import (
88
"bytes"
9+
"context"
910
"net/http"
1011
"net/http/httptest"
1112
"sync"
@@ -63,6 +64,27 @@ func TestBind(t *testing.T) {
6364
time.Sleep(1 * time.Second)
6465
},
6566
)
67+
f.Get("/ticker",
68+
Bind(
69+
object{},
70+
Options{
71+
100 * time.Millisecond,
72+
},
73+
),
74+
func(ctx flamego.Context, msg chan<- *object) {
75+
ticker := time.NewTicker(1 * time.Second)
76+
defer func() { ticker.Stop() }()
77+
78+
for {
79+
select {
80+
case <-ticker.C:
81+
msg <- &object{Message: "Flamego"}
82+
case <-ctx.Request().Context().Done():
83+
return
84+
}
85+
}
86+
},
87+
)
6688

6789
t.Run("normal", func(t *testing.T) {
6890
resp := &mockResponseWriter{
@@ -118,4 +140,25 @@ data: {"Message":"Flamego"}
118140
`
119141
assert.Equal(t, wantBody, resp.Body())
120142
})
143+
144+
t.Run("close connection", func(t *testing.T) {
145+
server := httptest.NewServer(f)
146+
147+
reqContext, cancel := context.WithCancel(context.Background())
148+
req, err := http.NewRequestWithContext(reqContext, http.MethodGet, server.URL+"/ticker", nil)
149+
require.NoError(t, err)
150+
151+
// Close request connection after 1 second.
152+
go func() {
153+
time.Sleep(1 * time.Second)
154+
cancel()
155+
}()
156+
resp, err := http.DefaultClient.Do(req)
157+
require.NoError(t, err)
158+
err = resp.Body.Close()
159+
require.NoError(t, err)
160+
161+
// Sleep for 3 seconds to wait for new responses that may be in a closed request.
162+
time.Sleep(3 * time.Second)
163+
})
121164
}

0 commit comments

Comments
 (0)