Refactor screencast websocket connection and retry (#276)

* replace ip with index and retry connection, fixes #252
This commit is contained in:
sua yoo 2022-06-29 17:55:32 -07:00 committed by GitHub
parent 2717a60763
commit 301b05ff4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 130 additions and 77 deletions

View File

@ -36,6 +36,11 @@ type CloseMessage = Message & {
*/
@localized()
export class Screencast extends LitElement {
static baseURL = `${window.location.protocol === "https:" ? "wss" : "ws"}:${
process.env.WEBSOCKET_HOST || window.location.host
}/watch`;
static maxRetries = 10;
static styles = css`
.wrapper {
position: relative;
@ -132,87 +137,77 @@ export class Screencast extends LitElement {
@property({ type: Number })
scale: number = 1;
@property({ type: Array })
watchIPs: string[] = [];
// List of browser screens
@state()
private dataList: Array<ScreencastMessage | null> = [];
@state()
private isConnecting: boolean = false;
@state()
private focusedScreenData?: ScreencastMessage;
// Websocket connections
private wsMap: Map<string, WebSocket> = new Map();
private wsMap: Map<number, WebSocket> = new Map();
// Map data order to screen data
private dataMap: { [index: number]: ScreencastMessage | null } = {};
// Map page ID to data order
private pageOrderMap: Map<string, number> = new Map();
// Number of available browsers.
// Multiply by scale to get available browser window count
private browsersCount = 0;
private browsersCount = 1;
private screenWidth = 640;
private screenHeight = 480;
shouldUpdate(changedProperties: Map<string, any>) {
if (changedProperties.size === 1 && changedProperties.has("watchIPs")) {
// Check stringified value of IP list
return (
this.watchIPs.toString() !==
changedProperties.get("watchIPs").toString()
);
}
return true;
}
private timerIds: number[] = [];
protected firstUpdated() {
this.isConnecting = true;
// Connect to websocket server
this.connectWs();
this.connectAll();
}
async updated(changedProperties: Map<string, any>) {
if (
changedProperties.get("archiveId") ||
changedProperties.get("crawlId") ||
changedProperties.get("watchIPs") ||
changedProperties.get("authToken")
) {
// Reconnect
this.disconnectWs();
this.connectWs();
this.disconnectAll();
this.connectAll();
} else {
const prevScale = changedProperties.get("scale");
if (prevScale) {
if (this.scale > prevScale) {
this.scaleUp();
} else {
this.scaleDown();
}
}
}
}
disconnectedCallback() {
this.disconnectWs();
this.disconnectAll();
this.timerIds.forEach(window.clearTimeout);
super.disconnectedCallback();
}
render() {
const browserWindows = this.browsersCount * this.scale;
return html`
<div class="wrapper">
${this.isConnecting || !this.dataList.length
${!this.dataList.length
? html`<div class="spinner">
<sl-spinner></sl-spinner>
</div> `
: html`
<div class="screen-count">
<span
>${msg(
str`Running in ${
this.browsersCount * this.scale
} browser windows`
)}</span
>${browserWindows > 1
? msg(str`Running in ${browserWindows} browser windows`)
: msg(str`Running in 1 browser window`)}</span
>
<sl-tooltip
content=${msg(
str`${this.browsersCount} browsers × ${this.scale} crawlers. Number of crawlers corresponds to scale.`
str`${this.browsersCount} browser(s) × ${this.scale} crawler(s). Number of crawlers corresponds to scale.`
)}
><sl-icon name="info-circle"></sl-icon
></sl-tooltip>
@ -279,49 +274,58 @@ export class Screencast extends LitElement {
`;
}
private connectWs() {
private scaleUp() {
// Reconnect after 20 second delay
this.timerIds.push(
window.setTimeout(() => {
this.connectAll();
}, 20 * 1000)
);
}
private scaleDown() {
for (let idx = this.wsMap.size - 1; idx > this.scale - 1; idx--) {
const ws = this.wsMap.get(idx);
if (ws) {
ws.close(1000);
this.wsMap.delete(idx);
}
}
}
/**
* Connect to all crawler instances
*/
private connectAll() {
if (!this.archiveId || !this.crawlId) {
return;
}
if (!this.watchIPs?.length) {
console.warn("No watch IPs to connect to");
return;
for (let idx = 0; idx < this.scale; idx++) {
if (!this.wsMap.get(idx)) {
const ws = this.connectWs(idx);
ws.addEventListener("close", (e) => {
if (e.code !== 1000) {
// Not normal closure, try connecting again after 10 sec
this.timerIds.push(
window.setTimeout(() => {
this.retryConnectWs({ index: idx });
}, 10 * 1000)
);
}
});
this.wsMap.set(idx, ws);
}
}
const baseURL = `${window.location.protocol === "https:" ? "wss" : "ws"}:${
process.env.WEBSOCKET_HOST || window.location.host
}/watch/${this.archiveId}/${this.crawlId}`;
this.watchIPs.forEach((ip: string) => {
const ws = new WebSocket(
`${baseURL}/${ip}/ws?auth_bearer=${this.authToken || ""}`
);
ws.addEventListener("open", () => {
if (this.wsMap.size === this.watchIPs.length) {
this.isConnecting = false;
}
});
ws.addEventListener("close", () => {
this.wsMap.delete(ip);
});
ws.addEventListener("error", () => {
this.isConnecting = false;
});
ws.addEventListener("message", ({ data }) => {
this.handleMessage(JSON.parse(data));
});
this.wsMap.set(ip, ws);
});
}
private disconnectWs() {
this.isConnecting = false;
this.wsMap.forEach((ws) => {
ws.close();
private disconnectAll() {
this.wsMap.forEach((ws, i) => {
ws.close(1000);
this.wsMap.delete(i);
});
}
@ -352,10 +356,6 @@ export class Screencast extends LitElement {
return;
}
if (this.isConnecting) {
this.isConnecting = false;
}
let idx = this.pageOrderMap.get(id);
if (idx === undefined) {
@ -387,6 +387,61 @@ export class Screencast extends LitElement {
}
}
/**
* Connect & receive messages from crawler websocket instance
*/
private connectWs(index: number): WebSocket {
const ws = new WebSocket(
`${Screencast.baseURL}/${this.archiveId}/${
this.crawlId
}/${index}/ws?auth_bearer=${this.authToken || ""}`
);
ws.addEventListener("message", ({ data }) => {
this.handleMessage(JSON.parse(data));
});
return ws;
}
/**
* Retry connecting to websocket with exponential backoff
*/
private retryConnectWs(opts: {
index: number;
retries?: number;
delaySec?: number;
}): void {
const { index, retries = 0, delaySec = 10 } = opts;
if (index >= this.scale) {
return;
}
const ws = this.connectWs(index);
ws.addEventListener("close", (e) => {
if (e.code !== 1000) {
// Not normal closure, try connecting again
if (retries < Screencast.maxRetries) {
this.timerIds.push(
window.setTimeout(() => {
this.retryConnectWs({
index,
retries: retries + 1,
delaySec: delaySec * 2,
});
}, delaySec * 1000)
);
} else {
console.error(
`stopping websocket retries, tried ${Screencast.maxRetries} times with ${delaySec} second delay`
);
}
}
});
}
updateDataList() {
this.dataList = Object.values(this.dataMap);
}

View File

@ -482,7 +482,7 @@ export class CrawlDetail extends LiteElement {
return html`
<header class="flex justify-between">
<h3 class="text-lg font-medium mb-2">${msg("Watch Crawl")}</h3>
<h3 class="text-lg font-medium my-2">${msg("Watch Crawl")}</h3>
${isRunning && document.fullscreenEnabled
? html`
<sl-icon-button
@ -508,7 +508,6 @@ export class CrawlDetail extends LiteElement {
archiveId=${this.crawl.aid}
crawlId=${this.crawlId!}
scale=${this.crawl.scale}
.watchIPs=${this.crawl.watchIPs || []}
></btrix-screencast>
</div>
`

View File

@ -25,7 +25,6 @@ export type Crawl = {
fileCount?: number;
fileSize?: number;
completions?: number;
watchIPs?: Array<string>;
};
type SeedConfig = {