Pārlūkot izejas kodu

feat(xmpp): resume the connection when online

The XmppConnection will try to resume the connection
only when the internet is online. This speeds up the process
by avoiding unnecessary connect attempts while offline as each
failed attempt increases the backoff delay.
dev1
paweldomas 5 gadus atpakaļ
vecāks
revīzija
376e5176b3
3 mainītis faili ar 302 papildinājumiem un 34 dzēšanām
  1. 152
    0
      modules/xmpp/ResumeTask.js
  2. 144
    0
      modules/xmpp/ResumeTask.spec.js
  3. 6
    34
      modules/xmpp/XmppConnection.js

+ 152
- 0
modules/xmpp/ResumeTask.js Parādīt failu

@@ -0,0 +1,152 @@
1
+import { getLogger } from 'jitsi-meet-logger';
2
+
3
+import {
4
+    default as NetworkInfo,
5
+    NETWORK_INFO_EVENT
6
+} from '../connectivity/NetworkInfo';
7
+import { getJitterDelay } from '../util/Retry';
8
+
9
+const logger = getLogger(__filename);
10
+
11
+/**
12
+ * The class contains the logic for triggering connection resume via XEP-0198 stream management.
13
+ * It does two things, the first one is it tracks the internet online/offline status and it makes sure that
14
+ * the reconnect is attempted only while online. The seconds thing is that it tracks the retry attempts and extends
15
+ * the retry interval using the full jitter pattern.
16
+ */
17
+export default class ResumeTask {
18
+    /**
19
+     * Initializes new {@code RetryTask}.
20
+     * @param {Strophe.Connection} stropheConnection - The Strophe connection instance.
21
+     */
22
+    constructor(stropheConnection) {
23
+        this._stropheConn = stropheConnection;
24
+
25
+        /**
26
+         * The counter increased before each resume retry attempt, used to calculate exponential backoff.
27
+         * @type {number}
28
+         * @private
29
+         */
30
+        this._resumeRetryN = 0;
31
+
32
+        this._retryDelay = undefined;
33
+    }
34
+
35
+    /**
36
+     * @returns {number|undefined} - How much the app will wait before trying to resume the XMPP connection. When
37
+     * 'undefined' it means that no resume task was not scheduled.
38
+     */
39
+    get retryDelay() {
40
+        return this._retryDelay;
41
+    }
42
+
43
+    /**
44
+     * Called by {@link XmppConnection} when the connection drops and it's a signal it wants to schedule a reconnect.
45
+     *
46
+     * @returns {void}
47
+     */
48
+    schedule() {
49
+        this._cancelResume();
50
+
51
+        this._resumeRetryN += 1;
52
+
53
+        this._networkOnlineListener
54
+            = NetworkInfo.addEventListener(
55
+                NETWORK_INFO_EVENT,
56
+                ({ isOnline }) => {
57
+                    if (isOnline) {
58
+                        this._scheduleResume();
59
+                    } else {
60
+                        this._cancelResume();
61
+                    }
62
+                });
63
+
64
+        NetworkInfo.isOnline() && this._scheduleResume();
65
+    }
66
+
67
+    /**
68
+     * Schedules a delayed timeout which will execute the resume action.
69
+     * @private
70
+     * @returns {void}
71
+     */
72
+    _scheduleResume() {
73
+        if (this._resumeTimeout) {
74
+
75
+            // NO-OP
76
+            return;
77
+        }
78
+
79
+        // The retry delay will be:
80
+        //   1st retry: 1.5s - 3s
81
+        //   2nd retry: 3s - 9s
82
+        //   3rd and next retry: 4.5s - 27s
83
+        this._resumeRetryN = Math.min(3, this._resumeRetryN);
84
+        this._retryDelay = getJitterDelay(
85
+            /* retry */ this._resumeRetryN,
86
+            /* minDelay */ this._resumeRetryN * 1500,
87
+            3);
88
+
89
+        logger.info(`Will try to resume the XMPP connection in ${this.retryDelay}ms`);
90
+
91
+        this._resumeTimeout = setTimeout(() => this._resumeConnection(), this.retryDelay);
92
+    }
93
+
94
+    /**
95
+     * Cancels the delayed resume task.
96
+     *
97
+     * @private
98
+     * @returns {void}
99
+     */
100
+    _cancelResume() {
101
+        if (this._resumeTimeout) {
102
+            logger.info('Canceling connection resume task');
103
+            clearTimeout(this._resumeTimeout);
104
+            this._resumeTimeout = undefined;
105
+            this._retryDelay = undefined;
106
+        }
107
+    }
108
+
109
+    /**
110
+     * Resumes the XMPP connection using the stream management plugin.
111
+     *
112
+     * @private
113
+     * @returns {void}
114
+     */
115
+    _resumeConnection() {
116
+        const { streamManagement } = this._stropheConn;
117
+        const resumeToken = streamManagement.getResumeToken();
118
+
119
+        // Things may have changed since when the task was scheduled
120
+        if (!resumeToken) {
121
+            return;
122
+        }
123
+
124
+        logger.info('Trying to resume the XMPP connection');
125
+
126
+        const url = new URL(this._stropheConn.service);
127
+        let { search } = url;
128
+
129
+        search += search.indexOf('?') === -1 ? `?previd=${resumeToken}` : `&previd=${resumeToken}`;
130
+
131
+        url.search = search;
132
+
133
+        this._stropheConn.service = url.toString();
134
+
135
+        streamManagement.resume();
136
+    }
137
+
138
+    /**
139
+     * Cancels the retry task. It's called by {@link XmppConnection} when it's no longer interested in reconnecting for
140
+     * example when the disconnect method is called.
141
+     *
142
+     * @returns {void}
143
+     */
144
+    cancel() {
145
+        this._cancelResume();
146
+        this._resumeRetryN = 0;
147
+        if (this._networkOnlineListener) {
148
+            this._networkOnlineListener();
149
+            this._networkOnlineListener = null;
150
+        }
151
+    }
152
+}

