Add client configurable timeouts

This commit is contained in:
Andrey Sobolev
2025-09-30 13:42:02 +07:00
parent 4a42425301
commit 5d7ed6a4ff
7 changed files with 88 additions and 14 deletions
+39
View File
@@ -0,0 +1,39 @@
/**
* Example: Using custom timeout for BackRPC client
*
* This example demonstrates how to create a network client with custom timeout values.
*/
import { createNetworkClient } from '@hcengineering/network-client'
// Example 1: Default behavior - uses environment-based timeout
// In development (NODE_ENV=development): 3600 seconds (1 hour)
// In production: 3 seconds
const defaultClient = createNetworkClient('localhost:3737')
// Example 2: Custom timeout - 10 minutes
const customTimeoutClient = createNetworkClient('localhost:3737', 600)
// Example 3: Short timeout for fast failure detection - 1 second
const fastFailClient = createNetworkClient('localhost:3737', 1)
// Example 4: Very long timeout for debugging - 2 hours
const debugClient = createNetworkClient('localhost:3737', 7200)
async function main() {
try {
// Wait for connection with optional timeout
await defaultClient.waitConnection(5000) // 5 second connection timeout
console.log('Connected to server')
// The client will maintain connection based on its aliveTimeout
// Server will detect client as dead only after aliveTimeout seconds of inactivity
// Your application logic here...
} catch (error) {
console.error('Connection failed:', error)
}
}
main().catch(console.error)
+12 -4
View File
@@ -525,7 +525,9 @@ describe('backrpc', () => {
},
'localhost',
await server.getPort(),
tickMgr
tickMgr,
undefined,
3 // Set timeout to 3 seconds
)
// Wait for initial connection
@@ -609,7 +611,9 @@ describe('backrpc', () => {
},
'localhost',
await server.getPort(),
tickMgr
tickMgr,
undefined,
3 // Set timeout to 3 seconds
)
// Wait for initial connection
@@ -700,7 +704,9 @@ describe('backrpc', () => {
},
'localhost',
await server.getPort(),
tickMgr
tickMgr,
undefined,
3 // Set timeout to 3 seconds
)
const client2 = new BackRPCClient(
@@ -715,7 +721,9 @@ describe('backrpc', () => {
},
'localhost',
await server.getPort(),
tickMgr
tickMgr,
undefined,
3 // Set timeout to 3 seconds
)
// Wait for initial connections
+6 -2
View File
@@ -50,14 +50,18 @@ export class BackRPCClient<ClientT extends string = ClientId> {
lastPong: number = 0
aliveTimeout: number
constructor (
readonly clientId: ClientT,
readonly client: BackRPCClientHandler,
readonly host: string,
readonly port: number,
readonly tickMgr: TickManager,
options?: zmq.SocketOptions<zmq.Dealer>
options?: zmq.SocketOptions<zmq.Dealer>,
aliveTimeout?: number
) {
this.aliveTimeout = aliveTimeout ?? timeouts.aliveTimeout
this.dealer = new zmq.Dealer({ ...options, context })
this.setServerId = () => {}
@@ -106,7 +110,7 @@ export class BackRPCClient<ClientT extends string = ClientId> {
}
private async sendHello (): Promise<void> {
await this.doSend([backrpcOperations.hello, this.clientId as string, '', ''])
await this.doSend([backrpcOperations.hello, this.clientId as string, JSON.stringify({ aliveTimeout: this.aliveTimeout }), ''])
}
private async start (): Promise<void> {
+18 -3
View File
@@ -39,6 +39,7 @@ interface RPCClientInfo<ClientT extends string> {
requestsTotal: number
requestsTime: number
helloCounter: number
aliveTimeout: number // Per-client timeout in seconds
}
export class BackRPCServer<ClientT extends string = ClientId> {
@@ -93,9 +94,10 @@ export class BackRPCServer<ClientT extends string = ClientId> {
for (const [clientId, clientRecord] of [...this.revClientMapping.entries()]) {
const timeSinceLastSeen = now - clientRecord.lastSeen
if (timeSinceLastSeen > timeouts.aliveTimeout * 1000) {
// Use per-client timeout instead of global timeout
if (timeSinceLastSeen > clientRecord.aliveTimeout * 1000) {
console.warn(
`Client ${clientId} has been inactive for ${Math.round(timeSinceLastSeen / 1000)}s, marking as dead`
`Client ${clientId} has been inactive for ${Math.round(timeSinceLastSeen / 1000)}s (timeout: ${clientRecord.aliveTimeout}s), marking as dead`
)
this.handleClose(clientRecord.id, clientId, true)
}
@@ -177,6 +179,17 @@ export class BackRPCServer<ClientT extends string = ClientId> {
const needHello = !this.clientMapping.has(reqId.toString() as ClientT)
this.clientMapping.set(reqId.toString() as ClientT, clientId)
// Parse the timeout from the hello message
let clientAliveTimeout = timeouts.aliveTimeout
try {
const helloData = JSON.parse(payload.toString())
if (helloData.aliveTimeout !== undefined) {
clientAliveTimeout = helloData.aliveTimeout
}
} catch (err) {
// If parsing fails, use default timeout
}
const clientInfo: RPCClientInfo<ClientT> =
this.revClientMapping.get(clientIdText) ??
({
@@ -185,9 +198,11 @@ export class BackRPCServer<ClientT extends string = ClientId> {
requests: new Set(),
requestsTime: 0,
requestsTotal: 0,
helloCounter: 0
helloCounter: 0,
aliveTimeout: clientAliveTimeout
} satisfies RPCClientInfo<ClientT>)
clientInfo.helloCounter++
clientInfo.aliveTimeout = clientAliveTimeout // Update timeout on reconnection
this.revClientMapping.set(clientIdText, clientInfo)
void this.doSend([clientId, backrpcOperations.hello, this.uuid, ''])
+4 -2
View File
@@ -98,9 +98,11 @@ export class NetworkClientImpl implements NetworkClient {
constructor (
readonly host: string,
port: number,
private readonly tickMgr: TickManager
private readonly tickMgr: TickManager,
aliveTimeout?: number
) {
this.client = new BackRPCClient<ClientUuid>(this.clientId, this, host, port, tickMgr)
const options = undefined
this.client = new BackRPCClient<ClientUuid>(this.clientId, this, host, port, tickMgr, options, aliveTimeout)
}
async waitConnection (timeout?: number): Promise<void> {
+2 -2
View File
@@ -26,11 +26,11 @@ process.on('exit', () => {
shutdownNetworkTickMgr()
})
export function createNetworkClient (url: string): NetworkClient {
export function createNetworkClient (url: string, aliveTimeout?: number): NetworkClient {
const [host, portStr] = url.split(':')
const port = portStr != null ? parseInt(portStr, 10) : 3737
tickMgr.start()
return new NetworkClientImpl(host, port, tickMgr)
return new NetworkClientImpl(host, port, tickMgr, aliveTimeout)
}
export async function createAgent (
+7 -1
View File
@@ -1,5 +1,11 @@
// Check if we're in Node.js development mode
const isDevelopment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'development'
// In development mode with Node.js, use a huge timeout (1 hour) to allow for debugging
const devAliveTimeout = 3600 // 1 hour in seconds
export const timeouts = {
aliveTimeout: 3, // seconds - timeout for detecting dead agents/clients
aliveTimeout: isDevelopment ? devAliveTimeout : 3, // seconds - timeout for detecting dead agents/clients
unusedContainerTimeout: 5, // 10 seconds for container to be terminated because of being unused
pingInterval: 1 // seconds - how often to ping agents
}