Files
mostlymatter/app/web_hub.go
Scott Bishel e8081b7a0f MM24459: Implement ldap picture sync (#14540)
* implement ldap picture sync

* add testing timeout, and no change

* Revert "add testing timeout, and no change"

This reverts commit 765621a7290074e5664c4ca2a2843c84e01f4cf1.

* update app-layer

* updates from code review

* update app-layers

* remove debug statements

Co-authored-by: mattermod <mattermod@users.noreply.github.com>
2020-05-19 08:25:52 -06:00

593 строки
16 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"hash/fnv"
"runtime"
"runtime/debug"
"strconv"
"sync/atomic"
"github.com/mattermost/mattermost-server/v5/mlog"
"github.com/mattermost/mattermost-server/v5/model"
)
const (
broadcastQueueSize = 4096
)
type webConnActivityMessage struct {
userId string
sessionToken string
activityAt int64
}
type webConnDirectMessage struct {
conn *WebConn
msg model.WebSocketMessage
}
type webConnSessionMessage struct {
userId string
sessionToken string
isRegistered chan bool
}
// 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.
type Hub struct {
// connectionCount should be kept first.
// See https://github.com/mattermost/mattermost-server/pull/7281
connectionCount int64
app *App
connectionIndex int
register chan *WebConn
unregister chan *WebConn
broadcast chan *model.WebSocketEvent
stop chan struct{}
didStop chan struct{}
invalidateUser chan string
activity chan *webConnActivityMessage
directMsg chan *webConnDirectMessage
explicitStop bool
checkRegistered chan *webConnSessionMessage
}
// NewWebHub creates a new Hub.
func (a *App) NewWebHub() *Hub {
return &Hub{
app: a,
register: make(chan *WebConn),
unregister: make(chan *WebConn),
broadcast: make(chan *model.WebSocketEvent, broadcastQueueSize),
stop: make(chan struct{}),
didStop: make(chan struct{}),
invalidateUser: make(chan string),
activity: make(chan *webConnActivityMessage),
directMsg: make(chan *webConnDirectMessage),
checkRegistered: make(chan *webConnSessionMessage),
}
}
func (a *App) TotalWebsocketConnections() int {
return a.Srv().TotalWebsocketConnections()
}
// HubStart starts all the hubs.
func (a *App) HubStart() {
// Total number of hubs is twice the number of CPUs.
numberOfHubs := runtime.NumCPU() * 2
mlog.Info("Starting websocket hubs", mlog.Int("number_of_hubs", numberOfHubs))
a.Srv().SetHubs(make([]*Hub, numberOfHubs))
for i := 0; i < len(a.Srv().GetHubs()); i++ {
newHub := a.NewWebHub()
newHub.connectionIndex = i
err := a.Srv().SetHub(i, newHub)
if err != nil {
mlog.Warn("Error starting hub", mlog.Err(err), mlog.Int("index", i))
continue
}
newHub.Start()
}
}
func (a *App) PublishSkipClusterSend(message *model.WebSocketEvent) {
if message.GetBroadcast().UserId != "" {
hub := a.GetHubForUserId(message.GetBroadcast().UserId)
if hub != nil {
hub.Broadcast(message)
}
return
}
for _, hub := range a.Srv().GetHubs() {
hub.Broadcast(message)
}
}
func (a *App) invalidateCacheForUserSkipClusterSend(userId string) {
a.Srv().Store.Channel().InvalidateAllChannelMembersForUser(userId)
a.InvalidateWebConnSessionCacheForUser(userId)
}
func (a *App) invalidateCacheForWebhook(webhookId string) {
a.Srv().Store.Webhook().InvalidateWebhookCache(webhookId)
}
func (a *App) InvalidateWebConnSessionCacheForUser(userId string) {
hub := a.GetHubForUserId(userId)
if hub != nil {
hub.InvalidateUser(userId)
}
}
// HubStop stops all the hubs.
func (a *App) HubStop() {
mlog.Info("stopping websocket hub connections")
for _, hub := range a.Srv().GetHubs() {
hub.Stop()
}
a.Srv().SetHubs([]*Hub{})
}
// GetHubForUserId returns the hub for a given user id.
func (a *App) GetHubForUserId(userId string) *Hub {
if len(a.Srv().GetHubs()) == 0 {
return nil
}
hash := fnv.New32a()
hash.Write([]byte(userId))
index := hash.Sum32() % uint32(len(a.Srv().GetHubs()))
hub, err := a.Srv().GetHub(int(index))
if err != nil {
mlog.Warn("Requested hub doesn't exist", mlog.Int("hub_index", int(index)))
return nil
}
return hub
}
// HubRegister registers a connection to a hub.
func (a *App) HubRegister(webConn *WebConn) {
hub := a.GetHubForUserId(webConn.UserId)
if hub != nil {
if metrics := a.Metrics(); metrics != nil {
metrics.IncrementWebSocketBroadcastUsersRegistered(strconv.Itoa(hub.connectionIndex), 1)
}
hub.Register(webConn)
}
}
// HubUnregister unregisters a connection from a hub.
func (a *App) HubUnregister(webConn *WebConn) {
hub := a.GetHubForUserId(webConn.UserId)
if hub != nil {
if metrics := a.Metrics(); metrics != nil {
metrics.DecrementWebSocketBroadcastUsersRegistered(strconv.Itoa(hub.connectionIndex), 1)
}
hub.Unregister(webConn)
}
}
func (a *App) Publish(message *model.WebSocketEvent) {
if metrics := a.Metrics(); metrics != nil {
metrics.IncrementWebsocketEvent(message.EventType())
}
a.PublishSkipClusterSend(message)
if a.Cluster() != nil {
cm := &model.ClusterMessage{
Event: model.CLUSTER_EVENT_PUBLISH,
SendType: model.CLUSTER_SEND_BEST_EFFORT,
Data: message.ToJson(),
}
if message.EventType() == model.WEBSOCKET_EVENT_POSTED ||
message.EventType() == model.WEBSOCKET_EVENT_POST_EDITED ||
message.EventType() == model.WEBSOCKET_EVENT_DIRECT_ADDED ||
message.EventType() == model.WEBSOCKET_EVENT_GROUP_ADDED ||
message.EventType() == model.WEBSOCKET_EVENT_ADDED_TO_TEAM {
cm.SendType = model.CLUSTER_SEND_RELIABLE
}
a.Cluster().SendClusterMessage(cm)
}
}
func (a *App) invalidateCacheForChannel(channel *model.Channel) {
a.Srv().Store.Channel().InvalidateChannel(channel.Id)
a.invalidateCacheForChannelByNameSkipClusterSend(channel.TeamId, channel.Name)
if a.Cluster() != nil {
nameMsg := &model.ClusterMessage{
Event: model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_BY_NAME,
SendType: model.CLUSTER_SEND_BEST_EFFORT,
Props: make(map[string]string),
}
nameMsg.Props["name"] = channel.Name
if channel.TeamId == "" {
nameMsg.Props["id"] = "dm"
} else {
nameMsg.Props["id"] = channel.TeamId
}
a.Cluster().SendClusterMessage(nameMsg)
}
}
func (a *App) invalidateCacheForChannelMembers(channelId string) {
a.Srv().Store.User().InvalidateProfilesInChannelCache(channelId)
a.Srv().Store.Channel().InvalidateMemberCount(channelId)
a.Srv().Store.Channel().InvalidateGuestCount(channelId)
}
func (a *App) invalidateCacheForChannelMembersNotifyProps(channelId string) {
a.invalidateCacheForChannelMembersNotifyPropsSkipClusterSend(channelId)
if a.Cluster() != nil {
msg := &model.ClusterMessage{
Event: model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_MEMBERS_NOTIFY_PROPS,
SendType: model.CLUSTER_SEND_BEST_EFFORT,
Data: channelId,
}
a.Cluster().SendClusterMessage(msg)
}
}
func (a *App) invalidateCacheForChannelMembersNotifyPropsSkipClusterSend(channelId string) {
a.Srv().Store.Channel().InvalidateCacheForChannelMembersNotifyProps(channelId)
}
func (a *App) invalidateCacheForChannelByNameSkipClusterSend(teamId, name string) {
if teamId == "" {
teamId = "dm"
}
a.Srv().Store.Channel().InvalidateChannelByName(teamId, name)
}
func (a *App) invalidateCacheForChannelPosts(channelId string) {
a.Srv().Store.Channel().InvalidatePinnedPostCount(channelId)
a.Srv().Store.Post().InvalidateLastPostTimeCache(channelId)
}
func (a *App) InvalidateCacheForUser(userId string) {
a.invalidateCacheForUserSkipClusterSend(userId)
a.Srv().Store.User().InvalidateProfilesInChannelCacheByUser(userId)
a.Srv().Store.User().InvalidateProfileCacheForUser(userId)
if a.Cluster() != nil {
msg := &model.ClusterMessage{
Event: model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_USER,
SendType: model.CLUSTER_SEND_BEST_EFFORT,
Data: userId,
}
a.Cluster().SendClusterMessage(msg)
}
}
func (a *App) invalidateCacheForUserTeams(userId string) {
a.InvalidateWebConnSessionCacheForUser(userId)
a.Srv().Store.Team().InvalidateAllTeamIdsForUser(userId)
if a.Cluster() != nil {
msg := &model.ClusterMessage{
Event: model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_USER_TEAMS,
SendType: model.CLUSTER_SEND_BEST_EFFORT,
Data: userId,
}
a.Cluster().SendClusterMessage(msg)
}
}
// UpdateWebConnUserActivity sets the LastUserActivityAt of the hub for the given session.
func (a *App) UpdateWebConnUserActivity(session model.Session, activityAt int64) {
hub := a.GetHubForUserId(session.UserId)
if hub != nil {
hub.UpdateActivity(session.UserId, session.Token, activityAt)
}
}
// SessionIsRegistered determines if a specific session has been registered
func (a *App) SessionIsRegistered(session model.Session) bool {
hub := a.GetHubForUserId(session.UserId)
if hub != nil {
return hub.IsRegistered(session.UserId, session.Token)
}
return false
}
// Register registers a connection to the hub.
func (h *Hub) Register(webConn *WebConn) {
select {
case h.register <- webConn:
case <-h.stop:
}
}
// Unregister unregisters a connection from the hub.
func (h *Hub) Unregister(webConn *WebConn) {
select {
case h.unregister <- webConn:
case <-h.stop:
}
}
// Determines if a user's session is registered a connection from the hub.
func (h *Hub) IsRegistered(userId, sessionToken string) bool {
ws := &webConnSessionMessage{
userId: userId,
sessionToken: sessionToken,
isRegistered: make(chan bool),
}
select {
case h.checkRegistered <- ws:
return <-ws.isRegistered
case <-h.stop:
}
return false
}
// 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 `app.NewServer()`
// which returns a server, but only after that, we call `wsapi.Init()` through our FakeApp adapter
// to initialize the hub. But in the `NewServer` call itself, we call `RunOldAppInitialization`
// which directly proceeds to broadcast some messages happily.
// This needs to be fixed once the FakeApp adapter goes away. And possibly, we can look into
// doing hub initialization inside NewServer itself.
if h != nil && message != nil {
if metrics := h.app.Metrics(); metrics != nil {
metrics.IncrementWebSocketBroadcastBufferSize(strconv.Itoa(h.connectionIndex), 1)
}
select {
case h.broadcast <- message:
case <-h.stop:
}
}
}
// InvalidateUser invalidates the cache for the given user.
func (h *Hub) InvalidateUser(userId string) {
select {
case h.invalidateUser <- userId:
case <-h.stop:
}
}
// UpdateActivity sets the LastUserActivityAt field for the connection
// of the user.
func (h *Hub) UpdateActivity(userId, sessionToken string, activityAt int64) {
select {
case h.activity <- &webConnActivityMessage{
userId: userId,
sessionToken: sessionToken,
activityAt: activityAt,
}:
case <-h.stop:
}
}
// SendMessage sends the given message to the given connection.
func (h *Hub) SendMessage(conn *WebConn, msg model.WebSocketMessage) {
select {
case h.directMsg <- &webConnDirectMessage{
conn: conn,
msg: msg,
}:
case <-h.stop:
}
}
// Stop stops the hub.
func (h *Hub) Stop() {
close(h.stop)
<-h.didStop
}
// Start starts the hub.
func (h *Hub) Start() {
var doStart func()
var doRecoverableStart func()
var doRecover func()
doStart = func() {
mlog.Debug("Hub is starting", mlog.Int("index", h.connectionIndex))
connIndex := newHubConnectionIndex()
for {
select {
case webSessionMessage := <-h.checkRegistered:
conns := connIndex.ForUser(webSessionMessage.userId)
var isRegistered bool
for _, item := range conns {
if item.sessionToken.Load().(string) == webSessionMessage.sessionToken {
isRegistered = true
}
}
webSessionMessage.isRegistered <- isRegistered
case webConn := <-h.register:
connIndex.Add(webConn)
atomic.StoreInt64(&h.connectionCount, int64(len(connIndex.All())))
if webConn.IsAuthenticated() {
webConn.send <- webConn.createHelloMessage()
}
case webConn := <-h.unregister:
connIndex.Remove(webConn)
atomic.StoreInt64(&h.connectionCount, int64(len(connIndex.All())))
if len(webConn.UserId) == 0 {
continue
}
conns := connIndex.ForUser(webConn.UserId)
if len(conns) == 0 {
h.app.Srv().Go(func() {
h.app.SetStatusOffline(webConn.UserId, false)
})
continue
}
var latestActivity int64 = 0
for _, conn := range conns {
if conn.lastUserActivityAt > latestActivity {
latestActivity = conn.lastUserActivityAt
}
}
if h.app.IsUserAway(latestActivity) {
h.app.Srv().Go(func() {
h.app.SetStatusLastActivityAt(webConn.UserId, latestActivity)
})
}
case userId := <-h.invalidateUser:
for _, webConn := range connIndex.ForUser(userId) {
webConn.InvalidateCache()
}
case activity := <-h.activity:
for _, webConn := range connIndex.ForUser(activity.userId) {
if webConn.GetSessionToken() == activity.sessionToken {
webConn.lastUserActivityAt = activity.activityAt
}
}
case directMsg := <-h.directMsg:
if !connIndex.Has(directMsg.conn) {
continue
}
select {
case directMsg.conn.send <- directMsg.msg:
default:
mlog.Error("webhub.broadcast: cannot send, closing websocket for user", mlog.String("user_id", directMsg.conn.UserId))
close(directMsg.conn.send)
connIndex.Remove(directMsg.conn)
}
case msg := <-h.broadcast:
if metrics := h.app.Metrics(); metrics != nil {
metrics.DecrementWebSocketBroadcastBufferSize(strconv.Itoa(h.connectionIndex), 1)
}
candidates := connIndex.All()
if msg.GetBroadcast().UserId != "" {
candidates = connIndex.ForUser(msg.GetBroadcast().UserId)
}
msg = msg.PrecomputeJSON()
for _, webConn := range candidates {
if !connIndex.Has(webConn) {
continue
}
if webConn.shouldSendEvent(msg) {
select {
case webConn.send <- msg:
default:
mlog.Error("webhub.broadcast: cannot send, closing websocket for user", mlog.String("user_id", webConn.UserId))
close(webConn.send)
connIndex.Remove(webConn)
}
}
}
case <-h.stop:
for _, webConn := range connIndex.All() {
webConn.Close()
h.app.SetStatusOffline(webConn.UserId, false)
}
h.explicitStop = true
close(h.didStop)
return
}
}
}
doRecoverableStart = func() {
defer doRecover()
doStart()
}
doRecover = func() {
if !h.explicitStop {
if r := recover(); r != nil {
mlog.Error("Recovering from Hub panic.", mlog.Any("panic", r))
} else {
mlog.Error("Webhub stopped unexpectedly. Recovering.")
}
mlog.Error(string(debug.Stack()))
go doRecoverableStart()
}
}
go doRecoverableStart()
}
type hubConnectionIndexIndexes struct {
connections int
connectionsByUserId int
}
// hubConnectionIndex provides fast addition, removal, and iteration of web connections.
type hubConnectionIndex struct {
connections []*WebConn
connectionsByUserId map[string][]*WebConn
connectionIndexes map[*WebConn]*hubConnectionIndexIndexes
}
func newHubConnectionIndex() *hubConnectionIndex {
return &hubConnectionIndex{
connections: make([]*WebConn, 0, model.SESSION_CACHE_SIZE),
connectionsByUserId: make(map[string][]*WebConn),
connectionIndexes: make(map[*WebConn]*hubConnectionIndexIndexes),
}
}
func (i *hubConnectionIndex) Add(wc *WebConn) {
i.connections = append(i.connections, wc)
i.connectionsByUserId[wc.UserId] = append(i.connectionsByUserId[wc.UserId], wc)
i.connectionIndexes[wc] = &hubConnectionIndexIndexes{
connections: len(i.connections) - 1,
connectionsByUserId: len(i.connectionsByUserId[wc.UserId]) - 1,
}
}
func (i *hubConnectionIndex) Remove(wc *WebConn) {
indexes, ok := i.connectionIndexes[wc]
if !ok {
return
}
last := i.connections[len(i.connections)-1]
i.connections[indexes.connections] = last
i.connections = i.connections[:len(i.connections)-1]
i.connectionIndexes[last].connections = indexes.connections
userConnections := i.connectionsByUserId[wc.UserId]
last = userConnections[len(userConnections)-1]
userConnections[indexes.connectionsByUserId] = last
i.connectionsByUserId[wc.UserId] = userConnections[:len(userConnections)-1]
i.connectionIndexes[last].connectionsByUserId = indexes.connectionsByUserId
delete(i.connectionIndexes, wc)
}
func (i *hubConnectionIndex) Has(wc *WebConn) bool {
_, ok := i.connectionIndexes[wc]
return ok
}
func (i *hubConnectionIndex) ForUser(id string) []*WebConn {
return i.connectionsByUserId[id]
}
func (i *hubConnectionIndex) All() []*WebConn {
return i.connections
}