MM-61130: Use a channelMember map at web_hub level (#28810)

Tests at very high scale indicates that the iteration
of all connections during websocket broadcast starts
to become a bottleneck.

To optimize this, we move the channelMember cache from
inside web_conn.go to the hubConnectionIndex.

This involves adding a new map keyed by the channelID
and containing all webConns where the user is a member
of that channel. Subsequently, a new method needed to
be added to invalidate the cache which previously
used to happen in web_conn.

And as a last step, we remove the cache from web_conn
to reduce SQL queries to the DB.

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

```release-note
NONE
```
Этот коммит содержится в:
Agniva De Sarker
2024-11-08 09:57:54 +05:30
коммит произвёл GitHub
родитель 37d97e8024
Коммит bd8774bdce
11 изменённых файлов: 564 добавлений и 329 удалений

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

@@ -16,6 +16,7 @@ import (
"reflect"
"sort"
"strings"
"sync"
"testing"
"time"
@@ -27,6 +28,7 @@ import (
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/app"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
"github.com/mattermost/mattermost/server/v8/channels/testlib"
"github.com/mattermost/mattermost/server/v8/channels/utils"
"github.com/mattermost/mattermost/server/v8/channels/utils/testutils"
)
@@ -2937,6 +2939,155 @@ func TestPermanentDeletePost(t *testing.T) {
})
}
func TestWebHubMembership(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
u1 := th.CreateUser()
th.LinkUserToTeam(u1, th.BasicTeam)
th.AddUserToChannel(u1, th.BasicChannel)
ch2 := th.CreatePrivateChannel()
u2 := th.CreateUser()
th.LinkUserToTeam(u2, th.BasicTeam)
th.AddUserToChannel(u2, ch2)
quitChan := make(chan struct{})
var wg sync.WaitGroup
wg.Add(3)
for _, obj := range []struct {
testName string
user *model.User
}{
{
testName: "basicUser",
user: th.BasicUser,
},
{
testName: "u1",
user: u1,
},
{
testName: "u2",
user: u2,
},
} {
cli := th.CreateClient()
_, _, err := cli.Login(context.Background(), obj.user.Username, obj.user.Password)
require.NoError(t, err)
wsClient, err := th.CreateWebSocketClientWithClient(cli)
require.NoError(t, err)
defer wsClient.Close()
wsClient.Listen()
go func(testName string) {
defer wg.Done()
var cnt int
for {
select {
case event := <-wsClient.EventChannel:
if event.EventType() == model.WebsocketEventPosted {
var post model.Post
err := json.Unmarshal([]byte(event.GetData()["post"].(string)), &post)
require.NoError(t, err)
cnt++
// Cases:
// Post to basicChannel should go to u1 and basicUser.
// Add u1 to ch2.
// Post to ch2 should go to u1, u2 and basicUser.
// Remove u1 from ch2.
// Post to ch2 should go to u2 and basicUser.
switch testName {
case "basicUser":
if cnt == 1 {
assert.Equal(t, th.BasicChannel.Id, post.ChannelId)
} else if cnt == 2 {
assert.Equal(t, ch2.Id, post.ChannelId)
} else if cnt == 3 {
// After removing, there will be a "removed from channel post"
assert.Equal(t, ch2.Id, post.ChannelId)
} else if cnt == 4 {
assert.Equal(t, ch2.Id, post.ChannelId)
} else {
assert.Fail(t, "more than 4 messages arrived for basicUser")
}
case "u1":
// First msg should be from basicChannel
if cnt == 1 {
assert.Equal(t, th.BasicChannel.Id, post.ChannelId)
} else if cnt == 2 {
// second should be from ch2
assert.Equal(t, ch2.Id, post.ChannelId)
} else {
assert.Fail(t, "more than 2 messages arrived for u1")
}
case "u2":
if cnt == 1 {
assert.Equal(t, ch2.Id, post.ChannelId)
} else if cnt == 2 {
// After removing, there will be a "removed from channel post"
assert.Equal(t, ch2.Id, post.ChannelId)
} else if cnt == 3 {
assert.Equal(t, ch2.Id, post.ChannelId)
} else {
assert.Fail(t, "more than 3 messages arrived for u2")
}
}
}
case <-quitChan:
return
}
}
}(obj.testName)
}
// Will send to basic channel
th.CreatePost()
// Add u1 to ch2
th.AddUserToChannel(u1, ch2)
// Send post to ch2
th.CreatePostWithClient(th.Client, ch2)
// Remove u1 from ch2
th.RemoveUserFromChannel(u1, ch2)
// Send post to ch2
th.CreatePostWithClient(th.Client, ch2)
// It is possible to create a signalling mechanism from the goroutines
// after all events are received, but we also want to verify that no additional
// events are being sent.
time.Sleep(2 * time.Second)
close(quitChan)
wg.Wait()
}
func TestWebHubCloseConnOnDBFail(t *testing.T) {
th := Setup(t).InitBasic()
defer func() {
th.TearDown()
// Asserting that the error message is present in the log
testlib.AssertLog(t, th.LogBuffer, mlog.LvlError.Name, "Error while registering to hub")
_, err := th.Server.Store().GetInternalMasterDB().Exec(`ALTER TABLE dummy RENAME to ChannelMembers`)
require.NoError(t, err)
}()
cli := th.CreateClient()
_, _, err := cli.Login(context.Background(), th.BasicUser.Username, th.BasicUser.Password)
require.NoError(t, err)
_, err = th.Server.Store().GetInternalMasterDB().Exec(`ALTER TABLE ChannelMembers RENAME to dummy`)
require.NoError(t, err)
wsClient, err := th.CreateWebSocketClientWithClient(cli)
require.NoError(t, err)
defer wsClient.Close()
require.NoError(t, th.TestLogger.Flush())
}
func TestDeletePostEvent(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()

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

@@ -70,7 +70,7 @@ func connectWebSocket(c *Context, w http.ResponseWriter, r *http.Request) {
} else {
cfg, err = c.App.Srv().Platform().PopulateWebConnConfig(c.AppContext.Session(), cfg, r.URL.Query().Get(sequenceNumberParam))
if err != nil {
c.Logger.Warn("Error while populating webconn config", mlog.String("id", r.URL.Query().Get(connectionIDParam)), mlog.Err(err))
c.Logger.Error("Error while populating webconn config", mlog.String("id", r.URL.Query().Get(connectionIDParam)), mlog.Err(err))
ws.Close()
return
}
@@ -78,7 +78,12 @@ func connectWebSocket(c *Context, w http.ResponseWriter, r *http.Request) {
wc := c.App.Srv().Platform().NewWebConn(cfg, c.App, c.App.Srv().Channels())
if c.AppContext.Session().UserId != "" {
c.App.Srv().Platform().HubRegister(wc)
err = c.App.Srv().Platform().HubRegister(wc)
if err != nil {
c.Logger.Error("Error while registering to hub", mlog.String("id", r.URL.Query().Get(connectionIDParam)), mlog.Err(err))
ws.Close()
return
}
}
wc.Pump()