2020-08-05 18:38:55 +02:00
|
|
|
/*
|
|
|
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2020-04-20 21:26:39 +02:00
|
|
|
import {createEnum} from "../../utils/enum.js";
|
|
|
|
import {ObservableValue} from "../../observable/ObservableValue.js";
|
2020-04-19 19:02:10 +02:00
|
|
|
|
|
|
|
export const ConnectionStatus = createEnum(
|
|
|
|
"Waiting",
|
|
|
|
"Reconnecting",
|
|
|
|
"Online"
|
|
|
|
);
|
|
|
|
|
|
|
|
export class Reconnector {
|
|
|
|
constructor({retryDelay, createMeasure, onlineStatus}) {
|
|
|
|
this._onlineStatus = onlineStatus;
|
|
|
|
this._retryDelay = retryDelay;
|
|
|
|
this._createTimeMeasure = createMeasure;
|
|
|
|
// assume online, and do our thing when something fails
|
|
|
|
this._state = new ObservableValue(ConnectionStatus.Online);
|
|
|
|
this._isReconnecting = false;
|
|
|
|
this._versionsResponse = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
get lastVersionsResponse() {
|
|
|
|
return this._versionsResponse;
|
|
|
|
}
|
|
|
|
|
|
|
|
get connectionStatus() {
|
|
|
|
return this._state;
|
|
|
|
}
|
|
|
|
|
|
|
|
get retryIn() {
|
|
|
|
if (this._state.get() === ConnectionStatus.Waiting) {
|
|
|
|
return this._retryDelay.nextValue - this._stateSince.measure();
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
async onRequestFailed(hsApi) {
|
2020-05-06 20:58:48 +02:00
|
|
|
if (!this._isReconnecting) {
|
|
|
|
this._isReconnecting = true;
|
|
|
|
|
2020-04-19 19:02:10 +02:00
|
|
|
const onlineStatusSubscription = this._onlineStatus && this._onlineStatus.subscribe(online => {
|
|
|
|
if (online) {
|
|
|
|
this.tryNow();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
try {
|
|
|
|
await this._reconnectLoop(hsApi);
|
|
|
|
} catch (err) {
|
|
|
|
// nothing is catching the error above us,
|
|
|
|
// so just log here
|
|
|
|
console.error(err);
|
|
|
|
} finally {
|
|
|
|
if (onlineStatusSubscription) {
|
|
|
|
// unsubscribe from this._onlineStatus
|
|
|
|
onlineStatusSubscription();
|
|
|
|
}
|
2020-05-06 20:58:48 +02:00
|
|
|
this._isReconnecting = false;
|
2020-04-19 19:02:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
tryNow() {
|
|
|
|
if (this._retryDelay) {
|
|
|
|
// this will interrupt this._retryDelay.waitForRetry() in _reconnectLoop
|
|
|
|
this._retryDelay.abort();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_setState(state) {
|
|
|
|
if (state !== this._state.get()) {
|
|
|
|
if (state === ConnectionStatus.Waiting) {
|
|
|
|
this._stateSince = this._createTimeMeasure();
|
|
|
|
} else {
|
|
|
|
this._stateSince = null;
|
|
|
|
}
|
|
|
|
this._state.set(state);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async _reconnectLoop(hsApi) {
|
|
|
|
this._versionsResponse = null;
|
|
|
|
this._retryDelay.reset();
|
|
|
|
|
|
|
|
while (!this._versionsResponse) {
|
|
|
|
try {
|
|
|
|
this._setState(ConnectionStatus.Reconnecting);
|
2020-05-06 19:38:33 +02:00
|
|
|
// use 30s timeout, as a tradeoff between not giving up
|
|
|
|
// too quickly on a slow server, and not waiting for
|
2020-04-19 19:02:10 +02:00
|
|
|
// a stale connection when we just came online again
|
2020-05-06 19:38:33 +02:00
|
|
|
const versionsRequest = hsApi.versions({timeout: 30000});
|
2020-04-19 19:02:10 +02:00
|
|
|
this._versionsResponse = await versionsRequest.response();
|
|
|
|
this._setState(ConnectionStatus.Online);
|
|
|
|
} catch (err) {
|
2020-05-06 19:38:33 +02:00
|
|
|
if (err.name === "ConnectionError") {
|
2020-04-19 19:02:10 +02:00
|
|
|
this._setState(ConnectionStatus.Waiting);
|
2020-05-05 23:13:41 +02:00
|
|
|
await this._retryDelay.waitForRetry();
|
2020-04-19 19:02:10 +02:00
|
|
|
} else {
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-04-20 21:26:39 +02:00
|
|
|
import {Clock as MockClock} from "../../mocks/Clock.js";
|
|
|
|
import {ExponentialRetryDelay} from "./ExponentialRetryDelay.js";
|
2020-05-06 19:38:33 +02:00
|
|
|
import {ConnectionError} from "../error.js"
|
2020-04-19 19:02:10 +02:00
|
|
|
|
|
|
|
export function tests() {
|
|
|
|
function createHsApiMock(remainingFailures) {
|
|
|
|
return {
|
|
|
|
versions() {
|
|
|
|
return {
|
|
|
|
response() {
|
|
|
|
if (remainingFailures) {
|
|
|
|
remainingFailures -= 1;
|
2020-04-19 19:05:12 +02:00
|
|
|
return Promise.reject(new ConnectionError());
|
2020-04-19 19:02:10 +02:00
|
|
|
} else {
|
|
|
|
return Promise.resolve(42);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
"test reconnecting with 1 failure": async assert => {
|
|
|
|
const clock = new MockClock();
|
|
|
|
const {createMeasure} = clock;
|
|
|
|
const onlineStatus = new ObservableValue(false);
|
2020-04-22 20:48:25 +02:00
|
|
|
const retryDelay = new ExponentialRetryDelay(clock.createTimeout);
|
2020-04-19 19:02:10 +02:00
|
|
|
const reconnector = new Reconnector({retryDelay, onlineStatus, createMeasure});
|
|
|
|
const {connectionStatus} = reconnector;
|
|
|
|
const statuses = [];
|
|
|
|
const subscription = reconnector.connectionStatus.subscribe(s => {
|
|
|
|
statuses.push(s);
|
|
|
|
});
|
|
|
|
reconnector.onRequestFailed(createHsApiMock(1));
|
|
|
|
await connectionStatus.waitFor(s => s === ConnectionStatus.Waiting).promise;
|
|
|
|
clock.elapse(2000);
|
|
|
|
await connectionStatus.waitFor(s => s === ConnectionStatus.Online).promise;
|
|
|
|
assert.deepEqual(statuses, [
|
|
|
|
ConnectionStatus.Reconnecting,
|
|
|
|
ConnectionStatus.Waiting,
|
|
|
|
ConnectionStatus.Reconnecting,
|
|
|
|
ConnectionStatus.Online
|
|
|
|
]);
|
|
|
|
assert.strictEqual(reconnector.lastVersionsResponse, 42);
|
|
|
|
subscription();
|
|
|
|
},
|
|
|
|
"test reconnecting with onlineStatus": async assert => {
|
|
|
|
const clock = new MockClock();
|
|
|
|
const {createMeasure} = clock;
|
|
|
|
const onlineStatus = new ObservableValue(false);
|
2020-04-22 20:48:25 +02:00
|
|
|
const retryDelay = new ExponentialRetryDelay(clock.createTimeout);
|
2020-04-19 19:02:10 +02:00
|
|
|
const reconnector = new Reconnector({retryDelay, onlineStatus, createMeasure});
|
|
|
|
const {connectionStatus} = reconnector;
|
|
|
|
reconnector.onRequestFailed(createHsApiMock(1));
|
|
|
|
await connectionStatus.waitFor(s => s === ConnectionStatus.Waiting).promise;
|
|
|
|
onlineStatus.set(true); //skip waiting
|
|
|
|
await connectionStatus.waitFor(s => s === ConnectionStatus.Online).promise;
|
|
|
|
assert.equal(connectionStatus.get(), ConnectionStatus.Online);
|
|
|
|
assert.strictEqual(reconnector.lastVersionsResponse, 42);
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|