-
Notifications
You must be signed in to change notification settings - Fork 57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[MM-57737] Improve client side call state consistency #681
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -888,6 +888,40 @@ func (p *Plugin) handleReconnect(userID, connID, channelID, originalConnID, prev | |
return nil | ||
} | ||
|
||
func (p *Plugin) handleCallStateRequest(channelID, userID, connID string) error { | ||
// We should go through only if the user has permissions to the requested channel | ||
// or if the user is the Calls bot. | ||
if !(p.isBot(userID) || p.API.HasPermissionToChannel(userID, channelID, model.PermissionReadChannel)) { | ||
return fmt.Errorf("forbidden") | ||
} | ||
|
||
// Locking is not ideal but it's the only way to guarantee a race free | ||
// sequence and a consistent state. | ||
// On the client we should make sure to make this request only when strictly | ||
// necessary (i.e first load, joining call, reconnecting). | ||
state, err := p.lockCallReturnState(channelID) | ||
if err != nil { | ||
return fmt.Errorf("failed to lock call: %w", err) | ||
} | ||
defer p.unlockCall(channelID) | ||
|
||
if state == nil { | ||
return fmt.Errorf("no call ongoing") | ||
} | ||
|
||
clientStateData, err := json.Marshal(state.getClientState(p.getBotID(), userID)) | ||
if err != nil { | ||
return fmt.Errorf("failed to marshal client state: %w", err) | ||
} | ||
|
||
p.publishWebSocketEvent(wsEventCallState, map[string]interface{}{ | ||
"channel_id": channelID, | ||
"call": string(clientStateData), | ||
}, &model.WebsocketBroadcast{ConnectionId: connID, ReliableClusterSend: true}) | ||
|
||
return nil | ||
} | ||
|
||
func (p *Plugin) WebSocketMessageHasBeenPosted(connID, userID string, req *model.WebSocketRequest) { | ||
var msg clientMessage | ||
msg.Type = strings.TrimPrefix(req.Action, wsActionPrefix) | ||
|
@@ -896,10 +930,14 @@ func (p *Plugin) WebSocketMessageHasBeenPosted(connID, userID string, req *model | |
us := p.sessions[connID] | ||
p.mut.RUnlock() | ||
|
||
if msg.Type != clientMessageTypeJoin && | ||
msg.Type != clientMessageTypeLeave && | ||
msg.Type != clientMessageTypeReconnect && us == nil { | ||
return | ||
if us == nil { | ||
// Only a few events don't require a user session to exist. For anything else | ||
// we should return. | ||
switch msg.Type { | ||
case clientMessageTypeJoin, clientMessageTypeLeave, clientMessageTypeReconnect, clientMessageTypeCallState: | ||
default: | ||
return | ||
Comment on lines
+936
to
+939
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As an aside, this is just bad language design. (I mean all c-derivatives here, not just go). Imagine you are a non-programmer (or a programmer who's used to fallthrough, or not used to fallthrough, really), is this quick to read through and know immediately what's happening? sheesh. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, I can revert to crazy if conditions if you prefer, just looked cleaner but not a big deal. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nah, no big deal, just complaining. |
||
} | ||
} | ||
|
||
if us != nil && !us.wsMsgLimiter.Allow() { | ||
|
@@ -985,6 +1023,19 @@ func (p *Plugin) WebSocketMessageHasBeenPosted(connID, userID string, req *model | |
p.LogError(err.Error()) | ||
} | ||
|
||
return | ||
case clientMessageTypeCallState: | ||
p.metrics.IncWebSocketEvent("in", "call_state") | ||
|
||
channelID, _ := req.Data["channelID"].(string) | ||
if channelID == "" { | ||
p.LogError("missing channelID") | ||
return | ||
} | ||
|
||
if err := p.handleCallStateRequest(channelID, userID, connID); err != nil { | ||
p.LogError("handleCallStateRequest failed", "err", err.Error(), "userID", userID, "connID", connID) | ||
} | ||
return | ||
case clientMessageTypeSDP: | ||
msgData, ok := req.Data["data"].([]byte) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -163,6 +163,8 @@ export default class ExpandedView extends React.PureComponent<Props, State> { | |
private pushToTalk = false; | ||
private screenPlayer: HTMLVideoElement | null = null; | ||
|
||
static contextType = window.ProductApi.WebSocketProvider; | ||
cpoile marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
#unlockNavigation?: () => void; | ||
|
||
private genStyle: () => Record<string, React.CSSProperties> = () => { | ||
|
@@ -592,9 +594,16 @@ export default class ExpandedView extends React.PureComponent<Props, State> { | |
public componentDidMount() { | ||
const callsClient = getCallsClient(); | ||
if (!callsClient) { | ||
logErr('callsClient should be defined'); | ||
return; | ||
} | ||
|
||
// On mount we request the call state through websocket. This avoids | ||
// making a potentially racy HTTP call and should guarantee | ||
// a consistent state. | ||
logDebug('requesting call state through ws'); | ||
this.context.sendMessage('custom_com.mattermost.calls_call_state', {channelID: callsClient.channelID}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if the websocket message isn't a bit confusing. Are you sending the call's state, or requesting it...? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just being consistent with other events there. We are using the direction (from/to client) to implicitly define whether it's request or response. I know you don't love it :p There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess HTTP fixes the confusion through the Method. Here we use a bit of context as it doesn't make any sense for the client to ever send the call state. Please let me know if this is blocking :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope, not blocking :) |
||
|
||
// keyboard shortcuts | ||
window.addEventListener('keydown', this.handleKBShortcuts, true); | ||
window.addEventListener('keyup', this.handleKeyUp, true); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make much of a difference if we immediately unocked? Looks like there's no reason no to, and it might reduce some of the non-idealness (and prevent the publish ws event from delaying the unlock).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have ways to get the call state without locking but the point here is that we need to queue the event to be sent through WS before unlocking otherwise we are subject to a race again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, of course 🤦