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  }