feat(defaults): apply RSocket battle-tested configurations for stability
feat(defaults): apply RSocket battle-tested configurations for stability
This commit is contained in:
+44
-3
@@ -60,6 +60,7 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
|
|||||||
fireInitQueries: true,
|
fireInitQueries: true,
|
||||||
auth: undefined as unknown as AuthenticationState,
|
auth: undefined as unknown as AuthenticationState,
|
||||||
markOnlineOnConnect: true,
|
markOnlineOnConnect: true,
|
||||||
|
// Set to false if you don't need full message history (reduces bandwidth/storage)
|
||||||
syncFullHistory: true,
|
syncFullHistory: true,
|
||||||
patchMessageBeforeSending: msg => msg,
|
patchMessageBeforeSending: msg => msg,
|
||||||
shouldSyncHistoryMessage: () => true,
|
shouldSyncHistoryMessage: () => true,
|
||||||
@@ -79,7 +80,11 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
|
|||||||
cachedGroupMetadata: async () => undefined,
|
cachedGroupMetadata: async () => undefined,
|
||||||
makeSignalRepository: makeLibSignalRepository,
|
makeSignalRepository: makeLibSignalRepository,
|
||||||
// Circuit breaker configuration
|
// Circuit breaker configuration
|
||||||
enableCircuitBreaker: true
|
enableCircuitBreaker: true,
|
||||||
|
// Listener limits (memory leak prevention)
|
||||||
|
// 8 base WS events + 10 dynamic listeners + 2 buffer slots
|
||||||
|
maxWebSocketListeners: 20,
|
||||||
|
maxSocketClientListeners: 50
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MEDIA_PATH_MAP: { [T in MediaType]?: string } = {
|
export const MEDIA_PATH_MAP: { [T in MediaType]?: string } = {
|
||||||
@@ -123,14 +128,50 @@ export const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP) as MediaType[]
|
|||||||
|
|
||||||
export const MIN_PREKEY_COUNT = 5
|
export const MIN_PREKEY_COUNT = 5
|
||||||
|
|
||||||
export const INITIAL_PREKEY_COUNT = 812
|
// Moderate prekey count (upstream uses 812, reduced to balance rate limiting and availability)
|
||||||
|
export const INITIAL_PREKEY_COUNT = 200
|
||||||
|
|
||||||
export const UPLOAD_TIMEOUT = 30000 // 30 seconds
|
export const UPLOAD_TIMEOUT = 30000 // 30 seconds
|
||||||
export const MIN_UPLOAD_INTERVAL = 5000 // 5 seconds minimum between uploads
|
// Moderate upload interval to balance rate limiting and responsiveness (was 5000)
|
||||||
|
export const MIN_UPLOAD_INTERVAL = 10_000 // 10 seconds minimum between uploads
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache TTL configuration (in seconds)
|
||||||
|
*/
|
||||||
export const DEFAULT_CACHE_TTLS = {
|
export const DEFAULT_CACHE_TTLS = {
|
||||||
SIGNAL_STORE: 5 * 60, // 5 minutes
|
SIGNAL_STORE: 5 * 60, // 5 minutes
|
||||||
MSG_RETRY: 60 * 60, // 1 hour
|
MSG_RETRY: 60 * 60, // 1 hour
|
||||||
CALL_OFFER: 5 * 60, // 5 minutes
|
CALL_OFFER: 5 * 60, // 5 minutes
|
||||||
USER_DEVICES: 5 * 60 // 5 minutes
|
USER_DEVICES: 5 * 60 // 5 minutes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum cache keys per store type - prevents memory leaks
|
||||||
|
* Based on RSocket's battle-tested configuration
|
||||||
|
*
|
||||||
|
* Usage: Use these limits when initializing LRU caches to prevent unbounded growth
|
||||||
|
* Example:
|
||||||
|
* import { DEFAULT_CACHE_MAX_KEYS } from './Defaults'
|
||||||
|
* const cache = new LRUCache({ max: DEFAULT_CACHE_MAX_KEYS.SIGNAL_STORE })
|
||||||
|
*/
|
||||||
|
export const DEFAULT_CACHE_MAX_KEYS = {
|
||||||
|
SIGNAL_STORE: 10_000,
|
||||||
|
MSG_RETRY: 10_000,
|
||||||
|
CALL_OFFER: 500,
|
||||||
|
USER_DEVICES: 5_000,
|
||||||
|
PLACEHOLDER_RESEND: 5_000,
|
||||||
|
LID_PER_SOCKET: 2_000,
|
||||||
|
LID_GLOBAL: 10_000
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry configuration with exponential backoff
|
||||||
|
* Delays in milliseconds: 1s, 2s, 5s, 10s, 20s
|
||||||
|
*/
|
||||||
|
export const RETRY_BACKOFF_DELAYS = [1000, 2000, 5000, 10000, 20000]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jitter factor for retry delays (0.15 = ±15% randomization)
|
||||||
|
* Helps prevent thundering herd problem
|
||||||
|
*/
|
||||||
|
export const RETRY_JITTER_FACTOR = 0.15
|
||||||
|
|||||||
@@ -13,7 +13,13 @@ export abstract class AbstractSocketClient extends EventEmitter {
|
|||||||
public config: SocketConfig
|
public config: SocketConfig
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.setMaxListeners(0)
|
// Set max listeners from config (default: 50)
|
||||||
|
// WARNING: 0 disables limit and allows potential memory leaks
|
||||||
|
const maxListeners = this.config.maxSocketClientListeners ?? 50
|
||||||
|
if (maxListeners === 0) {
|
||||||
|
this.config.logger?.warn('SocketClient setMaxListeners(0) allows UNLIMITED listeners - potential memory leak!')
|
||||||
|
}
|
||||||
|
this.setMaxListeners(maxListeners)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract connect(): void
|
abstract connect(): void
|
||||||
|
|||||||
@@ -31,7 +31,13 @@ export class WebSocketClient extends AbstractSocketClient {
|
|||||||
agent: this.config.agent
|
agent: this.config.agent
|
||||||
})
|
})
|
||||||
|
|
||||||
this.socket.setMaxListeners(0)
|
// Set max listeners from config (default: 20)
|
||||||
|
// WARNING: 0 disables limit and allows potential memory leaks
|
||||||
|
const maxListeners = this.config.maxWebSocketListeners ?? 20
|
||||||
|
if (maxListeners === 0) {
|
||||||
|
this.config.logger?.warn('WebSocket setMaxListeners(0) allows UNLIMITED listeners - potential memory leak!')
|
||||||
|
}
|
||||||
|
this.socket.setMaxListeners(maxListeners)
|
||||||
|
|
||||||
const events = ['close', 'error', 'upgrade', 'message', 'open', 'ping', 'pong', 'unexpected-response']
|
const events = ['close', 'error', 'upgrade', 'message', 'open', 'ping', 'pong', 'unexpected-response']
|
||||||
|
|
||||||
|
|||||||
@@ -165,4 +165,19 @@ export type SocketConfig = {
|
|||||||
|
|
||||||
/** Circuit breaker configuration for message operations */
|
/** Circuit breaker configuration for message operations */
|
||||||
messageCircuitBreaker?: Partial<CircuitBreakerOptions>
|
messageCircuitBreaker?: Partial<CircuitBreakerOptions>
|
||||||
|
|
||||||
|
// === Listener Limits (Memory Leak Prevention) ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max WebSocket event listeners (default: 20)
|
||||||
|
* Calculated as: 8 base WS events + 10 dynamic listeners + 2 buffer slots
|
||||||
|
* WARNING: Setting to 0 disables limit and allows potential memory leaks!
|
||||||
|
*/
|
||||||
|
maxWebSocketListeners?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max SocketClient event listeners (default: 50)
|
||||||
|
* WARNING: Setting to 0 disables limit and allows potential memory leaks!
|
||||||
|
*/
|
||||||
|
maxSocketClientListeners?: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
import { EventEmitter } from 'events'
|
import { EventEmitter } from 'events'
|
||||||
import { metrics } from './prometheus-metrics.js'
|
import { metrics } from './prometheus-metrics.js'
|
||||||
import type { CircuitBreaker } from './circuit-breaker.js'
|
import type { CircuitBreaker } from './circuit-breaker.js'
|
||||||
|
import { RETRY_BACKOFF_DELAYS, RETRY_JITTER_FACTOR } from '../Defaults/index.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backoff strategies
|
* Backoff strategies
|
||||||
@@ -631,6 +632,41 @@ export const retryConfigs = {
|
|||||||
jitter: 0.1,
|
jitter: 0.1,
|
||||||
shouldRetry: retryPredicates.onNetworkError,
|
shouldRetry: retryPredicates.onNetworkError,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSocket-style retry (uses RETRY_BACKOFF_DELAYS from Defaults)
|
||||||
|
* Delays: 1s, 2s, 5s, 10s, 20s with jitter
|
||||||
|
*/
|
||||||
|
rsocket: {
|
||||||
|
maxAttempts: RETRY_BACKOFF_DELAYS.length,
|
||||||
|
baseDelay: RETRY_BACKOFF_DELAYS[0],
|
||||||
|
maxDelay: RETRY_BACKOFF_DELAYS[RETRY_BACKOFF_DELAYS.length - 1],
|
||||||
|
backoffStrategy: 'exponential' as const,
|
||||||
|
jitter: RETRY_JITTER_FACTOR,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get retry delay with jitter applied
|
||||||
|
* Uses RETRY_BACKOFF_DELAYS and RETRY_JITTER_FACTOR from Defaults
|
||||||
|
*
|
||||||
|
* @param attempt - Current attempt number (1-based)
|
||||||
|
* @returns Delay in ms with jitter applied
|
||||||
|
*/
|
||||||
|
export function getRetryDelayWithJitter(attempt: number): number {
|
||||||
|
const index = Math.min(Math.max(attempt - 1, 0), RETRY_BACKOFF_DELAYS.length - 1)
|
||||||
|
const baseDelay = RETRY_BACKOFF_DELAYS[index] ?? RETRY_BACKOFF_DELAYS[0] ?? 1000
|
||||||
|
const jitterRange = baseDelay * RETRY_JITTER_FACTOR
|
||||||
|
const jitter = (Math.random() * 2 - 1) * jitterRange // ±15%
|
||||||
|
return Math.round(baseDelay + jitter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all retry delays with jitter for planning
|
||||||
|
* @returns Array of delays with jitter applied
|
||||||
|
*/
|
||||||
|
export function getAllRetryDelaysWithJitter(): number[] {
|
||||||
|
return RETRY_BACKOFF_DELAYS.map((_, i) => getRetryDelayWithJitter(i + 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
export default retry
|
export default retry
|
||||||
|
|||||||
Reference in New Issue
Block a user