Skip to content

Commit c5b2419

Browse files
authored
Fix websocket apis (#127)
Remove ping check and instead use a context that will be canceled if it the client sends a close message or an error occurs on reading. The context will be used to cancel all functions using it.
1 parent 732e0ef commit c5b2419

File tree

9 files changed

+265
-567
lines changed

9 files changed

+265
-567
lines changed

portal-ui/bindata_assetfs.go

Lines changed: 71 additions & 71 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

restapi/admin_console.go

Lines changed: 24 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
"fmt"
2323
"log"
2424
"strings"
25-
"sync"
2625
"time"
2726

2827
"github.com/gorilla/websocket"
@@ -32,97 +31,46 @@ import (
3231
const logTimeFormat string = "15:04:05 MST 01/02/2006"
3332

3433
// startConsoleLog starts log of the servers
35-
// by first setting a websocket reader that will
36-
// check for a heartbeat.
37-
//
38-
// A WaitGroup is used to handle goroutines and to ensure
39-
// all finish in the proper order. If any, sendConsoleLogInfo()
40-
// or wsReadCheck() returns, trace should end.
41-
func startConsoleLog(conn WSConn, client MinioAdmin) (mError error) {
42-
// a WaitGroup waits for a collection of goroutines to finish
43-
wg := sync.WaitGroup{}
44-
// a cancel context is needed to end all goroutines used
45-
ctx, cancel := context.WithCancel(context.Background())
46-
defer cancel()
47-
48-
// Set number of goroutines to wait. wg.Wait()
49-
// waitsuntil counter is zero (all are done)
50-
wg.Add(3)
51-
// start go routine for reading websocket heartbeat
52-
readErr := wsReadCheck(ctx, &wg, conn)
53-
// send Stream of Console Log Info to the ws c.connection
54-
logCh := sendConsoleLogInfo(ctx, &wg, conn, client)
55-
// If wsReadCheck returns it means that it is not possible to check
56-
// ws heartbeat anymore so we stop from doing Console Log, cancel context
57-
// for all goroutines.
58-
go func(wg *sync.WaitGroup) {
59-
defer wg.Done()
60-
if err := <-readErr; err != nil {
61-
log.Println("error on wsReadCheck:", err)
62-
mError = err
63-
}
64-
// cancel context for all goroutines.
65-
cancel()
66-
}(&wg)
67-
68-
// get logCh err on finish
69-
if err := <-logCh; err != nil {
70-
mError = err
71-
}
72-
73-
// if logCh closes for any reason,
74-
// cancel context for all goroutines
75-
cancel()
76-
// wait all goroutines to finish
77-
wg.Wait()
78-
return mError
79-
}
80-
81-
// sendlogInfo sends stream of Console Log Info to the ws connection
82-
func sendConsoleLogInfo(ctx context.Context, wg *sync.WaitGroup, conn WSConn, client MinioAdmin) <-chan error {
83-
// decrements the WaitGroup counter
84-
// by one when the function returns
85-
defer wg.Done()
86-
ch := make(chan error)
87-
go func(ch chan<- error) {
88-
defer close(ch)
89-
90-
// TODO: accept parameters as variables
91-
// name of node, default = "" (all)
92-
node := ""
93-
// number of log lines
94-
lineCount := 100
95-
// type of logs "minio"|"application"|"all" default = "all"
96-
logKind := "all"
97-
// Start listening on all Console Log activity.
98-
logCh := client.getLogs(ctx, node, lineCount, logKind)
99-
100-
for logInfo := range logCh {
34+
func startConsoleLog(ctx context.Context, conn WSConn, client MinioAdmin) error {
35+
// TODO: accept parameters as variables
36+
// name of node, default = "" (all)
37+
node := ""
38+
// number of log lines
39+
lineCount := 100
40+
// type of logs "minio"|"application"|"all" default = "all"
41+
logKind := "all"
42+
// Start listening on all Console Log activity.
43+
logCh := client.getLogs(ctx, node, lineCount, logKind)
44+
45+
for {
46+
select {
47+
case <-ctx.Done():
48+
return nil
49+
case logInfo, ok := <-logCh:
50+
// zero value returned because the channel is closed and empty
51+
if !ok {
52+
return nil
53+
}
10154
if logInfo.Err != nil {
10255
log.Println("error on console logs:", logInfo.Err)
103-
ch <- logInfo.Err
104-
return
56+
return logInfo.Err
10557
}
10658

10759
// Serialize message to be sent
10860
bytes, err := json.Marshal(serializeConsoleLogInfo(&logInfo))
10961
if err != nil {
11062
fmt.Println("error on json.Marshal:", err)
111-
ch <- err
112-
return
63+
return err
11364
}
11465

11566
// Send Message through websocket connection
11667
err = conn.writeMessage(websocket.TextMessage, bytes)
11768
if err != nil {
11869
log.Println("error writeMessage:", err)
119-
ch <- err
120-
return
70+
return err
12171
}
12272
}
123-
}(ch)
124-
125-
return ch
73+
}
12674
}
12775

12876
func serializeConsoleLogInfo(l *madmin.LogInfo) (logInfo madmin.LogInfo) {

restapi/admin_console_test.go

Lines changed: 13 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
"fmt"
2323
"testing"
2424

25-
"github.com/gorilla/websocket"
2625
"github.com/minio/minio/pkg/madmin"
2726
"github.com/stretchr/testify/assert"
2827
)
@@ -39,11 +38,13 @@ func TestAdminConsoleLog(t *testing.T) {
3938
assert := assert.New(t)
4039
adminClient := adminClientMock{}
4140
mockWSConn := mockConn{}
42-
function := "startConsoleLog()"
41+
function := "startConsoleLog(ctx, )"
42+
ctx := context.Background()
4343

4444
testReceiver := make(chan madmin.LogInfo, 5)
4545
textToReceive := "test message"
4646
testStreamSize := 5
47+
isClosed := false // testReceiver is closed?
4748

4849
// Test-1: Serve Console with no errors until Console finishes sending
4950
// define mock function behavior for minio server Console
@@ -63,26 +64,24 @@ func TestAdminConsoleLog(t *testing.T) {
6364
}(ch)
6465
return ch
6566
}
66-
// mock function of conn.ReadMessage(), no error on read
67-
connReadMessageMock = func() (messageType int, p []byte, err error) {
68-
return 0, []byte{}, nil
69-
}
7067
writesCount := 1
7168
// mock connection WriteMessage() no error
7269
connWriteMessageMock = func(messageType int, data []byte) error {
7370
// emulate that receiver gets the message written
7471
var t madmin.LogInfo
7572
_ = json.Unmarshal(data, &t)
7673
if writesCount == testStreamSize {
77-
// for testing we need to close the receiver channel
78-
close(testReceiver)
74+
if !isClosed {
75+
close(testReceiver)
76+
isClosed = true
77+
}
7978
return nil
8079
}
8180
testReceiver <- t
8281
writesCount++
8382
return nil
8483
}
85-
if err := startConsoleLog(mockWSConn, adminClient); err != nil {
84+
if err := startConsoleLog(ctx, mockWSConn, adminClient); err != nil {
8685
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
8786
}
8887
// check that the TestReceiver got the same number of data from Console.
@@ -94,50 +93,11 @@ func TestAdminConsoleLog(t *testing.T) {
9493
connWriteMessageMock = func(messageType int, data []byte) error {
9594
return fmt.Errorf("error on write")
9695
}
97-
if err := startConsoleLog(mockWSConn, adminClient); assert.Error(err) {
96+
if err := startConsoleLog(ctx, mockWSConn, adminClient); assert.Error(err) {
9897
assert.Equal("error on write", err.Error())
9998
}
10099

101-
// Test-3: error happens while reading, unexpected Close Error should return error.
102-
connWriteMessageMock = func(messageType int, data []byte) error {
103-
return nil
104-
}
105-
// mock function of conn.ReadMessage(), returns unexpected Close Error CloseAbnormalClosure
106-
connReadMessageMock = func() (messageType int, p []byte, err error) {
107-
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseAbnormalClosure, Text: ""}
108-
}
109-
if err := startConsoleLog(mockWSConn, adminClient); assert.Error(err) {
110-
assert.Equal("websocket: close 1006 (abnormal closure)", err.Error())
111-
}
112-
113-
// Test-4: error happens while reading, expected Close Error NormalClosure
114-
// expected Close Error should not return an error, just end Console.
115-
connReadMessageMock = func() (messageType int, p []byte, err error) {
116-
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: ""}
117-
}
118-
if err := startConsoleLog(mockWSConn, adminClient); err != nil {
119-
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
120-
}
121-
122-
// Test-5: error happens while reading, expected Close Error CloseGoingAway
123-
// expected Close Error should not return an error, just return.
124-
connReadMessageMock = func() (messageType int, p []byte, err error) {
125-
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseGoingAway, Text: ""}
126-
}
127-
if err := startConsoleLog(mockWSConn, adminClient); err != nil {
128-
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
129-
}
130-
131-
// Test-6: error happens while reading, non Close Error Type should be returned as
132-
// error
133-
connReadMessageMock = func() (messageType int, p []byte, err error) {
134-
return 0, []byte{}, fmt.Errorf("error on read")
135-
}
136-
if err := startConsoleLog(mockWSConn, adminClient); assert.Error(err) {
137-
assert.Equal("error on read", err.Error())
138-
}
139-
140-
// Test-7: error happens on GetLogs Minio, Console should stop
100+
// Test-3: error happens on GetLogs Minio, Console should stop
141101
// and error shall be returned.
142102
minioGetLogsMock = func(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo {
143103
ch := make(chan madmin.LogInfo)
@@ -156,12 +116,10 @@ func TestAdminConsoleLog(t *testing.T) {
156116
}(ch)
157117
return ch
158118
}
159-
// mock function of conn.ReadMessage(), no error on read, should stay unless
160-
// context is done.
161-
connReadMessageMock = func() (messageType int, p []byte, err error) {
162-
return 0, []byte{}, nil
119+
connWriteMessageMock = func(messageType int, data []byte) error {
120+
return nil
163121
}
164-
if err := startConsoleLog(mockWSConn, adminClient); assert.Error(err) {
122+
if err := startConsoleLog(ctx, mockWSConn, adminClient); assert.Error(err) {
165123
assert.Equal("error on Console", err.Error())
166124
}
167125
}

restapi/admin_trace.go

Lines changed: 20 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323
"log"
2424
"net/http"
2525
"strings"
26-
"sync"
2726

2827
"github.com/gorilla/websocket"
2928
"github.com/minio/minio/pkg/madmin"
@@ -50,93 +49,40 @@ type callStats struct {
5049
}
5150

5251
// startTraceInfo starts trace of the servers
53-
// by first setting a websocket reader that will
54-
// check for a heartbeat.
55-
//
56-
// A WaitGroup is used to handle goroutines and to ensure
57-
// all finish in the proper order. If any, sendTraceInfo()
58-
// or wsReadCheck() returns, trace should end.
59-
func startTraceInfo(conn WSConn, client MinioAdmin) (mError error) {
60-
// a WaitGroup waits for a collection of goroutines to finish
61-
wg := sync.WaitGroup{}
62-
// a cancel context is needed to end all goroutines used
63-
ctx, cancel := context.WithCancel(context.Background())
64-
defer cancel()
65-
66-
// Set number of goroutines to wait. wg.Wait()
67-
// waitsuntil counter is zero (all are done)
68-
wg.Add(3)
69-
// start go routine for reading websocket heartbeat
70-
readErr := wsReadCheck(ctx, &wg, conn)
71-
// send Stream of Trace Info to the ws c.connection
72-
traceCh := sendTraceInfo(ctx, &wg, conn, client)
73-
// If wsReadCheck returns it means that it is not possible to check
74-
// ws heartbeat anymore so we stop from doing trace, cancel context
75-
// for all goroutines.
76-
go func(wg *sync.WaitGroup) {
77-
defer wg.Done()
78-
if err := <-readErr; err != nil {
79-
log.Println("error on wsReadCheck:", err)
80-
mError = err
81-
}
82-
// cancel context for all goroutines.
83-
cancel()
84-
}(&wg)
85-
86-
// get traceCh error on finish
87-
if err := <-traceCh; err != nil {
88-
mError = err
89-
}
90-
91-
// if traceCh closes for any reason,
92-
// cancel context for all goroutines
93-
cancel()
94-
// wait all goroutines to finish
95-
wg.Wait()
96-
return mError
97-
}
98-
99-
// sendTraceInfo sends stream of Trace Info to the ws connection
100-
func sendTraceInfo(ctx context.Context, wg *sync.WaitGroup, conn WSConn, client MinioAdmin) <-chan error {
101-
// decrements the WaitGroup counter
102-
// by one when the function returns
103-
defer wg.Done()
104-
ch := make(chan error)
105-
go func(ch chan<- error) {
106-
defer close(ch)
107-
108-
// trace all traffic
109-
allTraffic := true
110-
// Trace failed requests only
111-
errOnly := false
112-
// Start listening on all trace activity.
113-
traceCh := client.serviceTrace(ctx, allTraffic, errOnly)
114-
115-
for traceInfo := range traceCh {
52+
func startTraceInfo(ctx context.Context, conn WSConn, client MinioAdmin) error {
53+
// trace all traffic
54+
allTraffic := true
55+
// Trace failed requests only
56+
errOnly := false
57+
// Start listening on all trace activity.
58+
traceCh := client.serviceTrace(ctx, allTraffic, errOnly)
59+
for {
60+
select {
61+
case <-ctx.Done():
62+
return nil
63+
case traceInfo, ok := <-traceCh:
64+
// zero value returned because the channel is closed and empty
65+
if !ok {
66+
return nil
67+
}
11668
if traceInfo.Err != nil {
11769
log.Println("error on serviceTrace:", traceInfo.Err)
118-
ch <- traceInfo.Err
119-
return
70+
return traceInfo.Err
12071
}
12172
// Serialize message to be sent
12273
traceInfoBytes, err := json.Marshal(shortTrace(&traceInfo))
12374
if err != nil {
12475
fmt.Println("error on json.Marshal:", err)
125-
ch <- err
126-
return
76+
return err
12777
}
12878
// Send Message through websocket connection
12979
err = conn.writeMessage(websocket.TextMessage, traceInfoBytes)
13080
if err != nil {
13181
log.Println("error writeMessage:", err)
132-
ch <- err
133-
return
82+
return err
13483
}
13584
}
136-
// TODO: verbose
137-
}(ch)
138-
139-
return ch
85+
}
14086
}
14187

14288
// shortTrace creates a shorter Trace Info message.

0 commit comments

Comments
 (0)