diff --git a/examples/custom-timeout-example.ts b/examples/custom-timeout-example.ts new file mode 100644 index 0000000000..99da8167b6 --- /dev/null +++ b/examples/custom-timeout-example.ts @@ -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) diff --git a/packages/backrpc/src/__test__/backrpc.spec.ts b/packages/backrpc/src/__test__/backrpc.spec.ts index a80820565b..f1dde35afe 100644 --- a/packages/backrpc/src/__test__/backrpc.spec.ts +++ b/packages/backrpc/src/__test__/backrpc.spec.ts @@ -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 diff --git a/packages/backrpc/src/client.ts b/packages/backrpc/src/client.ts index 16160d0d8f..8d22b712b1 100644 --- a/packages/backrpc/src/client.ts +++ b/packages/backrpc/src/client.ts @@ -50,14 +50,18 @@ export class BackRPCClient { lastPong: number = 0 + aliveTimeout: number + constructor ( readonly clientId: ClientT, readonly client: BackRPCClientHandler, readonly host: string, readonly port: number, readonly tickMgr: TickManager, - options?: zmq.SocketOptions + options?: zmq.SocketOptions, + aliveTimeout?: number ) { + this.aliveTimeout = aliveTimeout ?? timeouts.aliveTimeout this.dealer = new zmq.Dealer({ ...options, context }) this.setServerId = () => {} @@ -106,7 +110,7 @@ export class BackRPCClient { } private async sendHello (): Promise { - 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 { diff --git a/packages/backrpc/src/server.ts b/packages/backrpc/src/server.ts index 74bb0df035..adb73c2379 100644 --- a/packages/backrpc/src/server.ts +++ b/packages/backrpc/src/server.ts @@ -39,6 +39,7 @@ interface RPCClientInfo { requestsTotal: number requestsTime: number helloCounter: number + aliveTimeout: number // Per-client timeout in seconds } export class BackRPCServer { @@ -93,9 +94,10 @@ export class BackRPCServer { 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 { 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 = this.revClientMapping.get(clientIdText) ?? ({ @@ -185,9 +198,11 @@ export class BackRPCServer { requests: new Set(), requestsTime: 0, requestsTotal: 0, - helloCounter: 0 + helloCounter: 0, + aliveTimeout: clientAliveTimeout } satisfies RPCClientInfo) clientInfo.helloCounter++ + clientInfo.aliveTimeout = clientAliveTimeout // Update timeout on reconnection this.revClientMapping.set(clientIdText, clientInfo) void this.doSend([clientId, backrpcOperations.hello, this.uuid, '']) diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 613011d6a5..a1438c54e7 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -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(this.clientId, this, host, port, tickMgr) + const options = undefined + this.client = new BackRPCClient(this.clientId, this, host, port, tickMgr, options, aliveTimeout) } async waitConnection (timeout?: number): Promise { diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 1a652e5351..181c3d4c34 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -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 ( diff --git a/packages/core/src/api/timeouts.ts b/packages/core/src/api/timeouts.ts index 1da76a7556..f02168af87 100644 --- a/packages/core/src/api/timeouts.ts +++ b/packages/core/src/api/timeouts.ts @@ -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 }