github.com/coincircle/mattermost-server@v4.8.1-0.20180321182714-9d701c704416+incompatible/app/web_hub.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See License.txt for license information. 3 4 package app 5 6 import ( 7 "fmt" 8 "hash/fnv" 9 "runtime" 10 "runtime/debug" 11 "strconv" 12 "strings" 13 "sync/atomic" 14 "time" 15 16 l4g "github.com/alecthomas/log4go" 17 18 "github.com/mattermost/mattermost-server/model" 19 "github.com/mattermost/mattermost-server/utils" 20 ) 21 22 const ( 23 BROADCAST_QUEUE_SIZE = 4096 24 DEADLOCK_TICKER = 15 * time.Second // check every 15 seconds 25 DEADLOCK_WARN = (BROADCAST_QUEUE_SIZE * 99) / 100 // number of buffered messages before printing stack trace 26 ) 27 28 type Hub struct { 29 // connectionCount should be kept first. 30 // See https://github.com/mattermost/mattermost-server/pull/7281 31 connectionCount int64 32 app *App 33 connectionIndex int 34 register chan *WebConn 35 unregister chan *WebConn 36 broadcast chan *model.WebSocketEvent 37 stop chan struct{} 38 didStop chan struct{} 39 invalidateUser chan string 40 ExplicitStop bool 41 goroutineId int 42 } 43 44 func (a *App) NewWebHub() *Hub { 45 return &Hub{ 46 app: a, 47 register: make(chan *WebConn, 1), 48 unregister: make(chan *WebConn, 1), 49 broadcast: make(chan *model.WebSocketEvent, BROADCAST_QUEUE_SIZE), 50 stop: make(chan struct{}), 51 didStop: make(chan struct{}), 52 invalidateUser: make(chan string), 53 ExplicitStop: false, 54 } 55 } 56 57 func (a *App) TotalWebsocketConnections() int { 58 count := int64(0) 59 for _, hub := range a.Hubs { 60 count = count + atomic.LoadInt64(&hub.connectionCount) 61 } 62 63 return int(count) 64 } 65 66 func (a *App) HubStart() { 67 // Total number of hubs is twice the number of CPUs. 68 numberOfHubs := runtime.NumCPU() * 2 69 l4g.Info(utils.T("api.web_hub.start.starting.debug"), numberOfHubs) 70 71 a.Hubs = make([]*Hub, numberOfHubs) 72 a.HubsStopCheckingForDeadlock = make(chan bool, 1) 73 74 for i := 0; i < len(a.Hubs); i++ { 75 a.Hubs[i] = a.NewWebHub() 76 a.Hubs[i].connectionIndex = i 77 a.Hubs[i].Start() 78 } 79 80 go func() { 81 ticker := time.NewTicker(DEADLOCK_TICKER) 82 83 defer func() { 84 ticker.Stop() 85 }() 86 87 for { 88 select { 89 case <-ticker.C: 90 for _, hub := range a.Hubs { 91 if len(hub.broadcast) >= DEADLOCK_WARN { 92 l4g.Error("Hub processing might be deadlock on hub %v goroutine %v with %v events in the buffer", hub.connectionIndex, hub.goroutineId, len(hub.broadcast)) 93 buf := make([]byte, 1<<16) 94 runtime.Stack(buf, true) 95 output := fmt.Sprintf("%s", buf) 96 splits := strings.Split(output, "goroutine ") 97 98 for _, part := range splits { 99 if strings.Contains(part, fmt.Sprintf("%v", hub.goroutineId)) { 100 l4g.Error("Trace for possible deadlock goroutine %v", part) 101 } 102 } 103 } 104 } 105 106 case <-a.HubsStopCheckingForDeadlock: 107 return 108 } 109 } 110 }() 111 } 112 113 func (a *App) HubStop() { 114 l4g.Info(utils.T("api.web_hub.start.stopping.debug")) 115 116 select { 117 case a.HubsStopCheckingForDeadlock <- true: 118 default: 119 l4g.Warn("We appear to have already sent the stop checking for deadlocks command") 120 } 121 122 for _, hub := range a.Hubs { 123 hub.Stop() 124 } 125 126 a.Hubs = []*Hub{} 127 } 128 129 func (a *App) GetHubForUserId(userId string) *Hub { 130 hash := fnv.New32a() 131 hash.Write([]byte(userId)) 132 index := hash.Sum32() % uint32(len(a.Hubs)) 133 return a.Hubs[index] 134 } 135 136 func (a *App) HubRegister(webConn *WebConn) { 137 a.GetHubForUserId(webConn.UserId).Register(webConn) 138 } 139 140 func (a *App) HubUnregister(webConn *WebConn) { 141 a.GetHubForUserId(webConn.UserId).Unregister(webConn) 142 } 143 144 func (a *App) Publish(message *model.WebSocketEvent) { 145 if metrics := a.Metrics; metrics != nil { 146 metrics.IncrementWebsocketEvent(message.Event) 147 } 148 149 a.PublishSkipClusterSend(message) 150 151 if a.Cluster != nil { 152 cm := &model.ClusterMessage{ 153 Event: model.CLUSTER_EVENT_PUBLISH, 154 SendType: model.CLUSTER_SEND_BEST_EFFORT, 155 Data: message.ToJson(), 156 } 157 158 if message.Event == model.WEBSOCKET_EVENT_POSTED || 159 message.Event == model.WEBSOCKET_EVENT_POST_EDITED || 160 message.Event == model.WEBSOCKET_EVENT_DIRECT_ADDED || 161 message.Event == model.WEBSOCKET_EVENT_GROUP_ADDED || 162 message.Event == model.WEBSOCKET_EVENT_ADDED_TO_TEAM { 163 cm.SendType = model.CLUSTER_SEND_RELIABLE 164 } 165 166 a.Cluster.SendClusterMessage(cm) 167 } 168 } 169 170 func (a *App) PublishSkipClusterSend(message *model.WebSocketEvent) { 171 if message.Broadcast.UserId != "" { 172 if len(a.Hubs) != 0 { 173 a.GetHubForUserId(message.Broadcast.UserId).Broadcast(message) 174 } 175 } else { 176 for _, hub := range a.Hubs { 177 hub.Broadcast(message) 178 } 179 } 180 } 181 182 func (a *App) InvalidateCacheForChannel(channel *model.Channel) { 183 a.InvalidateCacheForChannelSkipClusterSend(channel.Id) 184 a.InvalidateCacheForChannelByNameSkipClusterSend(channel.TeamId, channel.Name) 185 186 if a.Cluster != nil { 187 msg := &model.ClusterMessage{ 188 Event: model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL, 189 SendType: model.CLUSTER_SEND_BEST_EFFORT, 190 Data: channel.Id, 191 } 192 193 a.Cluster.SendClusterMessage(msg) 194 195 nameMsg := &model.ClusterMessage{ 196 Event: model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_BY_NAME, 197 SendType: model.CLUSTER_SEND_BEST_EFFORT, 198 Props: make(map[string]string), 199 } 200 201 nameMsg.Props["name"] = channel.Name 202 if channel.TeamId == "" { 203 nameMsg.Props["id"] = "dm" 204 } else { 205 nameMsg.Props["id"] = channel.TeamId 206 } 207 208 a.Cluster.SendClusterMessage(nameMsg) 209 } 210 } 211 212 func (a *App) InvalidateCacheForChannelSkipClusterSend(channelId string) { 213 a.Srv.Store.Channel().InvalidateChannel(channelId) 214 } 215 216 func (a *App) InvalidateCacheForChannelMembers(channelId string) { 217 a.InvalidateCacheForChannelMembersSkipClusterSend(channelId) 218 219 if a.Cluster != nil { 220 msg := &model.ClusterMessage{ 221 Event: model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_MEMBERS, 222 SendType: model.CLUSTER_SEND_BEST_EFFORT, 223 Data: channelId, 224 } 225 a.Cluster.SendClusterMessage(msg) 226 } 227 } 228 229 func (a *App) InvalidateCacheForChannelMembersSkipClusterSend(channelId string) { 230 a.Srv.Store.User().InvalidateProfilesInChannelCache(channelId) 231 a.Srv.Store.Channel().InvalidateMemberCount(channelId) 232 } 233 234 func (a *App) InvalidateCacheForChannelMembersNotifyProps(channelId string) { 235 a.InvalidateCacheForChannelMembersNotifyPropsSkipClusterSend(channelId) 236 237 if a.Cluster != nil { 238 msg := &model.ClusterMessage{ 239 Event: model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_MEMBERS_NOTIFY_PROPS, 240 SendType: model.CLUSTER_SEND_BEST_EFFORT, 241 Data: channelId, 242 } 243 a.Cluster.SendClusterMessage(msg) 244 } 245 } 246 247 func (a *App) InvalidateCacheForChannelMembersNotifyPropsSkipClusterSend(channelId string) { 248 a.Srv.Store.Channel().InvalidateCacheForChannelMembersNotifyProps(channelId) 249 } 250 251 func (a *App) InvalidateCacheForChannelByNameSkipClusterSend(teamId, name string) { 252 if teamId == "" { 253 teamId = "dm" 254 } 255 256 a.Srv.Store.Channel().InvalidateChannelByName(teamId, name) 257 } 258 259 func (a *App) InvalidateCacheForChannelPosts(channelId string) { 260 a.InvalidateCacheForChannelPostsSkipClusterSend(channelId) 261 262 if a.Cluster != nil { 263 msg := &model.ClusterMessage{ 264 Event: model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_POSTS, 265 SendType: model.CLUSTER_SEND_BEST_EFFORT, 266 Data: channelId, 267 } 268 a.Cluster.SendClusterMessage(msg) 269 } 270 } 271 272 func (a *App) InvalidateCacheForChannelPostsSkipClusterSend(channelId string) { 273 a.Srv.Store.Post().InvalidateLastPostTimeCache(channelId) 274 } 275 276 func (a *App) InvalidateCacheForUser(userId string) { 277 a.InvalidateCacheForUserSkipClusterSend(userId) 278 279 if a.Cluster != nil { 280 msg := &model.ClusterMessage{ 281 Event: model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_USER, 282 SendType: model.CLUSTER_SEND_BEST_EFFORT, 283 Data: userId, 284 } 285 a.Cluster.SendClusterMessage(msg) 286 } 287 } 288 289 func (a *App) InvalidateCacheForUserSkipClusterSend(userId string) { 290 a.Srv.Store.Channel().InvalidateAllChannelMembersForUser(userId) 291 a.Srv.Store.User().InvalidateProfilesInChannelCacheByUser(userId) 292 a.Srv.Store.User().InvalidatProfileCacheForUser(userId) 293 294 if len(a.Hubs) != 0 { 295 a.GetHubForUserId(userId).InvalidateUser(userId) 296 } 297 } 298 299 func (a *App) InvalidateCacheForWebhook(webhookId string) { 300 a.InvalidateCacheForWebhookSkipClusterSend(webhookId) 301 302 if a.Cluster != nil { 303 msg := &model.ClusterMessage{ 304 Event: model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_WEBHOOK, 305 SendType: model.CLUSTER_SEND_BEST_EFFORT, 306 Data: webhookId, 307 } 308 a.Cluster.SendClusterMessage(msg) 309 } 310 } 311 312 func (a *App) InvalidateCacheForWebhookSkipClusterSend(webhookId string) { 313 a.Srv.Store.Webhook().InvalidateWebhookCache(webhookId) 314 } 315 316 func (a *App) InvalidateWebConnSessionCacheForUser(userId string) { 317 if len(a.Hubs) != 0 { 318 a.GetHubForUserId(userId).InvalidateUser(userId) 319 } 320 } 321 322 func (h *Hub) Register(webConn *WebConn) { 323 h.register <- webConn 324 325 if webConn.IsAuthenticated() { 326 webConn.SendHello() 327 } 328 } 329 330 func (h *Hub) Unregister(webConn *WebConn) { 331 select { 332 case h.unregister <- webConn: 333 case <-h.stop: 334 } 335 } 336 337 func (h *Hub) Broadcast(message *model.WebSocketEvent) { 338 if message != nil { 339 h.broadcast <- message 340 } 341 } 342 343 func (h *Hub) InvalidateUser(userId string) { 344 h.invalidateUser <- userId 345 } 346 347 func getGoroutineId() int { 348 var buf [64]byte 349 n := runtime.Stack(buf[:], false) 350 idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0] 351 id, err := strconv.Atoi(idField) 352 if err != nil { 353 id = -1 354 } 355 return id 356 } 357 358 func (h *Hub) Stop() { 359 close(h.stop) 360 <-h.didStop 361 } 362 363 func (h *Hub) Start() { 364 var doStart func() 365 var doRecoverableStart func() 366 var doRecover func() 367 368 doStart = func() { 369 h.goroutineId = getGoroutineId() 370 l4g.Debug("Hub for index %v is starting with goroutine %v", h.connectionIndex, h.goroutineId) 371 372 connections := newHubConnectionIndex() 373 374 for { 375 select { 376 case webCon := <-h.register: 377 connections.Add(webCon) 378 atomic.StoreInt64(&h.connectionCount, int64(len(connections.All()))) 379 case webCon := <-h.unregister: 380 connections.Remove(webCon) 381 382 if len(webCon.UserId) == 0 { 383 continue 384 } 385 386 if len(connections.ForUser(webCon.UserId)) == 0 { 387 h.app.Go(func() { 388 h.app.SetStatusOffline(webCon.UserId, false) 389 }) 390 } 391 case userId := <-h.invalidateUser: 392 for _, webCon := range connections.ForUser(userId) { 393 webCon.InvalidateCache() 394 } 395 case msg := <-h.broadcast: 396 candidates := connections.All() 397 if msg.Broadcast.UserId != "" { 398 candidates = connections.ForUser(msg.Broadcast.UserId) 399 } 400 msg.PrecomputeJSON() 401 for _, webCon := range candidates { 402 if webCon.ShouldSendEvent(msg) { 403 select { 404 case webCon.Send <- msg: 405 default: 406 l4g.Error(fmt.Sprintf("webhub.broadcast: cannot send, closing websocket for userId=%v", webCon.UserId)) 407 close(webCon.Send) 408 connections.Remove(webCon) 409 } 410 } 411 } 412 case <-h.stop: 413 userIds := make(map[string]bool) 414 415 for _, webCon := range connections.All() { 416 userIds[webCon.UserId] = true 417 webCon.Close() 418 } 419 420 for userId := range userIds { 421 h.app.SetStatusOffline(userId, false) 422 } 423 424 h.ExplicitStop = true 425 close(h.didStop) 426 427 return 428 } 429 } 430 } 431 432 doRecoverableStart = func() { 433 defer doRecover() 434 doStart() 435 } 436 437 doRecover = func() { 438 if !h.ExplicitStop { 439 if r := recover(); r != nil { 440 l4g.Error(fmt.Sprintf("Recovering from Hub panic. Panic was: %v", r)) 441 } else { 442 l4g.Error("Webhub stopped unexpectedly. Recovering.") 443 } 444 445 l4g.Error(string(debug.Stack())) 446 447 go doRecoverableStart() 448 } 449 } 450 451 go doRecoverableStart() 452 } 453 454 type hubConnectionIndexIndexes struct { 455 connections int 456 connectionsByUserId int 457 } 458 459 // hubConnectionIndex provides fast addition, removal, and iteration of web connections. 460 type hubConnectionIndex struct { 461 connections []*WebConn 462 connectionsByUserId map[string][]*WebConn 463 connectionIndexes map[*WebConn]*hubConnectionIndexIndexes 464 } 465 466 func newHubConnectionIndex() *hubConnectionIndex { 467 return &hubConnectionIndex{ 468 connections: make([]*WebConn, 0, model.SESSION_CACHE_SIZE), 469 connectionsByUserId: make(map[string][]*WebConn), 470 connectionIndexes: make(map[*WebConn]*hubConnectionIndexIndexes), 471 } 472 } 473 474 func (i *hubConnectionIndex) Add(wc *WebConn) { 475 i.connections = append(i.connections, wc) 476 i.connectionsByUserId[wc.UserId] = append(i.connectionsByUserId[wc.UserId], wc) 477 i.connectionIndexes[wc] = &hubConnectionIndexIndexes{ 478 connections: len(i.connections) - 1, 479 connectionsByUserId: len(i.connectionsByUserId[wc.UserId]) - 1, 480 } 481 } 482 483 func (i *hubConnectionIndex) Remove(wc *WebConn) { 484 indexes, ok := i.connectionIndexes[wc] 485 if !ok { 486 return 487 } 488 489 last := i.connections[len(i.connections)-1] 490 i.connections[indexes.connections] = last 491 i.connections = i.connections[:len(i.connections)-1] 492 i.connectionIndexes[last].connections = indexes.connections 493 494 userConnections := i.connectionsByUserId[wc.UserId] 495 last = userConnections[len(userConnections)-1] 496 userConnections[indexes.connectionsByUserId] = last 497 i.connectionsByUserId[wc.UserId] = userConnections[:len(userConnections)-1] 498 i.connectionIndexes[last].connectionsByUserId = indexes.connectionsByUserId 499 500 delete(i.connectionIndexes, wc) 501 } 502 503 func (i *hubConnectionIndex) ForUser(id string) []*WebConn { 504 return i.connectionsByUserId[id] 505 } 506 507 func (i *hubConnectionIndex) All() []*WebConn { 508 return i.connections 509 }