+ 144
- 0
modules/xmpp/ResumeTask.spec.js Parādīt failu

@@ -0,0 +1,144 @@
1
+import networkInfo, { default as NetworkInfo } from '../connectivity/NetworkInfo';
2
+import { nextTick } from '../util/TestUtils';
3
+
4
+import ResumeTask from './ResumeTask';
5
+
6
+/**
7
+ * A mock of the stream management plugin.
8
+ */
9
+class MockStreamManagement {
10
+    /**
11
+     * @return {string}
12
+     */
13
+    getResumeToken() {
14
+        return '1234';
15
+    }
16
+
17
+    /**
18
+     * @returns {void}
19
+     */
20
+    // eslint-disable-next-line no-empty-function,require-jsdoc
21
+    resume() {
22
+
23
+    }
24
+}
25
+
26
+/**
27
+ * A minimal set of Strophe.Connection class required by the {@link ResumeTask}.
28
+ */
29
+class MockStropheConection {
30
+    /**
31
+     * A constructor.
32
+     */
33
+    constructor() {
34
+        this.streamManagement = new MockStreamManagement();
35
+        this.service = 'wss://something.com/xmpp-websocket';
36
+    }
37
+}
38
+
39
+describe('ResumeTask', () => {
40
+    let connection, resumeTask;
41
+
42
+    beforeEach(() => {
43
+        NetworkInfo.updateNetworkInfo({ isOnline: true });
44
+
45
+        connection = new MockStropheConection();
46
+        resumeTask = new ResumeTask(connection);
47
+
48
+        jasmine.clock().install();
49
+    });
50
+    afterEach(() => {
51
+        jasmine.clock().uninstall();
52
+
53
+        // Need to unregister the listener added to the networkInfo global
54
+        resumeTask.cancel();
55
+    });
56
+    describe('the retry task', () => {
57
+        it('should be scheduled immediately if the internet is online', () => {
58
+            const retrySpy = spyOn(connection.streamManagement, 'resume');
59
+
60
+            resumeTask.schedule();
61
+
62
+            expect(resumeTask.retryDelay).not.toBe(undefined);
63
+
64
+            return nextTick(resumeTask.retryDelay + 10).then(() => {
65
+                expect(retrySpy).toHaveBeenCalled();
66
+            });
67
+        });
68
+        it('should be scheduled when the internet comes back online', () => {
69
+            NetworkInfo.updateNetworkInfo({ isOnline: false });
70
+
71
+            resumeTask.schedule();
72
+
73
+            expect(resumeTask.retryDelay).toBe(undefined);
74
+
75
+            NetworkInfo.updateNetworkInfo({ isOnline: true });
76
+
77
+            expect(resumeTask.retryDelay).not.toBe(undefined);
78
+        });
79
+        it('should not execute first scheduled and then canceled', () => {
80
+            const retrySpy = spyOn(connection.streamManagement, 'resume');
81
+
82
+            resumeTask.schedule();
83
+
84
+            const retryDelay = resumeTask.retryDelay;
85
+
86
+            resumeTask.cancel();
87
+
88
+            return nextTick(retryDelay + 10).then(() => {
89
+                expect(retrySpy).not.toHaveBeenCalled();
90
+            });
91
+        });
92
+        it('should be rescheduled if internet goes offline/online', () => {
93
+            resumeTask.schedule();
94
+
95
+            expect(resumeTask.retryDelay).not.toBe(undefined);
96
+
97
+            NetworkInfo.updateNetworkInfo({ isOnline: false });
98
+
99
+            expect(resumeTask.retryDelay).toBe(undefined);
100
+
101
+            NetworkInfo.updateNetworkInfo({ isOnline: true });
102
+
103
+            expect(resumeTask.retryDelay).not.toBe(undefined);
104
+        });
105
+    });
106
+    describe('the retryDelay', () => {
107
+        const between1and3seconds = delay => delay >= 1000 && delay <= 3000;
108
+        const between3and9seconds = delay => delay >= 3000 && delay <= 9000;
109
+        const between4500msAnd27seconds = delay => delay >= 4500 && delay <= 27000;
110
+
111
+        it('should be between 1 - 3 seconds for the first attempt', () => {
112
+            resumeTask.schedule();
113
+
114
+            expect(between1and3seconds(resumeTask.retryDelay)).toBeTruthy();
115
+        });
116
+        it('should be between 3 - 9 seconds for the second attempt', () => {
117
+            resumeTask.schedule();
118
+            resumeTask.schedule();
119
+
120
+            expect(between3and9seconds(resumeTask.retryDelay)).toBeTruthy(`retryDelay=${resumeTask.retryDelay}`);
121
+        });
122
+        it('should be between 4.5 - 27 seconds for the third attempt', () => {
123
+            resumeTask.schedule();
124
+            resumeTask.schedule();
125
+            resumeTask.schedule();
126
+
127
+            expect(between4500msAnd27seconds(resumeTask.retryDelay)).toBeTruthy();
128
+
129
+            // It should remain within the last range after the 3rd retry
130
+            resumeTask.schedule();
131
+            expect(between4500msAnd27seconds(resumeTask.retryDelay)).toBeTruthy();
132
+        });
133
+        it('should not increase when internet goes offline/online', () => {
134
+            resumeTask.schedule();
135
+
136
+            networkInfo.updateNetworkInfo({ isOnline: false });
137
+            networkInfo.updateNetworkInfo({ isOnline: true });
138
+            networkInfo.updateNetworkInfo({ isOnline: false });
139
+            networkInfo.updateNetworkInfo({ isOnline: true });
140
+
141
+            expect(between1and3seconds(resumeTask.retryDelay)).toBeTruthy();
142
+        });
143
+    });
144
+});

