MM-32950: Reliable WebSockets: Basic single server (#17406)

* MM-32950: Reliable WebSockets: Basic single server

This PR adds reliable websocket support for a single server.

Below is a brief overview of the three states of a connection:

Normal:
- All messages are routed via web hub.
- Each web conn has a send queue to which it gets pushed.
- A message gets pulled from the queue, and before it
gets written to the wire, it is added to the dead queue.

Disconnect:
- Hub Unregister gets called, where the connection is just
marked as inactive. And new messages keep getting pushed
to the send queue.

If it gets full, the channel is closed and the conn gets removed
from conn index.

Reconnect:
- We query the hub for the connection ID, and get back the
queues.
- We construct a WebConn reusing the old queues, or a fresh one
depending on whether the connection ID was found or not.
- Now there is a tricky bit here which needs to be carefully processed.
On register, we would always send the hello message in the send queue.
But we cannot do that now because the send queue might already have messages.

Therefore, we don't send the hello message from web hub, if we reuse a connection.

Instead, we move that logic to the web conn write pump. We check if
the sequence number is in dead queue, and if it is, then we drain
the dead queue, and start consuming from the active queue.
No hello message is sent here.

But if the message does not exist in the dead queue, and the sequence number
is actually something that should have existed, then we set
a new connction id and clear the dead queue, and send a hello message.
The client, on receiving a new connection id will automatically
set its sequence number to 0, and make the sync API calls to manage
any lost data.

https://mattermost.atlassian.net/browse/MM-32590

```release-note
NONE
```

* gofmt

* Add EnableReliableWebSockets to the client config

* Refactoring isInDeadQueue

* Passing index to drainDeadQueue

* refactoring webconn

* fix pointer

* review comments

* simplify hasMsgLoss

* safety comment

* fix test

* Trigger CI

* Trigger CI

Co-authored-by: Devin Binnie <devin.binnie@mattermost.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
Agniva De Sarker
2021-04-26 19:51:25 +05:30
коммит произвёл GitHub
родитель b0279a432d
Коммит cd4d322e4a
8 изменённых файлов: 718 добавлений и 114 удалений

Просмотреть файл

@@ -4,6 +4,10 @@
package app
import (
"bytes"
"net"
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/websocket"
@@ -100,17 +104,15 @@ func TestWebConnShouldSendEvent(t *testing.T) {
assert.False(t, basicUserWc.shouldSendEvent(event3))
}
func TestWebConnDeadQueue(t *testing.T) {
func TestWebConnAddDeadQueue(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableReliableWebSockets = true })
session := model.Session{
Id: model.NewId(),
}
wc := th.App.NewWebConn(&websocket.Conn{}, session, nil, "")
wc := th.App.NewWebConn(&WebConnConfig{
WebSocket: &websocket.Conn{},
})
for i := 0; i < 2; i++ {
msg := &model.WebSocketEvent{}
@@ -119,7 +121,7 @@ func TestWebConnDeadQueue(t *testing.T) {
}
for i := 0; i < 2; i++ {
assert.Equal(t, int64(i), wc.deadQueue[i].(*model.WebSocketEvent).GetSequence())
assert.Equal(t, int64(i), wc.deadQueue[i].GetSequence())
}
// Should push out the first two elements
@@ -129,6 +131,171 @@ func TestWebConnDeadQueue(t *testing.T) {
wc.addToDeadQueue(msg)
}
for i := 0; i < deadQueueSize; i++ {
assert.Equal(t, int64(i+2), wc.deadQueue[(i+2)%deadQueueSize].(*model.WebSocketEvent).GetSequence())
assert.Equal(t, int64(i+2), wc.deadQueue[(i+2)%deadQueueSize].GetSequence())
}
}
func TestWebConnIsInDeadQueue(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableReliableWebSockets = true
})
wc := th.App.NewWebConn(&WebConnConfig{
WebSocket: &websocket.Conn{},
})
var i int
for ; i < 2; i++ {
msg := &model.WebSocketEvent{}
msg = msg.SetSequence(int64(i))
wc.addToDeadQueue(msg)
}
wc.Sequence = int64(0)
ok, ind := wc.isInDeadQueue(wc.Sequence)
assert.True(t, ok)
assert.Equal(t, 0, ind)
assert.True(t, wc.hasMsgLoss())
wc.Sequence = int64(1)
ok, ind = wc.isInDeadQueue(wc.Sequence)
assert.True(t, ok)
assert.Equal(t, 1, ind)
assert.True(t, wc.hasMsgLoss())
wc.Sequence = int64(2)
ok, ind = wc.isInDeadQueue(wc.Sequence)
assert.False(t, ok)
assert.Equal(t, 0, ind)
assert.False(t, wc.hasMsgLoss())
for ; i < deadQueueSize+2; i++ {
msg := &model.WebSocketEvent{}
msg = msg.SetSequence(int64(i))
wc.addToDeadQueue(msg)
}
wc.Sequence = int64(129)
ok, ind = wc.isInDeadQueue(wc.Sequence)
assert.True(t, ok)
assert.Equal(t, 1, ind)
wc.Sequence = int64(128)
ok, ind = wc.isInDeadQueue(wc.Sequence)
assert.True(t, ok)
assert.Equal(t, 0, ind)
wc.Sequence = int64(2)
ok, ind = wc.isInDeadQueue(wc.Sequence)
assert.True(t, ok)
assert.Equal(t, 2, ind)
assert.True(t, wc.hasMsgLoss())
wc.Sequence = int64(0)
ok, ind = wc.isInDeadQueue(wc.Sequence)
assert.False(t, ok)
assert.Equal(t, 0, ind)
wc.Sequence = int64(130)
ok, ind = wc.isInDeadQueue(wc.Sequence)
assert.False(t, ok)
assert.Equal(t, 0, ind)
assert.False(t, wc.hasMsgLoss())
}
func TestWebConnDrainDeadQueue(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableReliableWebSockets = true
})
var dialConn = func(t *testing.T, a *App, addr net.Addr) *WebConn {
d := websocket.Dialer{}
c, _, err := d.Dial("ws://"+addr.String()+"/ws", nil)
require.NoError(t, err)
cfg := &WebConnConfig{
WebSocket: c,
}
return a.NewWebConn(cfg)
}
t.Run("Empty Queue", func(t *testing.T) {
var handler = func(t *testing.T) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
upgrader := &websocket.Upgrader{}
conn, err := upgrader.Upgrade(w, req, nil)
cnt := 0
for err == nil {
_, _, err = conn.ReadMessage()
cnt++
}
assert.Equal(t, 1, cnt)
if _, ok := err.(*websocket.CloseError); !ok {
require.NoError(t, err)
}
}
}
s := httptest.NewServer(handler(t))
defer s.Close()
wc := dialConn(t, th.App, s.Listener.Addr())
defer wc.WebSocket.Close()
wc.clearDeadQueue()
err := wc.drainDeadQueue(0)
require.NoError(t, err)
})
var handler = func(t *testing.T, seqNum int64, limit int) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
upgrader := &websocket.Upgrader{}
conn, err := upgrader.Upgrade(w, req, nil)
var buf []byte
i := seqNum
for err == nil {
_, buf, err = conn.ReadMessage()
ev := model.WebSocketEventFromJson(bytes.NewReader(buf))
require.LessOrEqual(t, int(i), limit)
assert.Equal(t, i, ev.Sequence)
i++
}
if _, ok := err.(*websocket.CloseError); !ok {
require.NoError(t, err)
}
}
}
run := func(seqNum int64, limit int) {
s := httptest.NewServer(handler(t, seqNum, limit))
defer s.Close()
wc := dialConn(t, th.App, s.Listener.Addr())
defer wc.WebSocket.Close()
for i := 0; i < limit; i++ {
msg := model.NewWebSocketEvent("", "", "", "", map[string]bool{})
msg = msg.SetSequence(int64(i))
wc.addToDeadQueue(msg)
}
wc.Sequence = seqNum
ok, index := wc.isInDeadQueue(wc.Sequence)
require.True(t, ok)
err := wc.drainDeadQueue(index)
require.NoError(t, err)
}
t.Run("Half-full Queue", func(t *testing.T) {
t.Run("Middle", func(t *testing.T) { run(int64(2), 10) })
t.Run("Beginning", func(t *testing.T) { run(int64(0), 10) })
t.Run("End", func(t *testing.T) { run(int64(9), 10) })
t.Run("Full", func(t *testing.T) { run(int64(deadQueueSize-1), deadQueueSize) })
})
t.Run("Cycled Queue", func(t *testing.T) {
t.Run("First un-overwritten", func(t *testing.T) { run(int64(10), deadQueueSize+10) })
t.Run("End", func(t *testing.T) { run(int64(127), deadQueueSize+10) })
t.Run("Cycled End", func(t *testing.T) { run(int64(137), deadQueueSize+10) })
t.Run("Overwritten First", func(t *testing.T) { run(int64(128), deadQueueSize+10) })
})
}