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 ```
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
37d97e8024
Коммит
bd8774bdce
@@ -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()
|
||||
|
||||
Ссылка в новой задаче
Block a user