Skip to content

Commit 8307952

Browse files
committed
chore(memory): bound amount of requests in flight on WS connection
1 parent f7bfc3b commit 8307952

2 files changed

Lines changed: 88 additions & 7 deletions

File tree

server/public_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,6 +1353,63 @@ func Test_WebsocketRejectsOversizedMessage(t *testing.T) {
13531353
t.Fatalf("unexpected websocket error after oversized message: %v", err)
13541354
}
13551355

1356+
func Test_WebsocketClosesWhenPendingRequestLimitExceeded(t *testing.T) {
1357+
parser, chain := setupChain(t)
1358+
1359+
s, dbpath := setupPublicHTTPServer(parser, chain, t, false)
1360+
defer closeAndDestroyPublicServer(t, s, dbpath)
1361+
s.ConnectFullPublicInterface()
1362+
1363+
releaseRequests := make(chan struct{})
1364+
defer close(releaseRequests)
1365+
startedRequests := make(chan struct{}, maxWebsocketPendingRequests)
1366+
originalPingHandler := requestHandlers["ping"]
1367+
requestHandlers["ping"] = func(s *WebsocketServer, c *websocketChannel, req *WsReq) (interface{}, error) {
1368+
startedRequests <- struct{}{}
1369+
<-releaseRequests
1370+
return struct{}{}, nil
1371+
}
1372+
defer func() {
1373+
requestHandlers["ping"] = originalPingHandler
1374+
}()
1375+
1376+
ts := httptest.NewServer(s.https.Handler)
1377+
defer ts.Close()
1378+
1379+
ws := connectWebsocket(t, ts)
1380+
defer ws.Close()
1381+
1382+
for i := 0; i < maxWebsocketPendingRequests; i++ {
1383+
if err := ws.WriteJSON(websocketReq{ID: strconv.Itoa(i), Method: "ping"}); err != nil {
1384+
t.Fatal(err)
1385+
}
1386+
}
1387+
for i := 0; i < maxWebsocketPendingRequests; i++ {
1388+
select {
1389+
case <-startedRequests:
1390+
case <-time.After(2 * time.Second):
1391+
t.Fatalf("timed out waiting for pending request %d", i)
1392+
}
1393+
}
1394+
1395+
if err := ws.WriteJSON(websocketReq{ID: "overflow", Method: "ping"}); err != nil {
1396+
t.Fatal(err)
1397+
}
1398+
1399+
if err := ws.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
1400+
t.Fatal(err)
1401+
}
1402+
_, _, err := ws.ReadMessage()
1403+
ws.SetReadDeadline(time.Time{})
1404+
if err == nil {
1405+
t.Fatal("expected websocket read error after pending request limit was exceeded")
1406+
}
1407+
var netErr net.Error
1408+
if errors.As(err, &netErr) && netErr.Timeout() {
1409+
t.Fatal("expected connection close after pending request limit was exceeded, got timeout")
1410+
}
1411+
}
1412+
13561413
var websocketTestsBitcoinType = []websocketTest{
13571414
{
13581415
name: "websocket getInfo",

server/websocket.go

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const outChannelSize = 500
2828
const defaultTimeout = 60 * time.Second
2929
const unknownMethodLabel = "unknown"
3030
const maxWebsocketMessageBytes int64 = 4 * 1024 * 1024
31+
const maxWebsocketPendingRequests = 48
3132
const websocketLogPreviewBytes = 256
3233

3334
// allRates is a special "currency" parameter that means all available currencies
@@ -44,6 +45,7 @@ type websocketChannel struct {
4445
id uint64
4546
conn *websocket.Conn
4647
out chan *WsRes
48+
pendingRequests chan struct{}
4749
ip string
4850
requestHeader http.Header
4951
alive bool
@@ -221,12 +223,13 @@ func (s *WebsocketServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
221223
}
222224
conn.SetReadLimit(maxWebsocketMessageBytes)
223225
c := &websocketChannel{
224-
id: atomic.AddUint64(&connectionCounter, 1),
225-
conn: conn,
226-
out: make(chan *WsRes, outChannelSize),
227-
ip: getIP(r),
228-
requestHeader: r.Header,
229-
alive: true,
226+
id: atomic.AddUint64(&connectionCounter, 1),
227+
conn: conn,
228+
out: make(chan *WsRes, outChannelSize),
229+
pendingRequests: make(chan struct{}, maxWebsocketPendingRequests),
230+
ip: getIP(r),
231+
requestHeader: r.Header,
232+
alive: true,
230233
}
231234
if s.is.WsGetAccountInfoLimit > 0 {
232235
c.getAddressInfoDescriptors = make(map[string]struct{})
@@ -290,6 +293,19 @@ func (c *websocketChannel) DataOut(data *WsRes) {
290293
}
291294
}
292295

296+
func (c *websocketChannel) acquireRequestSlot() bool {
297+
select {
298+
case c.pendingRequests <- struct{}{}:
299+
return true
300+
default:
301+
return false
302+
}
303+
}
304+
305+
func (c *websocketChannel) releaseRequestSlot() {
306+
<-c.pendingRequests
307+
}
308+
293309
func (s *WebsocketServer) inputLoop(c *websocketChannel) {
294310
defer func() {
295311
if r := recover(); r != nil {
@@ -313,7 +329,15 @@ func (s *WebsocketServer) inputLoop(c *websocketChannel) {
313329
s.closeChannel(c, "protocol_error")
314330
return
315331
}
316-
go s.onRequest(c, &req)
332+
if !c.acquireRequestSlot() {
333+
glog.Warning("Client ", c.id, " exceeded pending websocket request limit, ", c.ip)
334+
s.closeChannel(c, "pending_requests_limit")
335+
return
336+
}
337+
go func(req WsReq) {
338+
defer c.releaseRequestSlot()
339+
s.onRequest(c, &req)
340+
}(req)
317341
case websocket.BinaryMessage:
318342
glog.Error("Binary message received from ", c.id, ", ", c.ip)
319343
s.closeChannel(c, "protocol_error")

0 commit comments

Comments
 (0)