mirror of
https://github.com/fluencelabs/tendermint
synced 2025-05-29 06:01:21 +00:00
Read timeouts for WSConnection
This commit is contained in:
parent
290b74d8f1
commit
d8886614b5
@ -211,9 +211,9 @@ func _jsonStringToArg(ty reflect.Type, arg string) (reflect.Value, error) {
|
|||||||
// rpc.websocket
|
// rpc.websocket
|
||||||
|
|
||||||
const (
|
const (
|
||||||
WSConnectionReaperSeconds = 5
|
writeChanCapacity = 20
|
||||||
MaxFailedSends = 10
|
WSWriteTimeoutSeconds = 10 // exposed for tests
|
||||||
WriteChanBufferSize = 10
|
WSReadTimeoutSeconds = 10 // exposed for tests
|
||||||
)
|
)
|
||||||
|
|
||||||
// a single websocket connection
|
// a single websocket connection
|
||||||
@ -223,135 +223,154 @@ type WSConnection struct {
|
|||||||
QuitService
|
QuitService
|
||||||
|
|
||||||
id string
|
id string
|
||||||
wsConn *websocket.Conn
|
baseConn *websocket.Conn
|
||||||
writeChan chan WSResponse
|
writeChan chan WSResponse
|
||||||
failedSends int
|
readTimeout *time.Timer
|
||||||
|
|
||||||
evsw *events.EventSwitch
|
evsw *events.EventSwitch
|
||||||
}
|
}
|
||||||
|
|
||||||
// new websocket connection wrapper
|
// new websocket connection wrapper
|
||||||
func NewWSConnection(wsConn *websocket.Conn) *WSConnection {
|
func NewWSConnection(baseConn *websocket.Conn) *WSConnection {
|
||||||
con := &WSConnection{
|
wsc := &WSConnection{
|
||||||
id: wsConn.RemoteAddr().String(),
|
id: baseConn.RemoteAddr().String(),
|
||||||
wsConn: wsConn,
|
baseConn: baseConn,
|
||||||
writeChan: make(chan WSResponse, WriteChanBufferSize), // buffered. we keep track when its full
|
writeChan: make(chan WSResponse, writeChanCapacity), // error when full.
|
||||||
}
|
}
|
||||||
con.QuitService = *NewQuitService(log, "WSConnection", con)
|
wsc.QuitService = *NewQuitService(log, "WSConnection", wsc)
|
||||||
return con
|
return wsc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (con *WSConnection) OnStart() {
|
// wsc.Start() blocks until the connection closes.
|
||||||
con.QuitService.OnStart()
|
func (wsc *WSConnection) OnStart() {
|
||||||
// read subscriptions/unsubscriptions to events
|
wsc.QuitService.OnStart()
|
||||||
go con.read()
|
|
||||||
// write responses
|
// Read subscriptions/unsubscriptions to events
|
||||||
con.write()
|
go wsc.readRoutine()
|
||||||
|
|
||||||
|
// Custom Ping handler to touch readTimeout
|
||||||
|
wsc.readTimeout = time.NewTimer(time.Second * WSReadTimeoutSeconds)
|
||||||
|
wsc.baseConn.SetPingHandler(func(m string) error {
|
||||||
|
wsc.baseConn.WriteControl(websocket.PongMessage, []byte(m), time.Now().Add(time.Second*WSWriteTimeoutSeconds))
|
||||||
|
wsc.readTimeout.Reset(time.Second * WSReadTimeoutSeconds)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
go wsc.readTimeoutRoutine()
|
||||||
|
|
||||||
|
// Write responses, BLOCKING.
|
||||||
|
wsc.writeRoutine()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (con *WSConnection) OnStop() {
|
func (wsc *WSConnection) OnStop() {
|
||||||
con.QuitService.OnStop()
|
wsc.QuitService.OnStop()
|
||||||
con.evsw.RemoveListener(con.id)
|
wsc.evsw.RemoveListener(wsc.id)
|
||||||
// the write loop closes the websocket connection
|
wsc.readTimeout.Stop()
|
||||||
|
// The write loop closes the websocket connection
|
||||||
// when it exits its loop, and the read loop
|
// when it exits its loop, and the read loop
|
||||||
// closes the writeChan
|
// closes the writeChan
|
||||||
}
|
}
|
||||||
|
|
||||||
func (con *WSConnection) SetEventSwitch(evsw *events.EventSwitch) { con.evsw = evsw }
|
func (wsc *WSConnection) SetEventSwitch(evsw *events.EventSwitch) { wsc.evsw = evsw }
|
||||||
|
|
||||||
// attempt to write response to writeChan and record failures
|
func (wsc *WSConnection) readTimeoutRoutine() {
|
||||||
func (con *WSConnection) safeWrite(resp WSResponse) {
|
|
||||||
select {
|
select {
|
||||||
case con.writeChan <- resp:
|
case <-wsc.readTimeout.C:
|
||||||
// yay
|
log.Notice("Stopping connection due to read timeout")
|
||||||
con.failedSends = 0
|
wsc.Stop()
|
||||||
default:
|
case <-wsc.Quit:
|
||||||
// channel is full
|
return
|
||||||
// if this happens too many times in a row,
|
|
||||||
// close connection
|
|
||||||
con.failedSends += 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// read from the socket and subscribe to or unsubscribe from events
|
// Attempt to write response to writeChan and record failures
|
||||||
func (con *WSConnection) read() {
|
func (wsc *WSConnection) writeResponse(resp WSResponse) {
|
||||||
defer close(con.writeChan)
|
select {
|
||||||
reaper := time.Tick(time.Second * WSConnectionReaperSeconds)
|
case wsc.writeChan <- resp:
|
||||||
|
default:
|
||||||
|
log.Notice("Stopping connection due to writeChan overflow", "id", wsc.id)
|
||||||
|
wsc.Stop() // writeChan capacity exceeded, error.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from the socket and subscribe to or unsubscribe from events
|
||||||
|
func (wsc *WSConnection) readRoutine() {
|
||||||
|
defer close(wsc.writeChan)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
// TODO: this actually doesn't work
|
case <-wsc.Quit:
|
||||||
// since ReadMessage blocks. Really it needs its own
|
return
|
||||||
// go routine
|
|
||||||
case <-reaper:
|
|
||||||
if con.failedSends > MaxFailedSends {
|
|
||||||
// sending has failed too many times.
|
|
||||||
// kill the connection
|
|
||||||
con.Stop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
var in []byte
|
var in []byte
|
||||||
_, in, err := con.wsConn.ReadMessage()
|
// Do not set a deadline here like below:
|
||||||
|
// wsc.baseConn.SetReadDeadline(time.Now().Add(time.Second * WSReadTimeoutSeconds))
|
||||||
|
// The client may not send anything for a while.
|
||||||
|
// We use `readTimeout` to handle read timeouts.
|
||||||
|
_, in, err := wsc.baseConn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Notice("Failed to read from connection", "id", wsc.id)
|
||||||
// an error reading the connection,
|
// an error reading the connection,
|
||||||
// kill the connection
|
// kill the connection
|
||||||
con.Stop()
|
wsc.Stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var req WSRequest
|
var req WSRequest
|
||||||
err = json.Unmarshal(in, &req)
|
err = json.Unmarshal(in, &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errStr := fmt.Sprintf("Error unmarshaling data: %s", err.Error())
|
errStr := fmt.Sprintf("Error unmarshaling data: %s", err.Error())
|
||||||
con.safeWrite(WSResponse{Error: errStr})
|
wsc.writeResponse(WSResponse{Error: errStr})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch req.Type {
|
switch req.Type {
|
||||||
case "subscribe":
|
case "subscribe":
|
||||||
log.Notice("New event subscription", "con id", con.id, "event", req.Event)
|
log.Notice("New event subscription", "id", wsc.id, "event", req.Event)
|
||||||
con.evsw.AddListenerForEvent(con.id, req.Event, func(msg interface{}) {
|
wsc.evsw.AddListenerForEvent(wsc.id, req.Event, func(msg interface{}) {
|
||||||
resp := WSResponse{
|
resp := WSResponse{
|
||||||
Event: req.Event,
|
Event: req.Event,
|
||||||
Data: msg,
|
Data: msg,
|
||||||
}
|
}
|
||||||
con.safeWrite(resp)
|
wsc.writeResponse(resp)
|
||||||
})
|
})
|
||||||
case "unsubscribe":
|
case "unsubscribe":
|
||||||
if req.Event != "" {
|
if req.Event != "" {
|
||||||
con.evsw.RemoveListenerForEvent(req.Event, con.id)
|
wsc.evsw.RemoveListenerForEvent(req.Event, wsc.id)
|
||||||
} else {
|
} else {
|
||||||
con.evsw.RemoveListener(con.id)
|
wsc.evsw.RemoveListener(wsc.id)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
con.safeWrite(WSResponse{Error: "Unknown request type: " + req.Type})
|
wsc.writeResponse(WSResponse{Error: "Unknown request type: " + req.Type})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// receives on a write channel and writes out on the socket
|
// receives on a write channel and writes out on the socket
|
||||||
func (con *WSConnection) write() {
|
func (wsc *WSConnection) writeRoutine() {
|
||||||
defer con.wsConn.Close()
|
defer wsc.baseConn.Close()
|
||||||
n, err := new(int64), new(error)
|
n, err := new(int64), new(error)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case msg := <-con.writeChan:
|
case <-wsc.Quit:
|
||||||
|
return
|
||||||
|
case msg := <-wsc.writeChan:
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
binary.WriteJSON(msg, buf, n, err)
|
binary.WriteJSON(msg, buf, n, err)
|
||||||
if *err != nil {
|
if *err != nil {
|
||||||
log.Error("Failed to marshal WSResponse to JSON", "error", err)
|
log.Error("Failed to marshal WSResponse to JSON", "error", err)
|
||||||
} else {
|
} else {
|
||||||
if err := con.wsConn.WriteMessage(websocket.TextMessage, buf.Bytes()); err != nil {
|
wsc.baseConn.SetWriteDeadline(time.Now().Add(time.Second * WSWriteTimeoutSeconds))
|
||||||
|
if err := wsc.baseConn.WriteMessage(websocket.TextMessage, buf.Bytes()); err != nil {
|
||||||
log.Warn("Failed to write response on websocket", "error", err)
|
log.Warn("Failed to write response on websocket", "error", err)
|
||||||
con.Stop()
|
wsc.Stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case <-con.Quit:
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//----------------------------------------
|
||||||
|
|
||||||
// main manager for all websocket connections
|
// main manager for all websocket connections
|
||||||
// holds the event switch
|
// holds the event switch
|
||||||
type WebsocketManager struct {
|
type WebsocketManager struct {
|
||||||
@ -373,6 +392,7 @@ func NewWebsocketManager(evsw *events.EventSwitch) *WebsocketManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upgrade the request/response (via http.Hijack) and starts the WSConnection.
|
||||||
func (wm *WebsocketManager) websocketHandler(w http.ResponseWriter, r *http.Request) {
|
func (wm *WebsocketManager) websocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
wsConn, err := wm.Upgrade(w, r, nil)
|
wsConn, err := wm.Upgrade(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -385,7 +405,7 @@ func (wm *WebsocketManager) websocketHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
con := NewWSConnection(wsConn)
|
con := NewWSConnection(wsConn)
|
||||||
log.Notice("New websocket connection", "origin", con.id)
|
log.Notice("New websocket connection", "origin", con.id)
|
||||||
con.SetEventSwitch(wm.evsw)
|
con.SetEventSwitch(wm.evsw)
|
||||||
con.Start()
|
con.Start() // Blocking
|
||||||
}
|
}
|
||||||
|
|
||||||
// rpc.websocket
|
// rpc.websocket
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/tendermint/tendermint/Godeps/_workspace/src/github.com/gorilla/websocket"
|
"github.com/tendermint/tendermint/Godeps/_workspace/src/github.com/gorilla/websocket"
|
||||||
"github.com/tendermint/tendermint/binary"
|
"github.com/tendermint/tendermint/binary"
|
||||||
_ "github.com/tendermint/tendermint/config/tendermint_test"
|
_ "github.com/tendermint/tendermint/config/tendermint_test"
|
||||||
|
"github.com/tendermint/tendermint/rpc/server"
|
||||||
"github.com/tendermint/tendermint/rpc/types"
|
"github.com/tendermint/tendermint/rpc/types"
|
||||||
"github.com/tendermint/tendermint/types"
|
"github.com/tendermint/tendermint/types"
|
||||||
)
|
)
|
||||||
@ -55,13 +56,32 @@ func unsubscribe(t *testing.T, con *websocket.Conn, eventid string) {
|
|||||||
// wait for an event; do things that might trigger events, and check them when they are received
|
// wait for an event; do things that might trigger events, and check them when they are received
|
||||||
func waitForEvent(t *testing.T, con *websocket.Conn, eventid string, dieOnTimeout bool, f func(), check func(string, []byte) error) {
|
func waitForEvent(t *testing.T, con *websocket.Conn, eventid string, dieOnTimeout bool, f func(), check func(string, []byte) error) {
|
||||||
// go routine to wait for webscoket msg
|
// go routine to wait for webscoket msg
|
||||||
gch := make(chan []byte) // good channel
|
goodCh := make(chan []byte)
|
||||||
ech := make(chan error) // error channel
|
errCh := make(chan error)
|
||||||
|
quitCh := make(chan struct{})
|
||||||
|
defer close(quitCh)
|
||||||
|
|
||||||
|
// Write pings repeatedly
|
||||||
|
// TODO: Maybe move this out to something that manages the con?
|
||||||
|
go func() {
|
||||||
|
pingTicker := time.NewTicker((time.Second * rpcserver.WSReadTimeoutSeconds) / 2)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-quitCh:
|
||||||
|
pingTicker.Stop()
|
||||||
|
return
|
||||||
|
case <-pingTicker.C:
|
||||||
|
con.WriteControl(websocket.PingMessage, []byte("whatevs"), time.Now().Add(time.Second))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Read message
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
_, p, err := con.ReadMessage()
|
_, p, err := con.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ech <- err
|
errCh <- err
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
// if the event id isnt what we're waiting on
|
// if the event id isnt what we're waiting on
|
||||||
@ -70,11 +90,11 @@ func waitForEvent(t *testing.T, con *websocket.Conn, eventid string, dieOnTimeou
|
|||||||
Event string `json:"event"`
|
Event string `json:"event"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(p, &response); err != nil {
|
if err := json.Unmarshal(p, &response); err != nil {
|
||||||
ech <- err
|
errCh <- err
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if response.Event == eventid {
|
if response.Event == eventid {
|
||||||
gch <- p
|
goodCh <- p
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,17 +104,17 @@ func waitForEvent(t *testing.T, con *websocket.Conn, eventid string, dieOnTimeou
|
|||||||
// do stuff (transactions)
|
// do stuff (transactions)
|
||||||
f()
|
f()
|
||||||
|
|
||||||
// wait for an event or 10 seconds
|
// wait for an event or timeout
|
||||||
ticker := time.Tick(10 * time.Second)
|
timeout := time.NewTimer(10 * time.Second)
|
||||||
select {
|
select {
|
||||||
case <-ticker:
|
case <-timeout.C:
|
||||||
if dieOnTimeout {
|
if dieOnTimeout {
|
||||||
con.Close()
|
con.Close()
|
||||||
t.Fatalf("%s event was not received in time", eventid)
|
t.Fatalf("%s event was not received in time", eventid)
|
||||||
}
|
}
|
||||||
// else that's great, we didn't hear the event
|
// else that's great, we didn't hear the event
|
||||||
// and we shouldn't have
|
// and we shouldn't have
|
||||||
case p := <-gch:
|
case p := <-goodCh:
|
||||||
if dieOnTimeout {
|
if dieOnTimeout {
|
||||||
// message was received and expected
|
// message was received and expected
|
||||||
// run the check
|
// run the check
|
||||||
@ -106,7 +126,7 @@ func waitForEvent(t *testing.T, con *websocket.Conn, eventid string, dieOnTimeou
|
|||||||
con.Close()
|
con.Close()
|
||||||
t.Fatalf("%s event was not expected", eventid)
|
t.Fatalf("%s event was not expected", eventid)
|
||||||
}
|
}
|
||||||
case err := <-ech:
|
case err := <-errCh:
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user