Browse Source

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
master
virtuacoplenny 6 years ago
parent
commit
84b589719f

+ 1
- 0
config.js View File

@@ -346,6 +346,7 @@ var config = {
346 346
 
347 347
     // List of undocumented settings used in jitsi-meet
348 348
     /**
349
+     _immediateReloadThreshold
349 350
      autoRecord
350 351
      autoRecordToken
351 352
      debug

+ 1
- 1
connection.js View File

@@ -132,7 +132,7 @@ function connect(id, password, roomName) {
132 132
          *
133 133
          */
134 134
         function handleConnectionEstablished() {
135
-            APP.store.dispatch(connectionEstablished(connection));
135
+            APP.store.dispatch(connectionEstablished(connection, Date.now()));
136 136
             unsubscribe();
137 137
             resolve(connection);
138 138
         }

+ 16
- 0
react/features/analytics/AnalyticsEvents.js View File

@@ -97,6 +97,22 @@ export function createAudioOnlyChangedEvent(enabled) {
97 97
     };
98 98
 }
99 99
 
100
+/**
101
+ * Creates an event for about the JitsiConnection.
102
+ *
103
+ * @param {string} action - The action that the event represents.
104
+ * @param {boolean} attributes - Additional attributes to attach to the event.
105
+ * @returns {Object} The event in a format suitable for sending via
106
+ * sendAnalytics.
107
+ */
108
+export function createConnectionEvent(action, attributes = {}) {
109
+    return {
110
+        action,
111
+        actionSubject: 'connection',
112
+        attributes
113
+    };
114
+}
115
+
100 116
 /**
101 117
  * Creates an event for an action on the deep linking page.
102 118
  *

+ 26
- 1
react/features/app/actions.js View File

@@ -12,10 +12,13 @@ import {
12 12
 } from '../base/config';
13 13
 import { setLocationURL } from '../base/connection';
14 14
 import { loadConfig } from '../base/lib-jitsi-meet';
15
-import { parseURIString } from '../base/util';
15
+import { parseURIString, toURLString } from '../base/util';
16
+import { setFatalError } from '../overlay';
16 17
 
17 18
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
18 19
 
20
+const logger = require('jitsi-meet-logger').getLogger(__filename);
21
+
19 22
 declare var APP: Object;
20 23
 
21 24
 /**
@@ -266,6 +269,28 @@ export function redirectWithStoredParams(pathname: string) {
266 269
     };
267 270
 }
268 271
 
272
+/**
273
+ * Reloads the page.
274
+ *
275
+ * @protected
276
+ * @returns {Function}
277
+ */
278
+export function reloadNow() {
279
+    return (dispatch: Dispatch<Function>, getState: Function) => {
280
+        dispatch(setFatalError(undefined));
281
+
282
+        const { locationURL } = getState()['features/base/connection'];
283
+
284
+        logger.info(`Reloading the conference using URL: ${locationURL}`);
285
+
286
+        if (navigator.product === 'ReactNative') {
287
+            dispatch(appNavigate(toURLString(locationURL)));
288
+        } else {
289
+            dispatch(reloadWithStoredParams());
290
+        }
291
+    };
292
+}
293
+
269 294
 /**
270 295
  * Reloads the page by restoring the original URL.
271 296
  *

+ 56
- 0
react/features/base/conference/middleware.js View File

@@ -1,9 +1,11 @@
1 1
 // @flow
2 2
 
3
+import { reloadNow } from '../../app';
3 4
 import {
4 5
     ACTION_PINNED,
5 6
     ACTION_UNPINNED,
6 7
     createAudioOnlyChangedEvent,
8
+    createConnectionEvent,
7 9
     createPinnedEvent,
8 10
     sendAnalytics
9 11
 } from '../../analytics';
@@ -194,6 +196,14 @@ function _connectionEstablished({ dispatch }, next, action) {
194 196
  * @returns {Object} The value returned by {@code next(action)}.
195 197
  */
