fix(connection): reload immediately on possible split-brain (#3162)
* fix(connection): reload immediately on possible split-brain There isn't an explicit way to know when a split brain scenario has happened. It is assumed it arises when an "item-not-found" connection error is encountered early on in the conference. So, store when a connection has happened so it be calculated how much time has elapsed and if the threshold has not been exceeded then do an immediate reload of the app instead of showing the overlay with a reload timer. * squash: rename isItemNotFoundError -> isShardChangedError
This commit is contained in:
parent
1c6d22b75e
commit
84b589719f
|
@ -346,6 +346,7 @@ var config = {
|
|||
|
||||
// List of undocumented settings used in jitsi-meet
|
||||
/**
|
||||
_immediateReloadThreshold
|
||||
autoRecord
|
||||
autoRecordToken
|
||||
debug
|
||||
|
|
|
@ -132,7 +132,7 @@ function connect(id, password, roomName) {
|
|||
*
|
||||
*/
|
||||
function handleConnectionEstablished() {
|
||||
APP.store.dispatch(connectionEstablished(connection));
|
||||
APP.store.dispatch(connectionEstablished(connection, Date.now()));
|
||||
unsubscribe();
|
||||
resolve(connection);
|
||||
}
|
||||
|
|
|
@ -97,6 +97,22 @@ export function createAudioOnlyChangedEvent(enabled) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event for about the JitsiConnection.
|
||||
*
|
||||
* @param {string} action - The action that the event represents.
|
||||
* @param {boolean} attributes - Additional attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createConnectionEvent(action, attributes = {}) {
|
||||
return {
|
||||
action,
|
||||
actionSubject: 'connection',
|
||||
attributes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event for an action on the deep linking page.
|
||||
*
|
||||
|
|
|
@ -12,10 +12,13 @@ import {
|
|||
} from '../base/config';
|
||||
import { setLocationURL } from '../base/connection';
|
||||
import { loadConfig } from '../base/lib-jitsi-meet';
|
||||
import { parseURIString } from '../base/util';
|
||||
import { parseURIString, toURLString } from '../base/util';
|
||||
import { setFatalError } from '../overlay';
|
||||
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
|
@ -266,6 +269,28 @@ export function redirectWithStoredParams(pathname: string) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the page.
|
||||
*
|
||||
* @protected
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function reloadNow() {
|
||||
return (dispatch: Dispatch<Function>, getState: Function) => {
|
||||
dispatch(setFatalError(undefined));
|
||||
|
||||
const { locationURL } = getState()['features/base/connection'];
|
||||
|
||||
logger.info(`Reloading the conference using URL: ${locationURL}`);
|
||||
|
||||
if (navigator.product === 'ReactNative') {
|
||||
dispatch(appNavigate(toURLString(locationURL)));
|
||||
} else {
|
||||
dispatch(reloadWithStoredParams());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the page by restoring the original URL.
|
||||
*
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
// @flow
|
||||
|
||||
import { reloadNow } from '../../app';
|
||||
import {
|
||||
ACTION_PINNED,
|
||||
ACTION_UNPINNED,
|
||||
createAudioOnlyChangedEvent,
|
||||
createConnectionEvent,
|
||||
createPinnedEvent,
|
||||
sendAnalytics
|
||||
} from '../../analytics';
|
||||
|
@ -194,6 +196,14 @@ function _connectionEstablished({ dispatch }, next, action) {
|
|||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _connectionFailed({ dispatch, getState }, next, action) {
|
||||
// In the case of a split-brain error, reload early and prevent further
|
||||
// handling of the action.
|
||||
if (_isMaybeSplitBrainError(getState, action)) {
|
||||
dispatch(reloadNow());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const result = next(action);
|
||||
|
||||
// FIXME: Workaround for the web version. Currently, the creation of the
|
||||
|
@ -235,6 +245,52 @@ function _connectionFailed({ dispatch, getState }, next, action) {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not a CONNECTION_FAILED action is for a possible split
|
||||
* brain error. A split brain error occurs when at least two users join a
|
||||
* conference on different bridges. It is assumed the split brain scenario
|
||||
* occurs very early on in the call.
|
||||
*
|
||||
* @param {Function} getState - The redux function for fetching the current
|
||||
* state.
|
||||
* @param {Action} action - The redux action {@code CONNECTION_FAILED} which is
|
||||
* being dispatched in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function _isMaybeSplitBrainError(getState, action) {
|
||||
const { error } = action;
|
||||
const isShardChangedError = error
|
||||
&& error.message === 'item-not-found'
|
||||
&& error.details
|
||||
&& error.details.shard_changed;
|
||||
|
||||
if (isShardChangedError) {
|
||||
const state = getState();
|
||||
const { timeEstablished } = state['features/base/connection'];
|
||||
const { _immediateReloadThreshold } = state['features/base/config'];
|
||||
|
||||
const timeSinceConnectionEstablished
|
||||
= timeEstablished && Date.now() - timeEstablished;
|
||||
const reloadThreshold = typeof _immediateReloadThreshold === 'number'
|
||||
? _immediateReloadThreshold : 1500;
|
||||
|
||||
const isWithinSplitBrainThreshold = !timeEstablished
|
||||
|| timeSinceConnectionEstablished <= reloadThreshold;
|
||||
|
||||
sendAnalytics(createConnectionEvent('failed', {
|
||||
...error,
|
||||
connectionEstablished: timeEstablished,
|
||||
splitBrain: isWithinSplitBrainThreshold,
|
||||
timeSinceConnectionEstablished
|
||||
}));
|
||||
|
||||
return isWithinSplitBrainThreshold;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature base/conference that the action {@code PIN_PARTICIPANT}
|
||||
* is being dispatched within a specific redux store. Pins the specified remote
|
||||
|
|
|
@ -15,7 +15,8 @@ export const CONNECTION_DISCONNECTED = Symbol('CONNECTION_DISCONNECTED');
|
|||
*
|
||||
* {
|
||||
* type: CONNECTION_ESTABLISHED,
|
||||
* connection: JitsiConnection
|
||||
* connection: JitsiConnection,
|
||||
* timeEstablished: number,
|
||||
* }
|
||||
*/
|
||||
export const CONNECTION_ESTABLISHED = Symbol('CONNECTION_ESTABLISHED');
|
||||
|
|
|
@ -49,7 +49,7 @@ export type ConnectionFailedError = {
|
|||
/**
|
||||
* The details about the connection failed event.
|
||||
*/
|
||||
details?: string,
|
||||
details?: Object,
|
||||
|
||||
/**
|
||||
* Error message.
|
||||
|
@ -126,7 +126,7 @@ export function connect(id: ?string, password: ?string) {
|
|||
connection.removeEventListener(
|
||||
JitsiConnectionEvents.CONNECTION_ESTABLISHED,
|
||||
_onConnectionEstablished);
|
||||
dispatch(connectionEstablished(connection));
|
||||
dispatch(connectionEstablished(connection, Date.now()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -138,16 +138,21 @@ export function connect(id: ?string, password: ?string) {
|
|||
* used to authenticate and the authentication failed.
|
||||
* @param {string} [credentials.jid] - The XMPP user's ID.
|
||||
* @param {string} [credentials.password] - The XMPP user's password.
|
||||
* @param {Object} details - Additional information about the error.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onConnectionFailed(
|
||||
err: string, msg: string, credentials: Object) {
|
||||
function _onConnectionFailed( // eslint-disable-line max-params
|
||||
err: string,
|
||||
msg: string,
|
||||
credentials: Object,
|
||||
details: Object) {
|
||||
unsubscribe();
|
||||
dispatch(
|
||||
connectionFailed(
|
||||
connection, {
|
||||
credentials,
|
||||
details,
|
||||
name: err,
|
||||
message: msg
|
||||
}
|
||||
|
@ -197,16 +202,21 @@ function _connectionDisconnected(connection: Object, message: string) {
|
|||
*
|
||||
* @param {JitsiConnection} connection - The {@code JitsiConnection} which was
|
||||
* established.
|
||||
* @param {number} timeEstablished - The time at which the
|
||||
* {@code JitsiConnection} which was established.
|
||||
* @public
|
||||
* @returns {{
|
||||
* type: CONNECTION_ESTABLISHED,
|
||||
* connection: JitsiConnection
|
||||
* connection: JitsiConnection,
|
||||
* timeEstablished: number
|
||||
* }}
|
||||
*/
|
||||
export function connectionEstablished(connection: Object) {
|
||||
export function connectionEstablished(
|
||||
connection: Object, timeEstablished: number) {
|
||||
return {
|
||||
type: CONNECTION_ESTABLISHED,
|
||||
connection
|
||||
connection,
|
||||
timeEstablished
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -65,7 +65,8 @@ function _connectionDisconnected(
|
|||
|
||||
return assign(state, {
|
||||
connecting: undefined,
|
||||
connection: undefined
|
||||
connection: undefined,
|
||||
timeEstablished: undefined
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -81,12 +82,16 @@ function _connectionDisconnected(
|
|||
*/
|
||||
function _connectionEstablished(
|
||||
state: Object,
|
||||
{ connection }: { connection: Object }) {
|
||||
{ connection, timeEstablished }: {
|
||||
connection: Object,
|
||||
timeEstablished: number
|
||||
}) {
|
||||
return assign(state, {
|
||||
connecting: undefined,
|
||||
connection,
|
||||
error: undefined,
|
||||
passwordRequired: undefined
|
||||
passwordRequired: undefined,
|
||||
timeEstablished
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -143,7 +148,8 @@ function _connectionWillConnect(
|
|||
// done before the new one is established.
|
||||
connection: undefined,
|
||||
error: undefined,
|
||||
passwordRequired: undefined
|
||||
passwordRequired: undefined,
|
||||
timeEstablished: undefined
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
import { appNavigate, reloadWithStoredParams } from '../app';
|
||||
import { toURLString } from '../base/util';
|
||||
|
||||
import {
|
||||
MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
|
||||
SET_FATAL_ERROR,
|
||||
SUSPEND_DETECTED
|
||||
} from './actionTypes';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Signals that the prompt for media permission is visible or not.
|
||||
*
|
||||
|
@ -30,28 +25,6 @@ export function mediaPermissionPromptVisibilityChanged(isVisible, browser) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the page.
|
||||
*
|
||||
* @protected
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function _reloadNow() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(setFatalError(undefined));
|
||||
|
||||
const { locationURL } = getState()['features/base/connection'];
|
||||
|
||||
logger.info(`Reloading the conference using URL: ${locationURL}`);
|
||||
|
||||
if (navigator.product === 'ReactNative') {
|
||||
dispatch(appNavigate(toURLString(locationURL)));
|
||||
} else {
|
||||
dispatch(reloadWithStoredParams());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that suspend was detected.
|
||||
*
|
||||
|
|
|
@ -7,13 +7,13 @@ import {
|
|||
createPageReloadScheduledEvent,
|
||||
sendAnalytics
|
||||
} from '../../analytics';
|
||||
import { reloadNow } from '../../app';
|
||||
import {
|
||||
isFatalJitsiConferenceError,
|
||||
isFatalJitsiConnectionError
|
||||
} from '../../base/lib-jitsi-meet';
|
||||
import { randomInt } from '../../base/util';
|
||||
|
||||
import { _reloadNow } from '../actions';
|
||||
import ReloadButton from './ReloadButton';
|
||||
|
||||
declare var APP: Object;
|
||||
|
@ -215,7 +215,7 @@ export default class AbstractPageReloadOverlay extends Component<*, *> {
|
|||
this._interval = undefined;
|
||||
}
|
||||
|
||||
this.props.dispatch(_reloadNow());
|
||||
this.props.dispatch(reloadNow());
|
||||
} else {
|
||||
this.setState(prevState => {
|
||||
return {
|
||||
|
|
|
@ -2,13 +2,13 @@ import React from 'react';
|
|||
import { Text, View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { appNavigate } from '../../app';
|
||||
import { appNavigate, reloadNow } from '../../app';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { LoadingIndicator } from '../../base/react';
|
||||
|
||||
import AbstractPageReloadOverlay, { abstractMapStateToProps }
|
||||
from './AbstractPageReloadOverlay';
|
||||
import { _reloadNow, setFatalError } from '../actions';
|
||||
import { setFatalError } from '../actions';
|
||||
import OverlayFrame from './OverlayFrame';
|
||||
import { pageReloadOverlay as styles } from './styles';
|
||||
|
||||
|
@ -55,7 +55,7 @@ class PageReloadOverlay extends AbstractPageReloadOverlay {
|
|||
*/
|
||||
_onReloadNow() {
|
||||
clearInterval(this._interval);
|
||||
this.props.dispatch(_reloadNow());
|
||||
this.props.dispatch(reloadNow());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,10 +4,9 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { reloadNow } from '../../app';
|
||||
import { translate } from '../../base/i18n';
|
||||
|
||||
import { _reloadNow } from '../actions';
|
||||
|
||||
/**
|
||||
* Implements a React Component for button for the overlays that will reload
|
||||
* the page.
|
||||
|
@ -82,7 +81,7 @@ function _mapDispatchToProps(dispatch: Function): Object {
|
|||
* @returns {Object} Dispatched action.
|
||||
*/
|
||||
_reloadNow() {
|
||||
dispatch(_reloadNow());
|
||||
dispatch(reloadNow());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue