Преглед изворни кода

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 година
родитељ
комит
84b589719f

+ 1
- 0
config.js Прегледај датотеку

346
 
346
 
347
     // List of undocumented settings used in jitsi-meet
347
     // List of undocumented settings used in jitsi-meet
348
     /**
348
     /**
349
+     _immediateReloadThreshold
349
      autoRecord
350
      autoRecord
350
      autoRecordToken
351
      autoRecordToken
351
      debug
352
      debug

+ 1
- 1
connection.js Прегледај датотеку

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

+ 16
- 0
react/features/analytics/AnalyticsEvents.js Прегледај датотеку

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
  * Creates an event for an action on the deep linking page.
117
  * Creates an event for an action on the deep linking page.
102
  *
118
  *

+ 26
- 1
react/features/app/actions.js Прегледај датотеку

12
 } from '../base/config';
12
 } from '../base/config';
13
 import { setLocationURL } from '../base/connection';
13
 import { setLocationURL } from '../base/connection';
14
 import { loadConfig } from '../base/lib-jitsi-meet';
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
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
18
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
18
 
19
 
20
+const logger = require('jitsi-meet-logger').getLogger(__filename);
21
+
19
 declare var APP: Object;
22
 declare var APP: Object;
20
 
23
 
21
 /**
24
 /**
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
  * Reloads the page by restoring the original URL.
295
  * Reloads the page by restoring the original URL.
271
  *
296
  *

+ 56
- 0
react/features/base/conference/middleware.js Прегледај датотеку

1
 // @flow
1
 // @flow
2
 
2
 
3
+import { reloadNow } from '../../app';
3
 import {
4
 import {
4
     ACTION_PINNED,
5
     ACTION_PINNED,
5
     ACTION_UNPINNED,
6
     ACTION_UNPINNED,
6
     createAudioOnlyChangedEvent,
7
     createAudioOnlyChangedEvent,
8
+    createConnectionEvent,
7
     createPinnedEvent,
9
     createPinnedEvent,
8
     sendAnalytics
10
     sendAnalytics
9
 } from '../../analytics';
11
 } from '../../analytics';
194
  * @returns {Object} The value returned by {@code next(action)}.
196
  * @returns {Object} The value returned by {@code next(action)}.
195
  */
197
  */
196
 function _connectionFailed({ dispatch, getState }, next, action) {
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
     const result = next(action);
207
     const result = next(action);
198
 
208
 
199
     // FIXME: Workaround for the web version. Currently, the creation of the
209
     // FIXME: Workaround for the web version. Currently, the creation of the
235
     return result;
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
  * Notifies the feature base/conference that the action {@code PIN_PARTICIPANT}
295
  * Notifies the feature base/conference that the action {@code PIN_PARTICIPANT}
240
  * is being dispatched within a specific redux store. Pins the specified remote
296
  * is being dispatched within a specific redux store. Pins the specified remote

+ 2
- 1
react/features/base/connection/actionTypes.js Прегледај датотеку

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

+ 17
- 7
react/features/base/connection/actions.native.js Прегледај датотеку

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

+ 10
- 4
react/features/base/connection/reducer.js Прегледај датотеку

65
 
65
 
66
     return assign(state, {
66
     return assign(state, {
67
         connecting: undefined,
67
         connecting: undefined,
68
-        connection: undefined
68
+        connection: undefined,
69
+        timeEstablished: undefined
69
     });
70
     });
70
 }
71
 }
71
 
72
 
81
  */
82
  */
82
 function _connectionEstablished(
83
 function _connectionEstablished(
83
         state: Object,
84
         state: Object,
84
-        { connection }: { connection: Object }) {
85
+        { connection, timeEstablished }: {
86
+            connection: Object,
87
+            timeEstablished: number
88
+        }) {
85
     return assign(state, {
89
     return assign(state, {
86
         connecting: undefined,
90
         connecting: undefined,
87
         connection,
91
         connection,
88
         error: undefined,
92
         error: undefined,
89
-        passwordRequired: undefined
93
+        passwordRequired: undefined,
94
+        timeEstablished
90
     });
95
     });
91
 }
96
 }
92
 
97
 
143
         // done before the new one is established.
148
         // done before the new one is established.
144
         connection: undefined,
149
         connection: undefined,
145
         error: undefined,
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 Прегледај датотеку

1
-import { appNavigate, reloadWithStoredParams } from '../app';
2
-import { toURLString } from '../base/util';
3
-
4
 import {
1
 import {
5
     MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
2
     MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
6
     SET_FATAL_ERROR,
3
     SET_FATAL_ERROR,
7
     SUSPEND_DETECTED
4
     SUSPEND_DETECTED
8
 } from './actionTypes';
5
 } from './actionTypes';
9
 
6
 
10
-const logger = require('jitsi-meet-logger').getLogger(__filename);
11
-
12
 /**
7
 /**
13
  * Signals that the prompt for media permission is visible or not.
8
  * Signals that the prompt for media permission is visible or not.
14
  *
9
  *
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
  * Signals that suspend was detected.
29
  * Signals that suspend was detected.
57
  *
30
  *

+ 2
- 2
react/features/overlay/components/AbstractPageReloadOverlay.js Прегледај датотеку

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

+ 3
- 3
react/features/overlay/components/PageReloadOverlay.native.js Прегледај датотеку

2
 import { Text, View } from 'react-native';
2
 import { Text, View } from 'react-native';
3
 import { connect } from 'react-redux';
3
 import { connect } from 'react-redux';
4
 
4
 
5
-import { appNavigate } from '../../app';
5
+import { appNavigate, reloadNow } from '../../app';
6
 import { translate } from '../../base/i18n';
6
 import { translate } from '../../base/i18n';
7
 import { LoadingIndicator } from '../../base/react';
7
 import { LoadingIndicator } from '../../base/react';
8
 
8
 
9
 import AbstractPageReloadOverlay, { abstractMapStateToProps }
9
 import AbstractPageReloadOverlay, { abstractMapStateToProps }
10
     from './AbstractPageReloadOverlay';
10
     from './AbstractPageReloadOverlay';
11
-import { _reloadNow, setFatalError } from '../actions';
11
+import { setFatalError } from '../actions';
12
 import OverlayFrame from './OverlayFrame';
12
 import OverlayFrame from './OverlayFrame';
13
 import { pageReloadOverlay as styles } from './styles';
13
 import { pageReloadOverlay as styles } from './styles';
14
 
14
 
55
      */
55
      */
56
     _onReloadNow() {
56
     _onReloadNow() {
57
         clearInterval(this._interval);
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 Прегледај датотеку

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

Loading…
Откажи
Сачувај