196 198
 function _connectionFailed({ dispatch, getState }, next, action) {
199
+    // In the case of a split-brain error, reload early and prevent further
200
+    // handling of the action.
201
+    if (_isMaybeSplitBrainError(getState, action)) {
202
+        dispatch(reloadNow());
203
+
204
+        return;
205
+    }
206
+
197 207
     const result = next(action);
198 208
 
199 209
     // FIXME: Workaround for the web version. Currently, the creation of the
@@ -235,6 +245,52 @@ function _connectionFailed({ dispatch, getState }, next, action) {
235 245
     return result;
236 246
 }
237 247
 
248
+/**
249
+ * Returns whether or not a CONNECTION_FAILED action is for a possible split
250
+ * brain error. A split brain error occurs when at least two users join a
251
+ * conference on different bridges. It is assumed the split brain scenario
252
+ * occurs very early on in the call.
253
+ *
254
+ * @param {Function} getState - The redux function for fetching the current
255
+ * state.
256
+ * @param {Action} action - The redux action {@code CONNECTION_FAILED} which is
257
+ * being dispatched in the specified {@code store}.
258
+ * @private
259
+ * @returns {boolean}
260
+ */
261
+function _isMaybeSplitBrainError(getState, action) {
262
+    const { error } = action;
263
+    const isShardChangedError = error
264
+        && error.message === 'item-not-found'
265
+        && error.details
266
+        && error.details.shard_changed;
267
+
268
+    if (isShardChangedError) {
269
+        const state = getState();
270
+        const { timeEstablished } = state['features/base/connection'];
271
+        const { _immediateReloadThreshold } = state['features/base/config'];
272
+
273
+        const timeSinceConnectionEstablished
274
+            = timeEstablished && Date.now() - timeEstablished;
275
+        const reloadThreshold = typeof _immediateReloadThreshold === 'number'
276
+            ? _immediateReloadThreshold : 1500;
277
+
278
+        const isWithinSplitBrainThreshold = !timeEstablished
279
+            || timeSinceConnectionEstablished <= reloadThreshold;
280
+
281
+        sendAnalytics(createConnectionEvent('failed', {
282
+            ...error,
283
+            connectionEstablished: timeEstablished,
284
+            splitBrain: isWithinSplitBrainThreshold,
285
+            timeSinceConnectionEstablished
286
+        }));
287
+
288
+        return isWithinSplitBrainThreshold;
289
+    }
290
+
291
+    return false;
292
+}
293
+
238 294
 /**
239 295
  * Notifies the feature base/conference that the action {@code PIN_PARTICIPANT}
240 296
  * is being dispatched within a specific redux store. Pins the specified remote

+ 2
- 1
react/features/base/connection/actionTypes.js View File

@@ -15,7 +15,8 @@ export const CONNECTION_DISCONNECTED = Symbol('CONNECTION_DISCONNECTED');
15 15
  *
16 16
  * {
17 17
  *     type: CONNECTION_ESTABLISHED,
18
- *     connection: JitsiConnection
18
+ *     connection: JitsiConnection,
19
+ *     timeEstablished: number,
19 20
  * }
20 21
  */
21 22
 export const CONNECTION_ESTABLISHED = Symbol('CONNECTION_ESTABLISHED');

+ 17
- 7
react/features/base/connection/actions.native.js View File

@@ -49,7 +49,7 @@ export type ConnectionFailedError = {
49 49
     /**
50 50
      * The details about the connection failed event.
51 51
      */
52
-    details?: string,
52
+    details?: Object,
53 53
 
54 54
     /**
55 55
      * Error message.
@@ -126,7 +126,7 @@ export function connect(id: ?string, password: ?string) {
126 126
             connection.removeEventListener(
127 127
                 JitsiConnectionEvents.CONNECTION_ESTABLISHED,
128 128
                 _onConnectionEstablished);
129
-            dispatch(connectionEstablished(connection));
129
+            dispatch(connectionEstablished(connection, Date.now()));
130 130
         }
131 131
 
132 132
         /**
@@ -138,16 +138,21 @@ export function connect(id: ?string, password: ?string) {
138 138
          * used to authenticate and the authentication failed.
139 139
          * @param {string} [credentials.jid] - The XMPP user's ID.
140 140
          * @param {string} [credentials.password] - The XMPP user's password.
141
+         * @param {Object} details - Additional information about the error.
141 142
          * @private
142 143
          * @returns {void}
143 144
          */
144
-        function _onConnectionFailed(
145
-                err: string, msg: string, credentials: Object) {
145
+        function _onConnectionFailed( // eslint-disable-line max-params
146
+                err: string,
147
+                msg: string,
148
+                credentials: Object,
149
+                details: Object) {
146 150
             unsubscribe();
147 151
             dispatch(
148 152
                 connectionFailed(
149 153
                     connection, {
150 154
                         credentials,
155
+                        details,
151 156
                         name: err,
152 157
                         message: msg
153 158
                     }
@@ -197,16 +202,21 @@ function _connectionDisconnected(connection: Object, message: string) {
197 202
  *
198 203
  * @param {JitsiConnection} connection - The {@code JitsiConnection} which was
199 204
  * established.
205
+ * @param {number} timeEstablished - The time at which the
206
+ * {@code JitsiConnection} which was established.
200 207
  * @public
201 208
  * @returns {{
202 209
  *     type: CONNECTION_ESTABLISHED,
203
- *     connection: JitsiConnection
210
+ *     connection: JitsiConnection,
211
+ *     timeEstablished: number
204 212
  * }}
205 213
  */
206
-export function connectionEstablished(connection: Object) {
214
+export function connectionEstablished(
215
+        connection: Object, timeEstablished: number) {
207 216
     return {
208 217
         type: CONNECTION_ESTABLISHED,
209
-        connection
218
+        connection,
219
+        timeEstablished
210 220
     };
211 221
 }
212 222
 

+ 10
- 4
react/features/base/connection/reducer.js View File

@@ -65,7 +65,8 @@ function _connectionDisconnected(
65 65
 
66 66
     return assign(state, {
67 67
         connecting: undefined,
68
-        connection: undefined
68
+        connection: undefined,
69
+        timeEstablished: undefined
69 70
     });
70 71
 }
71 72
 
@@ -81,12 +82,16 @@ function _connectionDisconnected(
81 82
  */
82 83
 function _connectionEstablished(
83 84
         state: Object,
84
-        { connection }: { connection: Object }) {
85
+        { connection, timeEstablished }: {
86
+            connection: Object,
87
+            timeEstablished: number
88
+        }) {
85 89
     return assign(state, {
86 90
         connecting: undefined,
87 91
         connection,
88 92
         error: undefined,
89
-        passwordRequired: undefined
93
+        passwordRequired: undefined,
94
+        timeEstablished
90 95
     });
91 96
 }
92 97
 
@@ -143,7 +148,8 @@ function _connectionWillConnect(
143 148
         // done before the new one is established.
144 149
         connection: undefined,
145 150
         error: undefined,
146
-        passwordRequired: undefined
151
+        passwordRequired: undefined,
152
+        timeEstablished: undefined
147 153
     });
148 154
 }
149 155
 

+ 0
- 27
react/features/overlay/actions.js View File

@@ -1,14 +1,9 @@
1
-import { appNavigate, reloadWithStoredParams } from '../app';
2
-import { toURLString } from '../base/util';
3
-
4 1
 import {
5 2
     MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
6 3
     SET_FATAL_ERROR,
7 4
     SUSPEND_DETECTED
8 5
 } from './actionTypes';
9 6
 
10
-const logger = require('jitsi-meet-logger').getLogger(__filename);
11
-
12 7
 /**
13 8
  * Signals that the prompt for media permission is visible or not.
14 9
  *
@@ -30,28 +25,6 @@ export function mediaPermissionPromptVisibilityChanged(isVisible, browser) {
30 25
     };
31 26
 }
32 27
 
33
-/**
34
- * Reloads the page.
35
- *
36
- * @protected
37
- * @returns {Function}
38
- */
39
-export function _reloadNow() {
40
-    return (dispatch, getState) => {
41
-        dispatch(setFatalError(undefined));
42
-
43
-        const { locationURL } = getState()['features/base/connection'];
44
-
45
-        logger.info(`Reloading the conference using URL: ${locationURL}`);
46
-
47
-        if (navigator.product === 'ReactNative') {
48
-            dispatch(appNavigate(toURLString(locationURL)));
49
-        } else {
50
-            dispatch(reloadWithStoredParams());
51
-        }
52
-    };
53
-}
54
-
55 28
 /**
56 29
  * Signals that suspend was detected.
57 30
  *

+ 2
- 2
react/features/overlay/components/AbstractPageReloadOverlay.js View File

@@ -7,13 +7,13 @@ import {
7 7
     createPageReloadScheduledEvent,
8 8
     sendAnalytics
9 9
 } from '../../analytics';
10
+import { reloadNow } from '../../app';
10 11
 import {
11 12
     isFatalJitsiConferenceError,
12 13
     isFatalJitsiConnectionError
13 14
 } from '../../base/lib-jitsi-meet';
14 15
 import { randomInt } from '../../base/util';
15 16
 
16
-import { _reloadNow } from '../actions';
17 17
 import ReloadButton from './ReloadButton';
18 18
 
19 19
 declare var APP: Object;
@@ -215,7 +215,7 @@ export default class AbstractPageReloadOverlay extends Component<*, *> {
215 215
                             this._interval = undefined;
216 216
                         }
217 217
 
218
-                        this.props.dispatch(_reloadNow());
218
+                        this.props.dispatch(reloadNow());
219 219
                     } else {
220 220
                         this.setState(prevState => {
221 221
                             return {

+ 3
- 3
react/features/overlay/components/PageReloadOverlay.native.js View File

@@ -2,13 +2,13 @@ import React from 'react';
2 2
 import { Text, View } from 'react-native';
3 3
 import { connect } from 'react-redux';
4 4
 
5
-import { appNavigate } from '../../app';
5
+import { appNavigate, reloadNow } from '../../app';
6 6
 import { translate } from '../../base/i18n';
7 7
 import { LoadingIndicator } from '../../base/react';
8 8
 
9 9
 import AbstractPageReloadOverlay, { abstractMapStateToProps }
10 10
     from './AbstractPageReloadOverlay';
11
-import { _reloadNow, setFatalError } from '../actions';
11
+import { setFatalError } from '../actions';
12 12
 import OverlayFrame from './OverlayFrame';
13 13
 import { pageReloadOverlay as styles } from './styles';
14 14
 
@@ -55,7 +55,7 @@ class PageReloadOverlay extends AbstractPageReloadOverlay {
55 55
      */
56 56
     _onReloadNow() {
57 57
         clearInterval(this._interval);
58
-        this.props.dispatch(_reloadNow());
58
+        this.props.dispatch(reloadNow());
59 59
     }
60 60
 
61 61
     /**

+ 2
- 3
react/features/overlay/components/ReloadButton.js View File

@@ -4,10 +4,9 @@ import PropTypes from 'prop-types';
4 4
 import React, { Component } from 'react';
5 5
 import { connect } from 'react-redux';
6 6
 
7
+import { reloadNow } from '../../app';
7 8
 import { translate } from '../../base/i18n';
8 9
 
9
-import { _reloadNow } from '../actions';
10
-
11 10
 /**
12 11
  * Implements a React Component for button for the overlays that will reload
13 12
  * the page.
@@ -82,7 +81,7 @@ function _mapDispatchToProps(dispatch: Function): Object {
82 81
          * @returns {Object} Dispatched action.
83 82
          */
84 83
         _reloadNow() {
85
-            dispatch(_reloadNow());
84
+            dispatch(reloadNow());
86 85
         }
87 86
     };
88 87
 }

Loading…
Cancel
Save