Skip to content

Commit 5017924

Browse files
committed
Cleanup CloseStatus/CloseError docs and improve wasm test script
1 parent 773d2f2 commit 5017924

File tree

9 files changed

+94
-33
lines changed

9 files changed

+94
-33
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ For a production quality example that shows off the full API, see the [echo exam
3636

3737
Use the [errors.As](https://golang.org/pkg/errors/#As) function [new in Go 1.13](https://golang.org/doc/go1.13#error_wrapping) to check for [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError).
3838
There is also [websocket.CloseStatus](https://godoc.org/nhooyr.io/websocket#CloseStatus) to quickly grab the close status code out of a [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError).
39-
See the [CloseError godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseError).
39+
See the [CloseStatus godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseStatus).
4040

4141
### Server
4242

ci/test.mk

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ codecov: _gotest
1212
curl -s https://codecov.io/bash | bash -s -- -Z -f ci/out/coverage.prof
1313

1414
_gotest:
15-
echo "--- gotest" && go test -parallel=32 -coverprofile=ci/out/coverage.prof -coverpkg=./... ./...
15+
echo "--- gotest" && go test -parallel=32 -coverprofile=ci/out/coverage.prof -coverpkg=./... $$TESTFLAGS ./...
1616
sed -i '/_stringer\.go/d' ci/out/coverage.prof
1717
sed -i '/wsjstest\/main\.go/d' ci/out/coverage.prof
1818
sed -i '/wsecho\.go/d' ci/out/coverage.prof
1919
sed -i '/assert\.go/d' ci/out/coverage.prof
20+
sed -i '/wsgrace\.go/d' ci/out/coverage.prof
2021

2122
gotest-wasm: wsjstest
2223
echo "--- wsjstest" && ./ci/wasmtest.sh

ci/wasmtest.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ set -euo pipefail
55
wsjstestOut="$(mktemp -d)/wsjstestOut"
66
mkfifo "$wsjstestOut"
77
timeout 45s wsjstest > "$wsjstestOut" &
8-
wsjstestPID="$!"
98

109
WS_ECHO_SERVER_URL="$(head -n 1 "$wsjstestOut")"
1110
export WS_ECHO_SERVER_URL
1211

1312
GOOS=js GOARCH=wasm go test -exec=wasmbrowsertest ./...
1413

15-
if ! wait "$wsjstestPID" ; then
14+
kill %%
15+
if ! wait %% ; then
1616
echo "wsjstest exited unsuccessfully"
1717
exit 1
1818
fi

conn.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,14 @@ func (c *Conn) timeoutLoop() {
175175

176176
case <-readCtx.Done():
177177
c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err()))
178-
// Guaranteed to eventually close the connection since it will not try and read
179-
// but only write.
180-
go c.exportedClose(StatusPolicyViolation, "read timed out", false)
178+
// Guaranteed to eventually close the connection since we can only ever send
179+
// one close frame.
180+
go func() {
181+
c.exportedClose(StatusPolicyViolation, "read timed out", true)
182+
// Ensure the connection closes, i.e if we already sent a close frame and timed out
183+
// to read the peer's close frame.
184+
c.close(nil)
185+
}()
181186
readCtx = context.Background()
182187
case <-writeCtx.Done():
183188
c.close(fmt.Errorf("write timed out: %w", writeCtx.Err()))
@@ -339,6 +344,13 @@ func (c *Conn) handleControl(ctx context.Context, h header) error {
339344

340345
err = fmt.Errorf("received close: %w", ce)
341346
c.writeClose(b, err, false)
347+
348+
if ctx.Err() != nil {
349+
// The above close probably has been returned by the peer in response
350+
// to our read timing out so we have to return the read timed out error instead.
351+
return fmt.Errorf("read timed out: %w", ctx.Err())
352+
}
353+
342354
return err
343355
default:
344356
panic(fmt.Sprintf("websocket: unexpected control opcode: %#v", h))

conn_test.go

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
"reflect"
2323
"strconv"
2424
"strings"
25-
"sync/atomic"
2625
"testing"
2726
"time"
2827

@@ -34,6 +33,7 @@ import (
3433
"nhooyr.io/websocket"
3534
"nhooyr.io/websocket/internal/assert"
3635
"nhooyr.io/websocket/internal/wsecho"
36+
"nhooyr.io/websocket/internal/wsgrace"
3737
"nhooyr.io/websocket/wsjson"
3838
"nhooyr.io/websocket/wspb"
3939
)
@@ -927,16 +927,7 @@ func TestConn(t *testing.T) {
927927
}
928928

929929
func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request) error, tls bool) (s *httptest.Server, closeFn func()) {
930-
var conns int64
931930
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
932-
atomic.AddInt64(&conns, 1)
933-
defer atomic.AddInt64(&conns, -1)
934-
935-
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
936-
defer cancel()
937-
938-
r = r.WithContext(ctx)
939-
940931
err := fn(w, r)
941932
if err != nil {
942933
tb.Errorf("server failed: %+v", err)
@@ -947,18 +938,12 @@ func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request) e
947938
} else {
948939
s = httptest.NewServer(h)
949940
}
941+
closeFn2 := wsgrace.Grace(s.Config)
950942
return s, func() {
951-
s.Close()
952-
953-
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
954-
defer cancel()
955-
956-
for atomic.LoadInt64(&conns) > 0 {
957-
if ctx.Err() != nil {
958-
tb.Fatalf("waiting for server to come down timed out: %v", ctx.Err())
959-
}
943+
err := closeFn2()
944+
if err != nil {
945+
tb.Fatal(err)
960946
}
961-
962947
}
963948
}
964949

doc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
//
1818
// Use the errors.As function new in Go 1.13 to check for websocket.CloseError.
1919
// Or use the CloseStatus function to grab the StatusCode out of a websocket.CloseError
20-
// See the CloseError example.
20+
// See the CloseStatus example.
2121
//
2222
// Wasm
2323
//

example_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func ExampleDial() {
6464

6565
// This example dials a server and then expects to be disconnected with status code
6666
// websocket.StatusNormalClosure.
67-
func ExampleCloseError() {
67+
func ExampleCloseStatus() {
6868
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
6969
defer cancel()
7070

internal/wsgrace/wsgrace.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package wsgrace
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"sync/atomic"
8+
"time"
9+
)
10+
11+
// Grace wraps s.Handler to gracefully shutdown WebSocket connections.
12+
// The returned function must be used to close the server instead of s.Close.
13+
func Grace(s *http.Server) (closeFn func() error) {
14+
h := s.Handler
15+
var conns int64
16+
s.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17+
atomic.AddInt64(&conns, 1)
18+
defer atomic.AddInt64(&conns, -1)
19+
20+
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
21+
defer cancel()
22+
23+
r = r.WithContext(ctx)
24+
25+
h.ServeHTTP(w, r)
26+
})
27+
28+
return func() error {
29+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
30+
defer cancel()
31+
32+
err := s.Shutdown(ctx)
33+
if err != nil {
34+
return fmt.Errorf("server shutdown failed: %v", err)
35+
}
36+
37+
t := time.NewTicker(time.Millisecond * 10)
38+
defer t.Stop()
39+
for {
40+
select {
41+
case <-t.C:
42+
if atomic.LoadInt64(&conns) == 0 {
43+
return nil
44+
}
45+
case <-ctx.Done():
46+
return fmt.Errorf("failed to wait for WebSocket connections: %v", ctx.Err())
47+
}
48+
}
49+
}
50+
}

internal/wsjstest/main.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@ import (
88
"net/http"
99
"net/http/httptest"
1010
"os"
11-
"runtime"
11+
"os/signal"
1212
"strings"
13+
"syscall"
1314

1415
"nhooyr.io/websocket"
1516
"nhooyr.io/websocket/internal/wsecho"
17+
"nhooyr.io/websocket/internal/wsgrace"
1618
)
1719

1820
func main() {
21+
log.SetPrefix("wsecho")
22+
1923
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2024
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
2125
Subprotocols: []string{"echo"},
@@ -30,11 +34,20 @@ func main() {
3034
if websocket.CloseStatus(err) != websocket.StatusNormalClosure {
3135
log.Fatalf("unexpected echo loop error: %+v", err)
3236
}
33-
34-
os.Exit(0)
3537
}))
38+
closeFn := wsgrace.Grace(s.Config)
39+
defer func() {
40+
err := closeFn()
41+
if err != nil {
42+
log.Fatal(err)
43+
}
44+
}()
3645

3746
wsURL := strings.Replace(s.URL, "http", "ws", 1)
3847
fmt.Printf("%v\n", wsURL)
39-
runtime.Goexit()
48+
49+
sigs := make(chan os.Signal, 1)
50+
signal.Notify(sigs, syscall.SIGTERM)
51+
52+
<-sigs
4053
}

0 commit comments

Comments
 (0)