mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-04 14:49:01 +02:00
[web] add external 15s heartbeat to socket.io connection (#22853)
* [web] add external 15s heartbeat to socket.io connection * [web] extend debugging context for client errors - include performance.now() timestamp for correlating other timings - include connectionState, especially for new externalHeartbeat detail - include spellCheckLanguage to check on client-side spelling impact * [web] remove unnecessary hook dependency Co-authored-by: Alf Eaton <alf.eaton@overleaf.com> * Refactor externalHeartbeat * Add connectionManager to context * Clear the interval earlier, and on connect * [web] refactor handling of socket debugging info * [web] add split-test for external socket heartbeat * [web] fully remove connectionManager from connection context --------- Co-authored-by: Alf Eaton <alf.eaton@overleaf.com> GitOrigin-RevId: fbebe64f8aa207eb4fd4a8f27d522d1cac35f9d4
This commit is contained in:
@@ -333,6 +333,7 @@ const _ProjectController = {
|
||||
const splitTests = [
|
||||
!anonymous && 'bib-file-tpr-prompt',
|
||||
'compile-log-events',
|
||||
'external-socket-heartbeat',
|
||||
'full-project-search',
|
||||
'math-preview',
|
||||
'null-test-share-modal',
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { ConnectionError, ConnectionState } from './types/connection-state'
|
||||
import {
|
||||
ConnectionError,
|
||||
ConnectionState,
|
||||
ExternalHeartbeat,
|
||||
SocketDebuggingInfo,
|
||||
} from './types/connection-state'
|
||||
import SocketIoShim from '../../../ide/connection/SocketIoShim'
|
||||
import getMeta from '../../../utils/meta'
|
||||
import { Socket } from '@/features/ide-react/connection/types/socket'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||
|
||||
const ONE_HOUR_IN_MS = 1000 * 60 * 60
|
||||
const TWO_MINUTES_IN_MS = 2 * 60 * 1000
|
||||
@@ -18,6 +24,8 @@ const MAX_RECONNECT_GRACEFULLY_INTERVAL_MS = 45 * 1000
|
||||
|
||||
const MAX_RETRY_CONNECT = 5
|
||||
|
||||
const externalSocketHeartbeat = isSplitTestEnabled('external-socket-heartbeat')
|
||||
|
||||
const initialState: ConnectionState = {
|
||||
readyState: WebSocket.CLOSED,
|
||||
forceDisconnected: false,
|
||||
@@ -43,6 +51,12 @@ export class ConnectionManager extends EventTarget {
|
||||
private reconnectCountdownInterval = 0
|
||||
readonly socket: Socket
|
||||
private userIsLeavingPage = false
|
||||
private externalHeartbeatInterval?: number
|
||||
private externalHeartbeat: ExternalHeartbeat = {
|
||||
currentStart: 0,
|
||||
lastSuccess: 0,
|
||||
lastLatency: 0,
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
@@ -61,14 +75,18 @@ export class ConnectionManager extends EventTarget {
|
||||
getMeta('ol-wsUrl') || '/socket.io',
|
||||
window.origin
|
||||
)
|
||||
const query = new URLSearchParams({
|
||||
projectId: getMeta('ol-project_id'),
|
||||
})
|
||||
if (externalSocketHeartbeat) {
|
||||
query.set('esh', '1')
|
||||
}
|
||||
const socket = SocketIoShim.connect(parsedURL.origin, {
|
||||
resource: parsedURL.pathname.slice(1),
|
||||
'auto connect': false,
|
||||
'connect timeout': 30 * 1000,
|
||||
'force new connection': true,
|
||||
query: new URLSearchParams({
|
||||
projectId: getMeta('ol-project_id'),
|
||||
}).toString(),
|
||||
query: query.toString(),
|
||||
reconnect: false,
|
||||
}) as unknown as Socket
|
||||
this.socket = socket
|
||||
@@ -86,6 +104,7 @@ export class ConnectionManager extends EventTarget {
|
||||
return
|
||||
}
|
||||
|
||||
socket.on('connect', () => this.onConnect())
|
||||
socket.on('disconnect', () => this.onDisconnect())
|
||||
socket.on('error', () => this.onConnectError())
|
||||
socket.on('connect_failed', () => this.onConnectError())
|
||||
@@ -112,6 +131,17 @@ export class ConnectionManager extends EventTarget {
|
||||
this.ensureIsConnected()
|
||||
}
|
||||
|
||||
getSocketDebuggingInfo(): SocketDebuggingInfo {
|
||||
return {
|
||||
client_id: this.socket.socket?.sessionid,
|
||||
transport: this.socket.socket?.transport?.name,
|
||||
publicId: this.socket.publicId,
|
||||
lastUserActivity: this.lastUserActivity,
|
||||
connectionState: this.state,
|
||||
externalHeartbeat: this.externalHeartbeat,
|
||||
}
|
||||
}
|
||||
|
||||
private changeState(state: ConnectionState) {
|
||||
const previousState = this.state
|
||||
this.state = state
|
||||
@@ -198,8 +228,24 @@ export class ConnectionManager extends EventTarget {
|
||||
}
|
||||
}
|
||||
|
||||
private onConnect() {
|
||||
if (externalSocketHeartbeat) {
|
||||
if (this.externalHeartbeatInterval) {
|
||||
window.clearInterval(this.externalHeartbeatInterval)
|
||||
}
|
||||
this.externalHeartbeatInterval = window.setInterval(
|
||||
() => this.sendExternalHeartbeat(),
|
||||
15_000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private onDisconnect() {
|
||||
this.connectionAttempt = null
|
||||
if (this.externalHeartbeatInterval) {
|
||||
window.clearInterval(this.externalHeartbeatInterval)
|
||||
}
|
||||
this.externalHeartbeat.currentStart = 0
|
||||
this.changeState({
|
||||
...this.state,
|
||||
readyState: WebSocket.CLOSED,
|
||||
@@ -401,4 +447,20 @@ export class ConnectionManager extends EventTarget {
|
||||
this.tryReconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private sendExternalHeartbeat() {
|
||||
const t0 = performance.now()
|
||||
this.socket.emit('debug.getHostname', () => {
|
||||
if (this.externalHeartbeat.currentStart !== t0) {
|
||||
return
|
||||
}
|
||||
const t1 = performance.now()
|
||||
this.externalHeartbeat = {
|
||||
currentStart: 0,
|
||||
lastSuccess: t1,
|
||||
lastLatency: t1 - t0,
|
||||
}
|
||||
})
|
||||
this.externalHeartbeat.currentStart = t0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,3 +18,18 @@ export type ConnectionState = {
|
||||
lastConnectionAttempt: number
|
||||
error: '' | ConnectionError
|
||||
}
|
||||
|
||||
export type ExternalHeartbeat = {
|
||||
currentStart: number
|
||||
lastSuccess: number
|
||||
lastLatency: number
|
||||
}
|
||||
|
||||
export type SocketDebuggingInfo = {
|
||||
client_id?: string
|
||||
publicId?: string
|
||||
transport?: string
|
||||
lastUserActivity: number
|
||||
connectionState: ConnectionState
|
||||
externalHeartbeat: ExternalHeartbeat
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { ConnectionState } from '../connection/types/connection-state'
|
||||
import {
|
||||
ConnectionState,
|
||||
SocketDebuggingInfo,
|
||||
} from '../connection/types/connection-state'
|
||||
import {
|
||||
ConnectionManager,
|
||||
StateChangeEvent,
|
||||
@@ -25,6 +28,7 @@ type ConnectionContextValue = {
|
||||
tryReconnectNow: () => void
|
||||
registerUserActivity: () => void
|
||||
disconnect: () => void
|
||||
getSocketDebuggingInfo: () => SocketDebuggingInfo
|
||||
}
|
||||
|
||||
export const ConnectionContext = createContext<
|
||||
@@ -75,6 +79,11 @@ export const ConnectionProvider: FC = ({ children }) => {
|
||||
connectionManager.disconnect()
|
||||
}, [connectionManager])
|
||||
|
||||
const getSocketDebuggingInfo = useCallback(
|
||||
() => connectionManager.getSocketDebuggingInfo(),
|
||||
[connectionManager]
|
||||
)
|
||||
|
||||
// Reload the page on force disconnect. Doing this in React-land means that we
|
||||
// can use useLocation(), which provides mockable location methods
|
||||
useEffect(() => {
|
||||
@@ -103,6 +112,7 @@ export const ConnectionProvider: FC = ({ children }) => {
|
||||
tryReconnectNow,
|
||||
registerUserActivity,
|
||||
disconnect,
|
||||
getSocketDebuggingInfo,
|
||||
}),
|
||||
[
|
||||
connectionManager.socket,
|
||||
@@ -113,6 +123,7 @@ export const ConnectionProvider: FC = ({ children }) => {
|
||||
secondsUntilReconnect,
|
||||
tryReconnectNow,
|
||||
disconnect,
|
||||
getSocketDebuggingInfo,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ export const IdeReactProvider: FC = ({ children }) => {
|
||||
// been called
|
||||
const [projectJoined, setProjectJoined] = useState(false)
|
||||
|
||||
const { socket } = useConnectionContext()
|
||||
const { socket, getSocketDebuggingInfo } = useConnectionContext()
|
||||
|
||||
const reportError = useCallback(
|
||||
(error: any, meta?: Record<string, any>) => {
|
||||
@@ -108,11 +108,12 @@ export const IdeReactProvider: FC = ({ children }) => {
|
||||
...meta,
|
||||
user_id: getMeta('ol-user_id'),
|
||||
project_id: projectId,
|
||||
client_id: socket.socket?.sessionid,
|
||||
transport: socket.socket?.transport?.name,
|
||||
client_now: new Date(),
|
||||
performance_now: performance.now(),
|
||||
release,
|
||||
client_load: LOADED_AT,
|
||||
spellCheckLanguage: scopeStore.get('project.spellCheckLanguage'),
|
||||
...getSocketDebuggingInfo(),
|
||||
}
|
||||
|
||||
const errorObj: Record<string, any> = {}
|
||||
@@ -130,7 +131,7 @@ export const IdeReactProvider: FC = ({ children }) => {
|
||||
},
|
||||
})
|
||||
},
|
||||
[socket.socket, release, projectId]
|
||||
[release, projectId, getSocketDebuggingInfo, scopeStore]
|
||||
)
|
||||
|
||||
// Populate scope values when joining project, then fire project:joined event
|
||||
|
||||
@@ -166,9 +166,8 @@ export const ScopeDecorator = (
|
||||
}
|
||||
|
||||
const ConnectionProvider: FC = ({ children }) => {
|
||||
const [value] = useState(() => ({
|
||||
socket: window._ide.socket as Socket,
|
||||
connectionState: {
|
||||
const [value] = useState(() => {
|
||||
const connectionState: ConnectionState = {
|
||||
readyState: WebSocket.OPEN,
|
||||
forceDisconnected: false,
|
||||
inactiveDisconnect: false,
|
||||
@@ -176,14 +175,30 @@ const ConnectionProvider: FC = ({ children }) => {
|
||||
forcedDisconnectDelay: 0,
|
||||
lastConnectionAttempt: 0,
|
||||
error: '',
|
||||
} as ConnectionState,
|
||||
isConnected: true,
|
||||
isStillReconnecting: false,
|
||||
secondsUntilReconnect: () => 0,
|
||||
tryReconnectNow: () => {},
|
||||
registerUserActivity: () => {},
|
||||
disconnect: () => {},
|
||||
}))
|
||||
}
|
||||
return {
|
||||
socket: window._ide.socket as Socket,
|
||||
connectionState,
|
||||
isConnected: true,
|
||||
isStillReconnecting: false,
|
||||
secondsUntilReconnect: () => 0,
|
||||
tryReconnectNow: () => {},
|
||||
registerUserActivity: () => {},
|
||||
disconnect: () => {},
|
||||
getSocketDebuggingInfo: () => ({
|
||||
client_id: 'fakeClientId',
|
||||
transport: 'fakeTransport',
|
||||
publicId: 'fakePublicId',
|
||||
lastUserActivity: 0,
|
||||
connectionState,
|
||||
externalHeartbeat: {
|
||||
currentStart: 0,
|
||||
lastSuccess: 0,
|
||||
lastLatency: 0,
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<ConnectionContext.Provider value={value}>
|
||||
|
||||
Reference in New Issue
Block a user