MM-57152: Get webconn count from the whole cluster (#26813)

We were setting the user status to offline without
checking for connections on other nodes in a cluster.

Now we implement a request-response mechanism for the whole
cluster and we check that before setting a user to offline.

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

```release-note
Fix a bug where the user status would incorrectly be set to offline
without checking for connections in other nodes in an HA cluster.
```

Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
Этот коммит содержится в:
Agniva De Sarker
2024-04-30 19:28:55 +05:30
коммит произвёл GitHub
родитель 5c11de1373
Коммит 0cee332001
9 изменённых файлов: 153 добавлений и 12 удалений

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

@@ -151,3 +151,6 @@ func (c *ClusterMock) ConfigChanged(previousConfig *model.Config, newConfig *mod
return nil
}
func (c *ClusterMock) HealthScore() int { return 0 }
func (c *ClusterMock) WebConnCountForUser(userID string) (int, *model.AppError) {
return 0, nil
}

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

@@ -149,3 +149,6 @@ func (c *ClusterMock) ConfigChanged(previousConfig *model.Config, newConfig *mod
return nil
}
func (c *ClusterMock) HealthScore() int { return 0 }
func (c *ClusterMock) WebConnCountForUser(userID string) (int, *model.AppError) {
return 0, nil
}

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

@@ -372,17 +372,6 @@ func (wc *WebConn) isSet(val string) bool {
return val != UnsetPresenceIndicator
}
// areAllInactive returns whether all of the connections
// are inactive or not.
func areAllInactive(conns []*WebConn) bool {
for _, conn := range conns {
if conn.active.Load() {
return false
}
}
return true
}
// GetSession returns the session of the connection.
func (wc *WebConn) GetSession() *model.Session {
return wc.session.Load()

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

@@ -50,6 +50,11 @@ type webConnCheckMessage struct {
result chan *CheckConnResult
}
type webConnCountMessage struct {
userID string
result chan int
}
// Hub is the central place to manage all websocket connections in the server.
// It handles different websocket events and sending messages to individual
// user connections.
@@ -70,6 +75,7 @@ type Hub struct {
explicitStop bool
checkRegistered chan *webConnSessionMessage
checkConn chan *webConnCheckMessage
connCount chan *webConnCountMessage
broadcastHooks map[string]BroadcastHook
}
@@ -87,6 +93,7 @@ func newWebHub(ps *PlatformService) *Hub {
directMsg: make(chan *webConnDirectMessage),
checkRegistered: make(chan *webConnSessionMessage),
checkConn: make(chan *webConnCheckMessage),
connCount: make(chan *webConnCountMessage),
}
}
@@ -236,6 +243,16 @@ func (ps *PlatformService) CheckWebConn(userID, connectionID string) *CheckConnR
return nil
}
// WebConnCountForUser returns the number of active websocket connections
// for a given userID.
func (ps *PlatformService) WebConnCountForUser(userID string) int {
hub := ps.GetHubForUserId(userID)
if hub != nil {
return hub.WebConnCountForUser(userID)
}
return 0
}
// Register registers a connection to the hub.
func (h *Hub) Register(webConn *WebConn) {
select {
@@ -281,6 +298,19 @@ func (h *Hub) CheckConn(userID, connectionID string) *CheckConnResult {
return nil
}
func (h *Hub) WebConnCountForUser(userID string) int {
req := &webConnCountMessage{
userID: userID,
result: make(chan int),
}
select {
case h.connCount <- req:
return <-req.result
case <-h.stop:
}
return 0
}
// Broadcast broadcasts the message to all connections in the hub.
func (h *Hub) Broadcast(message *model.WebSocketEvent) {
// XXX: The hub nil check is because of the way we setup our tests. We call
@@ -381,6 +411,8 @@ func (h *Hub) Start() {
}
}
req.result <- res
case req := <-h.connCount:
req.result <- connIndex.ForUserActiveCount(req.userID)
case <-ticker.C:
connIndex.RemoveInactiveConnections()
case webConn := <-h.register:
@@ -420,7 +452,26 @@ func (h *Hub) Start() {
if len(conns) == 0 || areAllInactive(conns) {
userID := webConn.UserId
h.platform.Go(func() {
h.platform.SetStatusOffline(userID, false)
// If this is an HA setup, get count for this user
// from other nodes.
var clusterCnt int
var appErr *model.AppError
if h.platform.Cluster() != nil {
clusterCnt, appErr = h.platform.Cluster().WebConnCountForUser(userID)
}
if appErr != nil {
mlog.Error("Error in trying to get the webconn count from cluster", mlog.Err(appErr))
// We take a conservative approach
// and do not set status to offline in case
// there's an error, rather than potentially
// incorrectly setting status to offline.
return
}
// Only set to offline if there are no
// active connections in other nodes as well.
if clusterCnt == 0 {
h.platform.SetStatusOffline(userID, false)
}
})
continue
}
@@ -552,6 +603,17 @@ func (h *Hub) Start() {
go doRecoverableStart()
}
// areAllInactive returns whether all of the connections
// are inactive or not.
func areAllInactive(conns []*WebConn) bool {
for _, conn := range conns {
if conn.active.Load() {
return false
}
}
return true
}
// hubConnectionIndex provides fast addition, removal, and iteration of web connections.
// It requires 3 functionalities which need to be very fast:
// - check if a connection exists or not.
@@ -629,6 +691,17 @@ func (i *hubConnectionIndex) ForUser(id string) []*WebConn {
return conns
}
// ForUserActiveCount returns the number of active connections for a userID
func (i *hubConnectionIndex) ForUserActiveCount(id string) int {
cnt := 0
for _, conn := range i.ForUser(id) {
if conn.active.Load() {
cnt++
}
}
return cnt
}
// ForConnection returns the connection from its ID.
func (i *hubConnectionIndex) ForConnection(id string) *WebConn {
return i.byConnectionId[id]

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

@@ -291,6 +291,7 @@ func TestHubConnIndex(t *testing.T) {
assert.ElementsMatch(t, connIndex.ForUser(wc2.UserId), []*WebConn{wc2, wc4})
assert.ElementsMatch(t, connIndex.ForUser(wc1.UserId), []*WebConn{})
assert.Len(t, connIndex.ForUser(wc1.UserId), 0)
assert.Len(t, connIndex.All(), 2)
assert.False(t, connIndex.Has(wc1))
assert.True(t, connIndex.Has(wc2))
@@ -454,7 +455,9 @@ func TestHubConnIndexInactive(t *testing.T) {
connIndex.Add(wc3)
assert.Nil(t, connIndex.RemoveInactiveByConnectionID(wc2.UserId, "conn2"))
assert.Equal(t, connIndex.ForUserActiveCount(wc2.UserId), 1)
assert.NotNil(t, connIndex.RemoveInactiveByConnectionID(wc2.UserId, "conn3"))
assert.Equal(t, connIndex.ForUserActiveCount(wc2.UserId), 1)
assert.Nil(t, connIndex.RemoveInactiveByConnectionID(wc1.UserId, "conn3"))
assert.False(t, connIndex.Has(wc3))
assert.Len(t, connIndex.ForUser(wc2.UserId), 1)
@@ -464,12 +467,14 @@ func TestHubConnIndexInactive(t *testing.T) {
connIndex.RemoveInactiveConnections()
assert.True(t, connIndex.Has(wc3))
assert.Len(t, connIndex.ForUser(wc2.UserId), 2)
assert.Equal(t, connIndex.ForUserActiveCount(wc2.UserId), 1)
assert.Len(t, connIndex.All(), 3)
wc3.lastUserActivityAt = model.GetMillis() - (time.Minute).Milliseconds()
connIndex.RemoveInactiveConnections()
assert.False(t, connIndex.Has(wc3))
assert.Len(t, connIndex.ForUser(wc2.UserId), 1)
assert.Equal(t, connIndex.ForUserActiveCount(wc2.UserId), 1)
assert.Len(t, connIndex.All(), 2)
}
@@ -543,6 +548,35 @@ func TestHubIsRegistered(t *testing.T) {
assert.False(t, th.Service.SessionIsRegistered(*session4))
}
func TestHubWebConnCount(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
session, err := th.Service.CreateSession(th.Context, &model.Session{
UserId: th.BasicUser.Id,
})
require.NoError(t, err)
mockSuite := &platform_mocks.SuiteIFace{}
mockSuite.On("GetSession", session.Token).Return(session, nil)
th.Suite = mockSuite
s := httptest.NewServer(dummyWebsocketHandler(t))
defer s.Close()
th.Service.Start(nil)
wc1 := registerDummyWebConn(t, th, s.Listener.Addr(), session)
wc2 := registerDummyWebConn(t, th, s.Listener.Addr(), session)
defer wc1.Close()
assert.Equal(t, 2, th.Service.WebConnCountForUser(th.BasicUser.Id))
wc2.Close()
assert.Equal(t, 1, th.Service.WebConnCountForUser(th.BasicUser.Id))
assert.Equal(t, 0, th.Service.WebConnCountForUser("none"))
}
// Always run this with -benchtime=0.1s
// See: https://github.com/golang/go/issues/27217.
func BenchmarkHubConnIndex(b *testing.B) {

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

@@ -103,3 +103,7 @@ func (c *FakeClusterInterface) ClearMessages() {
defer c.mut.Unlock()
c.messages = nil
}
func (c *FakeClusterInterface) WebConnCountForUser(userID string) (int, *model.AppError) {
return 0, nil
}

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

@@ -29,4 +29,7 @@ type ClusterInterface interface {
QueryLogs(page, perPage int) (map[string][]string, *model.AppError)
GetPluginStatuses() (model.PluginStatuses, *model.AppError)
ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError
// WebConnCountForUser returns the number of active webconn connections
// for a given userID.
WebConnCountForUser(userID string) (int, *model.AppError)
}

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

@@ -301,6 +301,36 @@ func (_m *ClusterInterface) StopInterNodeCommunication() {
_m.Called()
}
// WebConnCountForUser provides a mock function with given fields: userID
func (_m *ClusterInterface) WebConnCountForUser(userID string) (int, *model.AppError) {
ret := _m.Called(userID)
if len(ret) == 0 {
panic("no return value specified for WebConnCountForUser")
}
var r0 int
var r1 *model.AppError
if rf, ok := ret.Get(0).(func(string) (int, *model.AppError)); ok {
return rf(userID)
}
if rf, ok := ret.Get(0).(func(string) int); ok {
r0 = rf(userID)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func(string) *model.AppError); ok {
r1 = rf(userID)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// NewClusterInterface creates a new instance of ClusterInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewClusterInterface(t interface {

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

@@ -51,6 +51,8 @@ const (
ClusterGossipEventResponseGetPluginStatuses = "gossip_response_plugin_statuses"
ClusterGossipEventRequestSaveConfig = "gossip_request_save_config"
ClusterGossipEventResponseSaveConfig = "gossip_response_save_config"
ClusterGossipEventRequestWebConnCount = "gossip_request_webconn_count"
ClusterGossipEventResponseWebConnCount = "gossip_response_webconn_count"
// SendTypes for ClusterMessage.
ClusterSendBestEffort = "best_effort"