MM-61904: Make reliable websockets work in HA (#29489)
We do a cluster request to get the active and dead queues from other nodes in the cluster to sync any missing information. We check the dead queue in the other nodes to see if there's been any message loss or not. Accordingly, we send just the active queue or both active and dead queues. There's still an edge case that is left out where a client could have potentially connected and reconnected to multiple nodes leaving multiple active queues in multiple nodes. We don't handle this scenario because then potentially we need to create a slice of sendQueueSize * number_of_nodes. And then this can happen again, leading to an infinite increase in sendQueueSize. We leave this edge-case to Redis, acknowledging a limitation in our architecture. In this PR, when there's no message loss, we just take the active queue from the last node it connected to. And if there's message loss where the client's seqNum is within the last node's dead queue, we also handle that. But if there's severe message loss where the client's seqNum falls within the dead queue of another node, then we just send the data from that node to reconstruct the data as much as possible. It could be possible to set a new connection ID in this case, but this involves more data transfer always from all nodes and recomputing the state in the requestor node. https://mattermost.atlassian.net/browse/MM-61904 ```release-note NONE ``` Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
921604cf39
Коммит
cb75a20c54
@@ -254,7 +254,84 @@ func (ps *PlatformService) SessionIsRegistered(session model.Session) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (ps *PlatformService) CheckWebConn(userID, connectionID string) *CheckConnResult {
|
||||
func (ps *PlatformService) CheckWebConn(userID, connectionID string, seqNum int64) *CheckConnResult {
|
||||
if ps.Cluster() == nil || seqNum == 0 {
|
||||
hub := ps.GetHubForUserId(userID)
|
||||
if hub != nil {
|
||||
return hub.CheckConn(userID, connectionID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// We need some extra care for HA
|
||||
// Check other nodes
|
||||
// If any nodes return with an aq and/or dq, use that.
|
||||
// If all nodes return empty, proceed with local case.
|
||||
// We have to do this because a client might reconnect with an older seq num to a node
|
||||
// which it had connected before. So checking its local queue will lead the server to believe
|
||||
// that there is no msg loss, whereas there is actually loss.
|
||||
queueMap, err := ps.Cluster().GetWSQueues(userID, connectionID, seqNum)
|
||||
if err != nil {
|
||||
// If there is an error we do not have enough data to say anything reliably.
|
||||
// Fall back to unreliable case.
|
||||
ps.Log().Error("Error while getting websocket queues",
|
||||
mlog.String("connection_id", connectionID),
|
||||
mlog.String("user_id", userID),
|
||||
mlog.Int("sequence_number", seqNum),
|
||||
mlog.Err(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
connRes := &CheckConnResult{
|
||||
ConnectionID: connectionID,
|
||||
UserID: userID,
|
||||
}
|
||||
for _, queues := range queueMap {
|
||||
if queues == nil || queues.ActiveQ == nil {
|
||||
continue
|
||||
}
|
||||
// parse the activeq
|
||||
aq := make(chan model.WebSocketMessage, sendQueueSize)
|
||||
for _, aqItem := range queues.ActiveQ {
|
||||
item, err := ps.UnmarshalAQItem(aqItem)
|
||||
if err != nil {
|
||||
ps.Log().Error("Error while unmarshalling websocket message from active queue",
|
||||
mlog.String("connection_id", connectionID),
|
||||
mlog.String("user_id", userID),
|
||||
mlog.Err(err))
|
||||
return nil
|
||||
}
|
||||
// This cannot block because all send queues are of sendQueueSize at max.
|
||||
// TODO: There could be a case where there's severe message loss, and to
|
||||
// reliably get the messages, we need to get send queues from multiple nodes.
|
||||
// We leave that case for Redis.
|
||||
aq <- item
|
||||
}
|
||||
|
||||
connRes.ActiveQueue = aq
|
||||
connRes.ReuseCount = queues.ReuseCount
|
||||
|
||||
// parse the dq, wc.addToDeadQ()
|
||||
if queues.DeadQ != nil {
|
||||
dq, dqPtr, err := ps.UnmarshalDQ(queues.DeadQ)
|
||||
if err != nil {
|
||||
ps.Log().Error("Error while unmarshalling websocket message from dead queue",
|
||||
mlog.String("connection_id", connectionID),
|
||||
mlog.String("user_id", userID),
|
||||
mlog.Err(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
if dqPtr > 0 {
|
||||
connRes.DeadQueue = dq
|
||||
connRes.DeadQueuePointer = dqPtr
|
||||
}
|
||||
}
|
||||
|
||||
return connRes
|
||||
}
|
||||
|
||||
// Now we check local queue
|
||||
hub := ps.GetHubForUserId(userID)
|
||||
if hub != nil {
|
||||
return hub.CheckConn(userID, connectionID)
|
||||
|
||||
Ссылка в новой задаче
Block a user