Bläddra i källkod

[web] editor connection fixes (#23807)

* [haproxy] add support for xhr-polling in real-time

* [web] try 3 WebSockets then fallback to xhr-polling when (re-)connecting

* [web] ensure old socket.io transport has been closed when reconnecting

* [web] close xhr-polling transport before leaving the page

This will de-register the client, leave the project and clean up the
connected clients tracking ASAP.

* [web] rename constant to avoid confusion

Co-authored-by: Alf Eaton <alf.eaton@overleaf.com>

---------

Co-authored-by: Alf Eaton <alf.eaton@overleaf.com>
GitOrigin-RevId: 55e500c341145947e5589875438fc796394b6ae4
Jakob Ackermann 1 år sedan
förälder
incheckning
4d857b7937

+ 25 - 2
services/web/frontend/js/features/ide-react/connection/connection-manager.ts

@@ -22,7 +22,10 @@ const JOIN_PROJECT_RATE_LIMITED_DELAY = 15 * 1000
 const RECONNECT_GRACEFULLY_RETRY_INTERVAL_MS = 5000
 const RECONNECT_GRACEFULLY_RETRY_INTERVAL_MS = 5000
 const MAX_RECONNECT_GRACEFULLY_INTERVAL_MS = 45 * 1000
 const MAX_RECONNECT_GRACEFULLY_INTERVAL_MS = 45 * 1000
 
 
+const BEFORE_RECONNECT = 'beforeReconnect'
+
 const MAX_RETRY_CONNECT = 5
 const MAX_RETRY_CONNECT = 5
+const RETRY_WEBSOCKET = 3
 
 
 const externalSocketHeartbeat = isSplitTestEnabled('external-socket-heartbeat')
 const externalSocketHeartbeat = isSplitTestEnabled('external-socket-heartbeat')
 
 
@@ -49,6 +52,7 @@ export class ConnectionManager extends EventTarget {
   private protocolVersion = -1
   private protocolVersion = -1
   private readonly idleDisconnectInterval: number
   private readonly idleDisconnectInterval: number
   private reconnectCountdownInterval = 0
   private reconnectCountdownInterval = 0
+  private websocketFailureCount = 0
   readonly socket: Socket
   readonly socket: Socket
   private userIsLeavingPage = false
   private userIsLeavingPage = false
   private externalHeartbeatInterval?: number
   private externalHeartbeatInterval?: number
@@ -69,6 +73,10 @@ export class ConnectionManager extends EventTarget {
     window.addEventListener('online', () => this.onOnline())
     window.addEventListener('online', () => this.onOnline())
     window.addEventListener('beforeunload', () => {
     window.addEventListener('beforeunload', () => {
       this.userIsLeavingPage = true
       this.userIsLeavingPage = true
+      if (this.socket.socket.transport?.name === 'xhr-polling') {
+        // Websockets will close automatically.
+        this.socket.socket.disconnect()
+      }
     })
     })
 
 
     const parsedURL = new URL(
     const parsedURL = new URL(
@@ -106,7 +114,7 @@ export class ConnectionManager extends EventTarget {
     }
     }
 
 
     socket.on('connect', () => this.onConnect())
     socket.on('connect', () => this.onConnect())
-    socket.on('disconnect', () => this.onDisconnect())
+    socket.on('disconnect', (reason: string) => this.onDisconnect(reason))
     socket.on('error', () => this.onConnectError())
     socket.on('error', () => this.onConnectError())
     socket.on('connect_failed', () => this.onConnectError())
     socket.on('connect_failed', () => this.onConnectError())
     socket.on('joinProjectResponse', body => this.onJoinProjectResponse(body))
     socket.on('joinProjectResponse', body => this.onJoinProjectResponse(body))
@@ -210,6 +218,9 @@ export class ConnectionManager extends EventTarget {
   }
   }
 
 
   private onConnectError() {
   private onConnectError() {
+    if (this.socket.socket.transport?.name === 'websocket') {
+      this.websocketFailureCount++
+    }
     if (this.connectionAttempt === null) return // ignore errors once connected.
     if (this.connectionAttempt === null) return // ignore errors once connected.
     if (this.connectionAttempt++ < MAX_RETRY_CONNECT) {
     if (this.connectionAttempt++ < MAX_RETRY_CONNECT) {
       setTimeout(
       setTimeout(
@@ -242,9 +253,12 @@ export class ConnectionManager extends EventTarget {
         15_000
         15_000
       )
       )
     }
     }
+    // Reset on success regardless of transport. We want to upgrade back to websocket on reconnect.
+    this.websocketFailureCount = 0
   }
   }
 
 
-  private onDisconnect() {
+  private onDisconnect(reason: string) {
+    if (reason === BEFORE_RECONNECT) return // triggered from reconnect, ignore.
     this.connectionAttempt = null
     this.connectionAttempt = null
     if (this.externalHeartbeatInterval) {
     if (this.externalHeartbeatInterval) {
       window.clearInterval(this.externalHeartbeatInterval)
       window.clearInterval(this.externalHeartbeatInterval)
@@ -406,6 +420,15 @@ export class ConnectionManager extends EventTarget {
     })
     })
 
 
     this.addReconnectListeners()
     this.addReconnectListeners()
+    this.socket.socket.transports = ['xhr-polling']
+    if (this.websocketFailureCount < RETRY_WEBSOCKET) {
+      this.socket.socket.transports.unshift('websocket')
+    }
+    if (this.socket.socket.connecting || this.socket.socket.connected) {
+      // Ensure the old transport has been cleaned up.
+      // Socket.disconnect() does not accept a parameter. Go one level deeper.
+      this.socket.socket.onDisconnect(BEFORE_RECONNECT)
+    }
     this.socket.socket.connect()
     this.socket.socket.connect()
   }
   }
 
 

+ 4 - 0
services/web/frontend/js/features/ide-react/connection/types/socket.ts

@@ -22,11 +22,15 @@ export type Socket = {
   ): void
   ): void
   socket: {
   socket: {
     connected: boolean
     connected: boolean
+    connecting: boolean
     connect(): void
     connect(): void
+    onDisconnect(reason: string): void
+    disconnect(): void
     sessionid: string
     sessionid: string
     transport?: {
     transport?: {
       name: string
       name: string
     }
     }
+    transports: string[]
   }
   }
   disconnect(): void
   disconnect(): void
 }
 }

+ 1 - 0
services/web/frontend/js/ide/connection/SocketIoShim.js

@@ -15,6 +15,7 @@ class SocketShimBase {
 const transparentMethods = [
 const transparentMethods = [
   'connect',
   'connect',
   'disconnect',
   'disconnect',
+  'onDisconnect',
   'emit',
   'emit',
   'on',
   'on',
   'removeListener',
   'removeListener',