+ 6
- 34
modules/xmpp/XmppConnection.js Parādīt failu

@@ -3,8 +3,8 @@ import { $pres, Strophe } from 'strophe.js';
3 3
 import 'strophejs-plugin-stream-management';
4 4
 
5 5
 import Listenable from '../util/Listenable';
6
-import { getJitterDelay } from '../util/Retry';
7 6
 
7
+import ResumeTask from './ResumeTask';
8 8
 import LastSuccessTracker from './StropheLastSuccess';
9 9
 import PingConnectionPlugin from './strophe.ping';
10 10
 
@@ -53,12 +53,6 @@ export default class XmppConnection extends Listenable {
53 53
             websocketKeepAlive: typeof websocketKeepAlive === 'undefined' ? 4 * 60 * 1000 : Number(websocketKeepAlive)
54 54
         };
55 55
 
56
-        /**
57
-         * The counter increased before each resume retry attempt, used to calculate exponential backoff.
58
-         * @type {number}
59
-         * @private
60
-         */
61
-        this._resumeRetryN = 0;
62 56
         this._stropheConn = new Strophe.Connection(serviceUrl);
63 57
         this._usesWebsocket = serviceUrl.startsWith('ws:') || serviceUrl.startsWith('wss:');
64 58
 
@@ -68,6 +62,8 @@ export default class XmppConnection extends Listenable {
68 62
         this._lastSuccessTracker = new LastSuccessTracker();
69 63
         this._lastSuccessTracker.startTracking(this, this._stropheConn);
70 64
 
65
+        this._resumeTask = new ResumeTask(this._stropheConn);
66
+
71 67
         /**
72 68
          * @typedef DeferredSendIQ Object
73 69
          * @property {Element} iq - The IQ to send.
@@ -255,6 +251,7 @@ export default class XmppConnection extends Listenable {
255 251
             this._maybeEnableStreamResume();
256 252
             this._maybeStartWSKeepAlive();
257 253
             this._processDeferredIQs();
254
+            this._resumeTask.cancel();
258 255
             this.ping.startInterval(this.domain);
259 256
         } else if (status === Strophe.Status.DISCONNECTED) {
260 257
             this.ping.stopInterval();
@@ -302,7 +299,7 @@ export default class XmppConnection extends Listenable {
302 299
      * @returns {void}
303 300
      */
304 301
     disconnect(...args) {
305
-        clearTimeout(this._resumeTimeout);
302
+        this._resumeTask.cancel();
306 303
         clearTimeout(this._wsKeepAlive);
307 304
         this._clearDeferredIQs();
308 305
         this._stropheConn.disconnect(...args);
@@ -555,32 +552,7 @@ export default class XmppConnection extends Listenable {
555 552
         const resumeToken = streamManagement && streamManagement.getResumeToken();
556 553
 
557 554
         if (resumeToken) {
558
-            clearTimeout(this._resumeTimeout);
559
-
560
-            // FIXME detect internet offline
561
-            // The retry delay will be:
562
-            //   1st retry: 1.5s - 3s
563
-            //   2nd retry: 3s - 9s
564
-            //   3rd retry: 3s - 27s
565
-            this._resumeRetryN = Math.min(3, this._resumeRetryN + 1);
566
-            const retryTimeout = getJitterDelay(this._resumeRetryN, 1500, 3);
567
-
568
-            logger.info(`Will try to resume the XMPP connection in ${retryTimeout}ms`);
569
-
570
-            this._resumeTimeout = setTimeout(() => {
571
-                logger.info('Trying to resume the XMPP connection');
572
-
573
-                const url = new URL(this._stropheConn.service);
574
-                let { search } = url;
575
-
576
-                search += search.indexOf('?') === -1 ? `?previd=${resumeToken}` : `&previd=${resumeToken}`;
577
-
578
-                url.search = search;
579
-
580
-                this._stropheConn.service = url.toString();
581
-
582
-                streamManagement.resume();
583
-            }, retryTimeout);
555
+            this._resumeTask.schedule();
584 556
 
585 557
             return true;
586 558
         }

Notiek ielāde…
Atcelt
Saglabāt